From 3f19e51a7b72aaa4dbcd118ae37789eb207d5d16 Mon Sep 17 00:00:00 2001 From: ru-sh Date: Fri, 18 Sep 2026 10:28:58 +0400 Subject: [PATCH] Make terminal OSC 52 copy reach the system clipboard Programs in the integrated terminal copy by emitting OSC 52. xterm's clipboard addon hands the payload to IClipboardService.writeText with the type 'clipboard', which the browser implementation filed into an in-memory map instead of writing out, so on web the copy silently did nothing. Only the write path changes. readText('clipboard') still returns the in-memory value, so a read query cannot be used to pull the clipboard back into the terminal as input. Needs a secure context: without one navigator.clipboard is undefined and the execCommand fallback cannot run, since OSC 52 arrives with no user gesture. --- CHANGELOG.md | 6 +++++ patches/osc52-clipboard.diff | 43 ++++++++++++++++++++++++++++++++++++ patches/series | 1 + test/e2e/terminal.test.ts | 23 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 patches/osc52-clipboard.diff diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a3585ef0d5..db700a5c7565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ Code v99.99.999 ## Unreleased +### Fixed + +- Copying from the terminal with OSC 52 (used by tmux, vim, Claude Code and + similar) now writes to the system clipboard instead of being silently + discarded. Requires a secure context (HTTPS or localhost). + ## [4.137.0](https://github.com/coder/code-server/releases/tag/v4.137.0) - 2026-09-11 Code v1.137.0 diff --git a/patches/osc52-clipboard.diff b/patches/osc52-clipboard.diff new file mode 100644 index 000000000000..5bce3a2cddd4 --- /dev/null +++ b/patches/osc52-clipboard.diff @@ -0,0 +1,43 @@ +Make terminal OSC 52 copy reach the system clipboard + +Programs running in the integrated terminal (tmux, vim, Claude Code...) copy by +emitting OSC 52. xterm's clipboard addon is always loaded and hands the payload +to IClipboardService.writeText(text, 'clipboard') (see xtermTerminal.ts), but +BrowserClipboardService files *any* typed write into an in-memory map and never +touches navigator.clipboard, so on web the copy silently does nothing. + +'clipboard' is not an alternate clipboard, it is the system one, so let it fall +through. 'selection' (OSC 52 's'/'p', the X11 primary selection) keeps the +in-memory behaviour since browsers have no equivalent. + +Only the write path is changed. readText('clipboard') deliberately keeps +returning the in-memory value, so an OSC 52 read query (ESC]52;c;? BEL) cannot +be used by a remote program to exfiltrate the user's clipboard back into the +terminal as input. + +This needs a secure context (HTTPS or localhost); without one +navigator.clipboard is undefined and the execCommand fallback cannot run, +because OSC 52 arrives asynchronously with no user gesture attached. + +To reproduce: open a terminal and run + printf '\033]52;c;%s\a' "$(printf hello | base64)" +then paste. Before this patch the clipboard is unchanged. + +Fixes the web half of https://github.com/microsoft/vscode/issues/318254 -- drop +this patch once that lands upstream. +Index: code-server/lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts ++++ code-server/lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts +@@ -124,8 +124,9 @@ export class BrowserClipboardService ext + // Clear resources given we are writing text + this.clearResourcesState(); + +- // With type: only in-memory is supported +- if (type) { ++ // With type: only in-memory is supported, except for 'clipboard' which ++ // targets the system clipboard and is handled below. ++ if (type && type !== 'clipboard') { + this.mapTextToType.set(type, text); + this.logService.trace('BrowserClipboardService#writeText'); + return; diff --git a/patches/series b/patches/series index 5b548e3900dd..80a08e4e6346 100644 --- a/patches/series +++ b/patches/series @@ -25,3 +25,4 @@ signature-verification.diff copilot.diff app-name.diff csp-hashes.diff +osc52-clipboard.diff diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index 0de4f222a2a3..1c15b0553995 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -30,6 +30,29 @@ describe("Integrated Terminal", ["--disable-workspace-trust"], {}, () => { expect(stdout).toMatch(address) }) + // Programs in the terminal (tmux, vim, Claude Code...) copy by emitting OSC + // 52. VS Code hands that to the clipboard service with a type, which the + // browser implementation used to keep in memory instead of writing out. + test("should copy to the clipboard via OSC 52", async ({ codeServerPage }) => { + const origin = new URL(codeServerPage.page.url()).origin + await codeServerPage.page.context().grantPermissions(["clipboard-read", "clipboard-write"], { origin }) + + const expected = `osc52-${Date.now()}` + const encoded = Buffer.from(expected).toString("base64") + + await codeServerPage.focusTerminal() + + // The clipboard is only writable in a secure context, which the tests get + // by being served from localhost. printf turns \033 and \a into the + // escape and bell that delimit the sequence. + await codeServerPage.page.keyboard.type(`printf '\\033]52;c;${encoded}\\a'`) + await codeServerPage.page.keyboard.press("Enter") + + await expect + .poll(() => codeServerPage.page.evaluate(() => navigator.clipboard.readText()), { timeout: 20000 }) + .toBe(expected) + }) + // TODO@jsjoeio - add test to make sure full code-server path works test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => { const tmpFolderPath = await tmpdir(testName)