Skip to content

A program in a pane can read the operator's clipboard — the wrapper confirms its own unauthorized request #337

Description

@Wirasm

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

  1. readClipboard (TerminalController+Callbacks.swift:80-111) reads NSPasteboard.general immediately and completes with confirmed: false.
  2. Surface.zig:5934if (self.config.clipboard_read == .ask and !confirmed) return error.UnauthorizedPaste;
  3. 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.
  4. embedded.zig:713-725 — on UnauthorizedPaste, calls confirm_read_clipboard_cb with the string already read.
  5. confirmReadClipboard (TerminalController+Callbacks.swift:113-135) unconditionally resends the same string with confirmed: true.
  6. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions