A program running in a pane can read the operator's system clipboard with OSC 52 ;c;?, because the vendored wrapper confirms its own unauthorized request.
ghostty's clipboard-read default is ask — it is supposed to ask the host before handing a clipboard back to a program. helm's wrapper answers confirmed: true with no prompt shown anywhere.
Found while fixing the write side (#297 → PR #333), verified twice: by the agent that found it, and independently by a seam review that traced it end to end by reading only, without executing it.
The path, with citations
readClipboard (TerminalController+Callbacks.swift:80-111) reads NSPasteboard.general immediately and completes with confirmed: false.
Surface.zig:5934 — if (self.config.clipboard_read == .ask and !confirmed) return error.UnauthorizedPaste;
config/Config.zig:2391 — @"clipboard-read": ClipboardAccess = .ask is ghostty's real default, and helm sets no override: grepped for clipboard-read/clipboard_read across Sources/Helm, no hits. So helm runs under ask.
embedded.zig:713-725 — on UnauthorizedPaste, calls confirm_read_clipboard_cb with the string already read.
confirmReadClipboard (TerminalController+Callbacks.swift:113-135) unconditionally resends the same string with confirmed: true.
Surface.zig:5934 now passes, and :5938-5965 base64-encodes the data into an \x1b]52;c;…\x1b\\ reply queued straight to the pane's pty — read by whatever program asked.
So the guard exists, fires correctly, and is answered by a callback that confirms its own denial.
Why this is worse than what #297 was filed about
#297 was destruction — a program overwriting the clipboard, losing whatever was in it. Annoying, immediately visible, and now fixed.
This is disclosure, and it is silent. Whatever the operator last copied — a token, a password, a private URL, a chunk of someone else's message — is readable by any program in any pane, with no prompt, no log line and no trace. He would never know it happened.
It also predates #297's fix and survives it: before and after PR #333, unchanged. That PR was right not to fold it in — the write side is a patch, and this needs a prompt helm owns, which is a feature.
What it needs
A confirmation surface helm owns. ghostty is asking the right question; there is nobody to answer it. So:
- Something the operator actually sees, in the pane that asked, before the bytes go back.
- A decision about the default when nobody is at the pane — and note this is the inverse of
SpoolUnattendedPolicy's problem. There, a question nobody would answer had to be answered in advance and permissively so the agent could work. Here the safe default is to refuse, because a read that never happens costs a program a paste and a read that happens silently costs the operator a secret. Say which, and why, in the type rather than in a comment.
- Whether it is remembered per pane, per program, per session, or never.
Do not fix it by defeating the guard from the other side — setting clipboard-read = allow would make the disclosure deliberate rather than accidental, which is worse.
Acceptance
Related: #297 and PR #333 (the write side, fixed), #310 (where hostile-output policy is said to land in the painter).
A program running in a pane can read the operator's system clipboard with
OSC 52 ;c;?, because the vendored wrapper confirms its own unauthorized request.ghostty's
clipboard-readdefault isask— it is supposed to ask the host before handing a clipboard back to a program. helm's wrapper answersconfirmed: truewith no prompt shown anywhere.Found while fixing the write side (#297 → PR #333), verified twice: by the agent that found it, and independently by a seam review that traced it end to end by reading only, without executing it.
The path, with citations
readClipboard(TerminalController+Callbacks.swift:80-111) readsNSPasteboard.generalimmediately and completes withconfirmed: false.Surface.zig:5934—if (self.config.clipboard_read == .ask and !confirmed) return error.UnauthorizedPaste;config/Config.zig:2391—@"clipboard-read": ClipboardAccess = .askis ghostty's real default, and helm sets no override: grepped forclipboard-read/clipboard_readacrossSources/Helm, no hits. So helm runs underask.embedded.zig:713-725— onUnauthorizedPaste, callsconfirm_read_clipboard_cbwith the string already read.confirmReadClipboard(TerminalController+Callbacks.swift:113-135) unconditionally resends the same string withconfirmed: true.Surface.zig:5934now passes, and:5938-5965base64-encodes the data into an\x1b]52;c;…\x1b\\reply queued straight to the pane's pty — read by whatever program asked.So the guard exists, fires correctly, and is answered by a callback that confirms its own denial.
Why this is worse than what #297 was filed about
#297 was destruction — a program overwriting the clipboard, losing whatever was in it. Annoying, immediately visible, and now fixed.
This is disclosure, and it is silent. Whatever the operator last copied — a token, a password, a private URL, a chunk of someone else's message — is readable by any program in any pane, with no prompt, no log line and no trace. He would never know it happened.
It also predates #297's fix and survives it: before and after PR #333, unchanged. That PR was right not to fold it in — the write side is a patch, and this needs a prompt helm owns, which is a feature.
What it needs
A confirmation surface helm owns. ghostty is asking the right question; there is nobody to answer it. So:
SpoolUnattendedPolicy's problem. There, a question nobody would answer had to be answered in advance and permissively so the agent could work. Here the safe default is to refuse, because a read that never happens costs a program a paste and a read that happens silently costs the operator a secret. Say which, and why, in the type rather than in a comment.Do not fix it by defeating the guard from the other side — setting
clipboard-read = allowwould make the disclosure deliberate rather than accidental, which is worse.Acceptance
OSC 52 ;c;?from a program in a pane does not return the operator's clipboard without something he saw and answered.Related: #297 and PR #333 (the write side, fixed), #310 (where hostile-output policy is said to land in the painter).