Skip to content

/copy fails over SSH (headless) — consider OSC 52 fallback like OpenCode uses #939

Description

@Magot-777

Summary

/copy fails in every SSH / remote / container session with "Could not reach the
clipboard — is this a headless terminal?", because it writes to the OS clipboard of
the machine the CLI runs on rather than sending the text through the terminal.

This is a daily papercut for remote workflows. Copying agent output is a core loop in
a CLI agent, and today it silently degrades to manual terminal selection over SSH.

Notably, OpenCode handles this correctly using OSC 52 (text travels through the
terminal to the client), so ctrl+y works there in the exact same setup.

Expected Behavior

/copy should work in remote sessions. Two acceptable designs, in order of
preference:

  1. OSC 52 — emit ESC ] 52 ; c ; <base64> BEL when no local clipboard is
    available. The terminal emulator (VS Code, iTerm2, GNOME Terminal) intercepts it
    and writes to the client's clipboard. This is the standard mechanism for remote
    CLI clipboard and needs no dependency.

  2. Reuse the existing terminal-escape path that /terminal-setup already sets up for
    paste, so copy and paste use one consistent mechanism

Actual Behavior

/copy reports "Could not reach the clipboard — is this a headless terminal?" and
copies nothing.

Traced from the shipped bundle (dist/cli.mjs, minified — so function names are
accurate but there are no usable line numbers):

/copy → handleCopy() → writeClipboardText(), which first calls
isClipboardAvailable(). On Linux that reduces to isLinuxClipboardAvailable():

const e = process.env.DISPLAY;
if (e) {
  const t = e.match(/^:(\d+)/);
  return !t || existsSync(`/tmp/.X11-unix/X${t[1]}`);
}
return false;

With no DISPLAY it returns false and no copy is attempted. When it does proceed it
uses @crosscopy/clipboard → setText(), which targets the OS clipboard of the
machine the CLI runs on — the remote host, which has no clipboard.

The terminal-escape infrastructure already exists, for paste:

  • /terminal-setup writes VS Code keybindings using
    workbench.action.terminal.sendSequence (e.g. shift+enter, shift+tab).
  • textOnlyKeyFor() returns a terminal-specific paste sequence when it detects
    terminalType === "vscode", with platform branching for darwin/win32/linux.

So paste already routes through the terminal. Copy appears to be missing the
counterpart.

Steps to reproduce the issue

  1. Connect to a remote Linux host over SSH (Tailscale in my case, but any SSH works),
    from a VS Code integrated terminal using Remote-SSH.
  2. Confirm there is no display:
    echo $DISPLAY      # empty
    ls /tmp/.X11-unix/ # sockets may exist but nothing is listening
    
  3. Start Command Code, get any reply.
  4. Type /copy.
  5. Observe: "Could not reach the clipboard — is this a headless terminal?"

Expected: the reply lands in the local machine's clipboard.

The same session in OpenCode copies successfully with ctrl+y.

Command Code Version

1.66.0

Operating System

Linux

Terminal/IDE

vscode

Shell

bash

Session file (optional)

No response

Fix prompt (optional)

In writeClipboardText(), when isClipboardAvailable() returns false — and more
generally whenever the process is running on a different machine from the terminal
emulator — fall back to OSC 52 instead of failing.

What it lives in: the clipboard path in the CLI bundle, reachable from
handleCopy() (the /copy handler). Today it is:
isClipboardAvailable() → isLinuxClipboardAvailable() guard → dynamic import of
@crosscopy/clipboard → setText(). That last step is what needs a sibling.

What "correct" means:

  • Build ESC ] 52 ; c ; <base64-of-utf8-text> BEL and write it to stdout.
  • Prefer the local-clipboard path when it actually works; use OSC 52 as the fallback
    (or prefer OSC 52 whenever the terminal supports it — isOsc52Supported() in
    OpenCode is a reasonable model).
  • Guard against very large payloads: OSC 52 has practical size limits, so either
    truncate with a warning or keep the local path first so only remote sessions hit
    the escape-sequence path.
  • Keep /copy's existing behaviour of reporting the copied character count.

How to check:

  • Over SSH with DISPLAY unset, /copy should report success and the text should
    paste into an app on the client machine.
  • In a local terminal with a working clipboard, existing behaviour must be unchanged.
  • Unit-test the escape-sequence construction (exact bytes, correct base64 of UTF-8)
    without spawning a terminal.
  • Confirm no regression for the existing textOnlyKeyFor() paste paths.

Additional context

Workarounds considered and rejected:

  • X11 forwarding works, but requires an X server on the client. On modern macOS
    (26.04) that means installing XQuartz, since Apple removed the built-in X server.
    That is a heavy ask for clipboard access, and clipboard-over-X11 is slow.
  • Manual terminal selection works but is unusable for long output.

For comparison, OpenCode ships this as copyToClipboardOSC52(text), guarded by
isOsc52Supported(), in its terminal library — which is why copy works there in the
identical setup.

No session file attached. Happy to provide more detail, test a fix, or follow up.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions