[BUG] /share clipboard copy fails over SSH #6772

Open
opened 2026-02-16 18:05:13 -05:00 by yindo · 2 comments
Owner

Originally created by @mmabrouk on GitHub (Jan 19, 2026).

Originally assigned to: @rekram1-node on GitHub.

Description

The /share command fails to copy the share URL to the clipboard when using OpenCode over SSH, even with a terminal that supports OSC 52 clipboard escape sequences (Ghostty in this case).

Observed behavior:

  • Session is created successfully
  • Toast message "Share URL copied to clipboard!" appears
  • Clipboard remains empty - nothing is actually copied
    Expected behavior:
  • The share URL should be copied to the clipboard via OSC 52 escape sequence, which works over SSH when the terminal emulator supports it

Plugins

None

OpenCode version

1.1.25

Steps to reproduce

  1. SSH into a remote Linux server from a terminal that supports OSC 52 (e.g., Ghostty with default settings)
  2. Start OpenCode: opencode
  3. Have a conversation or start a new session
  4. Run /share command
  5. Observe: Toast shows "Share URL copied to clipboard!" but clipboard is empty

Screenshot and/or share link

N/A (cannot share because clipboard copy doesn't work 😅)

Operating System

  • Local machine: macOS with Ghostty 1.2.3 - Remote server: Ubuntu 24.04 (Linux 6.8.0-90-generic)

Terminal

Ghostty 1.2.3 (stable) - connected via SSH to remote Linux server

Originally created by @mmabrouk on GitHub (Jan 19, 2026). Originally assigned to: @rekram1-node on GitHub. ### Description The `/share` command fails to copy the share URL to the clipboard when using OpenCode over SSH, even with a terminal that supports OSC 52 clipboard escape sequences (Ghostty in this case). **Observed behavior:** - Session is created successfully - Toast message "Share URL copied to clipboard!" appears - Clipboard remains empty - nothing is actually copied **Expected behavior:** - The share URL should be copied to the clipboard via OSC 52 escape sequence, which works over SSH when the terminal emulator supports it ### Plugins None ### OpenCode version 1.1.25 ### Steps to reproduce 1. SSH into a remote Linux server from a terminal that supports OSC 52 (e.g., Ghostty with default settings) 2. Start OpenCode: opencode 3. Have a conversation or start a new session 4. Run /share command 5. Observe: Toast shows "Share URL copied to clipboard!" but clipboard is empty ### Screenshot and/or share link N/A (cannot share because clipboard copy doesn't work 😅) ### Operating System - Local machine: macOS with Ghostty 1.2.3 - Remote server: Ubuntu 24.04 (Linux 6.8.0-90-generic) ### Terminal Ghostty 1.2.3 (stable) - connected via SSH to remote Linux server
yindo added the opentuibug labels 2026-02-16 18:05:13 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Jan 19, 2026):

This issue might be a duplicate of existing issues. Please check:

  • #6111: Copy to clipboard broken over SSH (MobaXterm & Windows Terminal)
  • #2773: clipboard copy not works in remote SSH console
  • #8237: copying to clipboard doesn't work in a remote session (like a devcontainer)
  • #8174: Clipboard copy broken on macOS + WezTerm: appends instead of replacing, plays error sound
  • #6110: Wayland clipboard copy doesn't populate primary clipboard selection

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Jan 19, 2026): This issue might be a duplicate of existing issues. Please check: - #6111: Copy to clipboard broken over SSH (MobaXterm & Windows Terminal) - #2773: clipboard copy not works in remote SSH console - #8237: copying to clipboard doesn't work in a remote session (like a devcontainer) - #8174: Clipboard copy broken on macOS + WezTerm: appends instead of replacing, plays error sound - #6110: Wayland clipboard copy doesn't populate primary clipboard selection Feel free to ignore if none of these address your specific case.
Author
Owner

@mmabrouk commented on GitHub (Jan 19, 2026):

I am adding here a bit of research about the issue discovered with the agent:

Root Cause Analysis:

After investigating the source code (packages/opencode/src/cli/cmd/tui/util/clipboard.ts), I found that:

  1. OpenCode correctly implements OSC 52 in the writeOsc52() function (lines 13-21)
  2. The function writes the escape sequence via process.stdout.write(sequence)
  3. However, when the TUI is running, process.stdout appears to be intercepted by the opentui rendering framework
  4. The OSC 52 escape sequence gets captured by the TUI layer instead of being passed through to the terminal emulator
  5. The native fallback (xclip) also fails over SSH because there's no X11 display ($DISPLAY is empty) [this assumes my setup, opencode over simple ssh]

Proof that OSC 52 works in my terminal:

Running this directly in Ghostty successfully copies to clipboard:
printf '\e]52;c;%s\a' "$(echo -n 'test' | base64)"

Suggested fix:

What claude suggest for a fix it to write directly to /dev/tty instead of process.stdout. I don't have much expertise in these systems, so I cannot judge, however it feels there might be lots of edge cases to consider.

Yes, there's a simple fix. Looking at the code:
function writeOsc52(text: string): void {
if (!process.stdout.isTTY) return
const base64 = Buffer.from(text).toString("base64")
const osc52 = \x1b]52;c;${base64}\x07
const passthrough = process.env["TMUX"] || process.env["STY"]
const sequence = passthrough ? \x1bPtmux;\x1b${osc52}\x1b\\ : osc52
process.stdout.write(sequence) // <-- This gets intercepted by TUI
}

The fix: Write directly to /dev/tty instead of process.stdout:

import { openSync, writeSync, closeSync } from "fs"
function writeOsc52(text: string): void {
if (!process.stdout.isTTY) return
const base64 = Buffer.from(text).toString("base64")
const osc52 = \x1b]52;c;${base64}\x07
const passthrough = process.env["TMUX"] || process.env["STY"]
const sequence = passthrough ? \x1bPtmux;\x1b${osc52}\x1b\\ : osc52

try {
const fd = openSync("/dev/tty", "w")
writeSync(fd, sequence)
closeSync(fd)
} catch {
// Fallback to stdout if /dev/tty isn't available
process.stdout.write(sequence)
}
}

Why this works: /dev/tty is a direct handle to the controlling terminal, bypassing any stdout redirection or interception by the TUI framework.

@mmabrouk commented on GitHub (Jan 19, 2026): I am adding here a bit of research about the issue discovered with the agent: **Root Cause Analysis:** After investigating the source code (`packages/opencode/src/cli/cmd/tui/util/clipboard.ts`), I found that: 1. OpenCode correctly implements OSC 52 in the `writeOsc52()` function (lines 13-21) 2. The function writes the escape sequence via `process.stdout.write(sequence)` 3. **However**, when the TUI is running, `process.stdout` appears to be intercepted by the opentui rendering framework 4. The OSC 52 escape sequence gets captured by the TUI layer instead of being passed through to the terminal emulator 5. The native fallback (`xclip`) also fails over SSH because there's no X11 display (`$DISPLAY` is empty) [this assumes my setup, opencode over simple ssh] **Proof that OSC 52 works in my terminal:** Running this directly in Ghostty successfully copies to clipboard: printf '\e]52;c;%s\a' "$(echo -n 'test' | base64)" **Suggested fix:** What claude suggest for a fix it to write directly to `/dev/tty` instead of `process.stdout`. I don't have much expertise in these systems, so I cannot judge, however it feels there might be lots of edge cases to consider. > Yes, there's a simple fix. Looking at the code: > function writeOsc52(text: string): void { > if (!process.stdout.isTTY) return > const base64 = Buffer.from(text).toString("base64") > const osc52 = `\x1b]52;c;${base64}\x07` > const passthrough = process.env["TMUX"] || process.env["STY"] > const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 > process.stdout.write(sequence) // <-- This gets intercepted by TUI > } > > The fix: Write directly to /dev/tty instead of process.stdout: > > import { openSync, writeSync, closeSync } from "fs" > function writeOsc52(text: string): void { > if (!process.stdout.isTTY) return > const base64 = Buffer.from(text).toString("base64") > const osc52 = `\x1b]52;c;${base64}\x07` > const passthrough = process.env["TMUX"] || process.env["STY"] > const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 > > try { > const fd = openSync("/dev/tty", "w") > writeSync(fd, sequence) > closeSync(fd) > } catch { > // Fallback to stdout if /dev/tty isn't available > process.stdout.write(sequence) > } > } > > Why this works: /dev/tty is a direct handle to the controlling terminal, bypassing any stdout redirection or interception by the TUI framework.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#6772