jesseduffield / jesseduffield/lazygit

Feature request: built-in clipboard fallback (tmux / OSC 52) for headless SSH hosts

Open
#5,724 0 comments 2 reactions 0 assignees View on GitHub
enhancement
Dominant language
Go
Stars
82.4k
Forks
3k
Avg merge
2d 18h
Merged PRs (30d)
19

Description

### Is your feature request related to a problem? Please describe.

When I run lazygit on a headless host over SSH, every copy action fails with:

```
No clipboard utilities available. Please install xsel, xclip, wl-clipboard or
Termux:API add-on for termux-clipboard-get/set.
```

This comes from the `atotto/clipboard` dependency: on Linux its `init()` probes for wl-copy/xclip/xsel/termux and, finding none, sets its package-level `Unsupported` flag, after which `WriteAll`/`ReadAll` return an error. On a headless box there's no `DISPLAY`/`WAYLAND_DISPLAY` to begin with, so installing those tools doesn't actually fix anything — there's no display server for them to talk to. The result is that copying branch names, commit hashes, file paths, diffs, etc. just doesn't work in the very common "developing on a remote box over SSH inside tmux" setup.

### Describe the solution you'd like

A built-in clipboard fallback chain in `OSCommand.CopyToClipboard` / `PasteFromClipboard` (`pkg/commands/oscommands/os.go`), used **only** when no `os.copyToClipboardCmd` is configured **and** `clipboard.Unsupported` is true — so any existing setup with a real utility or a configured command is completely unaffected:

1. **tmux** (when `$TMUX` is set and `tmux` is on `PATH`): copy via `tmux load-buffer -w -`, paste via `tmux save-buffer -`. The `-w` flag relays the buffer to the outer terminal's system clipboard via tmux's own OSC 52 (with `set-clipboard on`), and because the tmux buffer is readable this gives **real bidirectional clipboard support**, including paste.
2. **OSC 52 escape** (no tmux, but the terminal supports it): emit the base64-encoded `\x1b]52;c;\x1b\\` sequence. Since OSC 52 read-back is refused by most terminals for security reasons, paste returns the last value copied in-session, matching how Neovim's and Helix's clipboard providers behave.
3. Otherwise, fall through to the current `atotto` path unchanged.

This mirrors what Neovim and Helix already do out of the box, so SSH users get a working clipboard without having to discover and hand-roll an escape-sequence one-liner.

### Describe alternatives you've considered

**1. Wait for upstream `atotto/clipboard` to add OSC 52** — there's an open PR for exactly this: [atotto/clipboard#76](https://github.com/atotto/clipboard/pull/76) ("feat: add osc52 support"). I don't think lazygit can rely on it, for several reasons:

- **It's unmerged and contested.** Open since July 2025 with no CI, and a reviewer has already pushed back on its core design (gating on `SSH_TTY`): *"Conditioning on SSH seems strange... is there a better way to tell OSC-52 should be used?"* It may never merge, or merge in a different shape.
- **It's write-only.** The PR explicitly defers read support ("more complicated... requires reading the terminal for response in an asynchronous manner which can also affect TUI applications"). lazygit's `PasteFromClipboard` is a real feature (e.g. *paste commit message from clipboard*), so a write-only library fix wouldn't cover us.
- **As written, it looks broken.** The implementation is:
```go
func writeOsc52(text string) error {
_, err := os.Stderr.WriteString("\033]52;c;" + text + "\a")
return err
}
```
Three problems with this:
- **No base64 encoding.** OSC 52 requires the payload to be base64-encoded. This writes the raw text, which the terminal will not decode as a valid OSC 52 payload, and it breaks outright on newlines/control characters — i.e. most diffs and multi-line commit messages. This alone makes it unusable for lazygit's copy targets.
- **Writes to `os.Stderr`.** Stderr may be redirected, captured, or not be the controlling terminal, in which case the sequence never reaches the terminal emulator. Writing to the controlling terminal (`/dev/tty`) is the reliable target.
- **No clipboard clearing / empty-payload guard.** An empty `text` produces `\x1b]52;c;\x1b\\`-equivalent, which is a defined request to *clear* the clipboard — an easy way to silently wipe the user's clipboard.

So even depending on the merged result would be premature until at least the base64 issue is fixed.
- **No tmux awareness.** It writes raw OSC 52 to stderr regardless of tmux. Inside tmux that can be swallowed depending on `set-clipboard`, whereas `tmux load-buffer -w` is the robust path and is the only one of the two that gives a working *paste*.
- **Policy belongs in lazygit.** Ordering (prefer tmux for bidirectional support), the gate (only when `clipboard.Unsupported`), and paste-cache semantics are application-level decisions the library can't make for us. Even an ideal upstream fix wouldn't remove the need for lazygit to choose and document this behavior.

**2. `os.copyToClipboardCmd` / `os.readFromClipboardCmd` (today's escape hatch).** This already works and is what I use now:
```yaml
os:
copyToClipboardCmd: "tmux load-buffer -w -"
readFromClipboardCmd: "tmux save-buffer -"
```
or an OSC 52 variant for non-tmux terminals:
```yaml
os:
copyToClipboardCmd: 'printf "\033]52;c;$(printf %s {{text}} | base64 | tr -d "\n")\a" > /dev/tty'
```
Fine as a workaround, but every remote user has to discover it's needed and get the framing right (the OSC 52 one is easy to botch — note it *does* base64-encode, unlike PR #76). Hence the request to make it built-in.

**3. Documentation only.** Add the snippets above to the docs instead of code. Lower risk, but doesn't remove the "broken out of the box on SSH" first impression.

I'd genuinely be happy with the built-in fallback *or* the docs route — I'd like your call on which (if either) you'd consider before writing anything up for review.

### Additional context

A few open questions I'd want your input on before a PR:

- **Gating:** my instinct is to keep the fallback strictly behind `clipboard.Unsupported` (zero behavior change for anyone with a working clipboard), rather than preferring tmux/OSC 52 whenever `$TMUX` is set. Agree?
- **OSC 52 to `/dev/tty` while gocui owns the screen:** in my testing the terminal processes the OSC sequence independently of the alt-screen, but I want to flag it rather than hide it.
- **Paste semantics:** OSC 52 has no reliable read-back, so that fallback can only return a session-local cache of what lazygit itself copied; the tmux fallback has no such limitation. I'd document the difference.

I've prototyped this against current `master` on my own headless dev host: the tmux path is verified end-to-end, the OSC 52 framing is factored into a pure helper and unit-tested, and the fallback selection is covered with tests using the existing `FakeCmdObjRunner`. I'm comfortable in Go and in this part of the codebase, and I'll own the review iterations myself. If you're open to the built-in approach I'll send a focused PR; if you'd prefer docs, I'll do that instead.

#### Why a custom command doesn't solve this

The template suggests trying a custom command first, but the [custom command system](https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium) can't address this, because the clipboard isn't a single action — it's an internal mechanism (`OSCommand.CopyToClipboard` / `PasteFromClipboard`) that's wired into many built-in actions across many contexts: copy branch name, copy commit hash/subject/author/tag, copy file name/path, copy file diff, copy selected text in the staging/patch views, and *paste* commit message from clipboard.

- A `customCommands` entry binds **one key in one context to one shell command**. It can't hook into or override those existing built-in copy actions, so I'd have to re-create every one of them as a separate custom command — and even then I couldn't replace the built-in keybindings or the paste-from-clipboard flow.
- Custom commands operate on **selected model objects** (`{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}`, …), not on the arbitrary internal strings lazygit copies. There's no `{{text}}` placeholder for "the thing lazygit is about to put on the clipboard," so a custom command can't see the value in the general case (e.g. a selected diff hunk or a truncated commit hash).

The one config-level lever that *does* fit is `os.copyToClipboardCmd` / `os.readFromClipboardCmd` (covered as alternative #2 above) — but that's a global override, not a `customCommands` entry, and it still leaves every user to discover and hand-roll it. That's exactly the gap this request is about.

Contributor guide

Open the contributing guide

Research direction

Start in pkg/commands/oscommands/os.go and inspect OSCommand.CopyToClipboard and PasteFromClipboard, then review the existing FakeCmdObjRunner tests. Implement and test the documented fallback ordering for tmux and OSC 52 only when the configured command is absent and clipboard.Unsupported is true, including the stated paste behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cli
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.