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:
- 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.
- OSC 52 escape (no tmux, but the terminal supports it): emit the base64-encoded
\x1b]52;c;<base64>\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.
- 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 ("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:
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:
os:
copyToClipboardCmd: "tmux load-buffer -w -"
readFromClipboardCmd: "tmux save-buffer -"
or an OSC 52 variant for non-tmux terminals:
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 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.
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:
This comes from the
atotto/clipboarddependency: on Linux itsinit()probes for wl-copy/xclip/xsel/termux and, finding none, sets its package-levelUnsupportedflag, after whichWriteAll/ReadAllreturn an error. On a headless box there's noDISPLAY/WAYLAND_DISPLAYto 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 noos.copyToClipboardCmdis configured andclipboard.Unsupportedis true — so any existing setup with a real utility or a configured command is completely unaffected:$TMUXis set andtmuxis onPATH): copy viatmux load-buffer -w -, paste viatmux save-buffer -. The-wflag relays the buffer to the outer terminal's system clipboard via tmux's own OSC 52 (withset-clipboard on), and because the tmux buffer is readable this gives real bidirectional clipboard support, including paste.\x1b]52;c;<base64>\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.atottopath 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/clipboardto add OSC 52 — there's an open PR for exactly this: atotto/clipboard#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
PasteFromClipboardis 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:
Three problems with this:
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.textproduces\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, whereastmux load-buffer -wis 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:or an OSC 52 variant for non-tmux terminals:
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:
clipboard.Unsupported(zero behavior change for anyone with a working clipboard), rather than preferring tmux/OSC 52 whenever$TMUXis set. Agree?/dev/ttywhile 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.I've prototyped this against current
masteron 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 existingFakeCmdObjRunner. 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 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.customCommandsentry 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.{{.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 acustomCommandsentry, and it still leaves every user to discover and hand-roll it. That's exactly the gap this request is about.