Skip to content

Commit dde81ba

Browse files
ru-shclaude
andcommitted
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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 20a260f commit dde81ba

4 files changed

Lines changed: 73 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ Code v99.99.999
2222

2323
## Unreleased
2424

25+
### Fixed
26+
27+
- Copying from the terminal with OSC 52 (used by tmux, vim, Claude Code and
28+
similar) now writes to the system clipboard instead of being silently
29+
discarded. Requires a secure context (HTTPS or localhost).
30+
2531
## [4.137.0](https://github.com/coder/code-server/releases/tag/v4.137.0) - 2026-09-11
2632

2733
Code v1.137.0

‎patches/osc52-clipboard.diff‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Make terminal OSC 52 copy reach the system clipboard
2+
3+
Programs running in the integrated terminal (tmux, vim, Claude Code...) copy by
4+
emitting OSC 52. xterm's clipboard addon is always loaded and hands the payload
5+
to IClipboardService.writeText(text, 'clipboard') (see xtermTerminal.ts), but
6+
BrowserClipboardService files *any* typed write into an in-memory map and never
7+
touches navigator.clipboard, so on web the copy silently does nothing.
8+
9+
'clipboard' is not an alternate clipboard, it is the system one, so let it fall
10+
through. 'selection' (OSC 52 's'/'p', the X11 primary selection) keeps the
11+
in-memory behaviour since browsers have no equivalent.
12+
13+
Only the write path is changed. readText('clipboard') deliberately keeps
14+
returning the in-memory value, so an OSC 52 read query (ESC]52;c;? BEL) cannot
15+
be used by a remote program to exfiltrate the user's clipboard back into the
16+
terminal as input.
17+
18+
This needs a secure context (HTTPS or localhost); without one
19+
navigator.clipboard is undefined and the execCommand fallback cannot run,
20+
because OSC 52 arrives asynchronously with no user gesture attached.
21+
22+
To reproduce: open a terminal and run
23+
printf '\033]52;c;%s\a' "$(printf hello | base64)"
24+
then paste. Before this patch the clipboard is unchanged.
25+
26+
Fixes the web half of https://github.com/microsoft/vscode/issues/318254 -- drop
27+
this patch once that lands upstream.
28+
Index: code-server/lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts
29+
===================================================================
30+
--- code-server.orig/lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts
31+
+++ code-server/lib/vscode/src/vs/platform/clipboard/browser/clipboardService.ts
32+
@@ -124,8 +124,9 @@ export class BrowserClipboardService ext
33+
// Clear resources given we are writing text
34+
this.clearResourcesState();
35+
36+
- // With type: only in-memory is supported
37+
- if (type) {
38+
+ // With type: only in-memory is supported, except for 'clipboard' which
39+
+ // targets the system clipboard and is handled below.
40+
+ if (type && type !== 'clipboard') {
41+
this.mapTextToType.set(type, text);
42+
this.logService.trace('BrowserClipboardService#writeText');
43+
return;

‎patches/series‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ signature-verification.diff
2525
copilot.diff
2626
app-name.diff
2727
csp-hashes.diff
28+
osc52-clipboard.diff

‎test/e2e/terminal.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ describe("Integrated Terminal", ["--disable-workspace-trust"], {}, () => {
3030
expect(stdout).toMatch(address)
3131
})
3232

33+
// Programs in the terminal (tmux, vim, Claude Code...) copy by emitting OSC
34+
// 52. VS Code hands that to the clipboard service with a type, which the
35+
// browser implementation used to keep in memory instead of writing out.
36+
test("should copy to the clipboard via OSC 52", async ({ codeServerPage }) => {
37+
const origin = new URL(codeServerPage.page.url()).origin
38+
await codeServerPage.page.context().grantPermissions(["clipboard-read", "clipboard-write"], { origin })
39+
40+
const expected = `osc52-${Date.now()}`
41+
const encoded = Buffer.from(expected).toString("base64")
42+
43+
await codeServerPage.focusTerminal()
44+
45+
// The clipboard is only writable in a secure context, which the tests get
46+
// by being served from localhost. printf turns \033 and \a into the
47+
// escape and bell that delimit the sequence.
48+
await codeServerPage.page.keyboard.type(`printf '\\033]52;c;${encoded}\\a'`)
49+
await codeServerPage.page.keyboard.press("Enter")
50+
51+
await expect
52+
.poll(() => codeServerPage.page.evaluate(() => navigator.clipboard.readText()), { timeout: 20000 })
53+
.toBe(expected)
54+
})
55+
3356
// TODO@jsjoeio - add test to make sure full code-server path works
3457
test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => {
3558
const tmpFolderPath = await tmpdir(testName)

0 commit comments

Comments
 (0)