Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions patches/osc52-clipboard.diff
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions patches/series
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ signature-verification.diff
copilot.diff
app-name.diff
csp-hashes.diff
osc52-clipboard.diff
23 changes: 23 additions & 0 deletions test/e2e/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down