From 2a8f0049a3d9f8470cc93a8971b96ad047053052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 01:34:37 +0000 Subject: [PATCH] handle OSC 52, so "press c to copy" reaches the user's clipboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xterm.js does not handle OSC 52 unless something registers for it, and nothing did. A program offering "press c to copy" emitted the sequence and it vanished, so its confirmation was true about having sent it and false about anything arriving. Found while signing a member into Claude Code on the production host: its first-run login prints an OAuth URL too long to read off a wrapped pane and offers to copy it, "(Copied!)" appeared, and the clipboard was untouched. The URL had to be recovered by running claude under tmux on the server and reassembling it from the captured pane — which is not a thing a member can be asked to do, and first-run login is every new member's first five minutes. Writes only. A lone `?` in the data position is a read request — a program asking the terminal to hand over whatever the user has copied — and it is deliberately not answered: a shell should not be able to exfiltrate the clipboard of the person watching it. The clipboard API needs a secure context and generally a user gesture; the keypress that caused the sequence is that gesture. A refusal is swallowed rather than thrown, since a copy that does not land is the status quo rather than a reason to break the pane. Co-Authored-By: Claude Opus 5 --- .../officerdev/src/apps/Terminal/Terminal.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx b/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx index 1a7ae05b..9de2badc 100644 --- a/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx +++ b/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx @@ -167,6 +167,34 @@ export const TerminalView = ({ // Clickable URLs in output. term.loadAddon(new WebLinksAddon()); + // ── OSC 52: let a program in the terminal put text on the user's clipboard ── + // + // xterm.js does not handle OSC 52 unless something registers for it, and nothing did. Programs that + // offer "press c to copy" emit the sequence and it vanished — so the confirmation they print was true + // about having SENT it and false about anything arriving. Observed while signing a member into Claude + // Code on 2026-08-12: its first-run login prints a URL too long to read off a wrapped pane and offers + // to copy it, "(Copied!)" appeared, and the clipboard was untouched. Without this, first-run login in + // the web terminal cannot be completed at all, which is every new member's first five minutes. + // + // The payload is `;`. A lone `?` in the data position is a READ request — a program + // asking the terminal to hand it whatever the user has copied — and it is deliberately not answered: + // a shell should not be able to exfiltrate the clipboard of the person watching it. Writes only. + // + // `navigator.clipboard` needs a secure context and generally a user gesture; the keypress that caused + // the sequence is that gesture. It can still be refused, so the failure is swallowed rather than + // thrown into the render — a copy that does not land is the status quo, not a reason to break the pane. + term.parser.registerOscHandler(52, (payload) => { + const data = payload.slice(payload.indexOf(';') + 1); + if (!data || data === '?') return true; + try { + const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0)); + void navigator.clipboard?.writeText(new TextDecoder().decode(bytes)); + } catch { + // Malformed base64, no clipboard API, or permission refused. + } + return true; + }); + const searchAddon = new SearchAddon(); term.loadAddon(searchAddon); searchRef.current = searchAddon;