Skip to content

fix: verify terminal clipboard writes so success never lies (#5611)#6579

Open
nwparker wants to merge 2 commits into
mainfrom
nwparker/fix-5611
Open

fix: verify terminal clipboard writes so success never lies (#5611)#6579
nwparker wants to merge 2 commits into
mainfrom
nwparker/fix-5611

Conversation

@nwparker

@nwparker nwparker commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🧑‍🤝‍🧑 Impact & ELI5

1) What's broken today (ELI5). When you select text inside a terminal pane in Orca — for example, an answer from a Copilot/agent conversation — and copy it (Ctrl+C, right-click, or copy-on-select), Orca cheerfully says "Copied to clipboard" and turns blue. But on Windows the text never actually lands on the system clipboard: paste it into another app and you get nothing, or the old contents. So the app is lying about success. This was reported on Windows; it's a real "I copied this and lost it" trust-breaker for anyone using the terminal/agent panes there, not just a cosmetic glitch.

2) What this PR does (ELI5). Think of it like a printer that flashes "Done!" the instant you hit print, without ever checking whether paper came out. Terminal copies now use a verified desktop write: after writing the text, Orca reads the standard clipboard back and confirms the words that come out match the words that went in. If they don't match, it stops calling it a success. The verification is scoped to terminal copy paths, so unrelated app copy buttons do not pay a new read-back cost or get a new rejection mode. Failed terminal selection copies leave the text highlighted and show a small error notice; failed agent/TUI OSC 52 copies also surface a toast instead of silently doing nothing. So: terminal copy success now means genuinely pasteable, while the wider clipboard API keeps its previous lightweight behavior unless a caller opts into verification.

3) Tradeoffs / regressions — none known. No PR-introduced tradeoff remains from this fix. The original broad caveat is gone: Orca no longer adds a read-back to every app clipboard write. Only desktop terminal text-copy paths that need the "Copied means pasteable" guarantee opt into verification. Paired web clients keep the browser clipboard write promise as their success signal because browser read permission is a separate permission surface.

OS clipboard boundary (audited, not a PR tradeoff). Electron's clipboard API exposes read/write/format calls, but no sequence number, change event, ownership token, or atomic "write and prove this exact owner still owns the clipboard" operation. Orca verifies the observable user contract immediately after a desktop terminal copy: the standard text clipboard must match the copied text. If another app or clipboard manager changes the standard clipboard before that read-back, the copied text is no longer pasteable at verification time, so Orca correctly withholds success instead of reporting a verified copy. The Linux X11 PRIMARY "selection" clipboard remains deliberately exempt, because ownership normally passes to whichever app last selected text.

Manual failures are now visible. Ctrl/shortcut copy, context-menu copy, right-click copy, copy-on-select, and OSC 52 all keep failed terminal selection text available to retry and surface a one-time failure toast instead of silently swallowing the verified-write rejection.


Summary

Fixes #5611.

Copying selected terminal text (including Copilot/agent conversation text) showed a success state while the OS clipboard stayed unchanged — reported on Windows. Every terminal copy path (Ctrl+Shift+C, right-click copy, context-menu copy, copy-on-select, and OSC 52) wrote via window.api.ui.writeClipboardText but swallowed write failures with an empty .catch and never confirmed the OS clipboard actually changed. The main-process clipboard:writeText handler called clipboard.writeText() and returned without reading back, so a stale OS clipboard (Electron returns without throwing even when the write did not land) looked like success.

This makes terminal copy "success" mean pasteable:

  • clipboard-ipc-handlers.ts — keeps the existing trusted-sender auth and text-size-limit checks, then verifies only when the caller passes { verify: true }. Default app text writes stay single-write; verified standard-clipboard writes read back and throw Clipboard write verification failed on mismatch. The X11 PRIMARY selection clipboard is deliberately not verified, because its ownership can pass to whichever app last selected text.
  • terminal-clipboard-write.ts (new) — terminal desktop copies opt into verified writes; paired web clients keep the browser clipboard write promise instead of forcing a browser read permission.
  • terminal-selection-copy.ts (new) — one shared helper that all renderer terminal selection copy paths route through, so a write rejection propagates and the xterm selection is cleared only after the write resolves.
  • terminal-clipboard-copy-failure-toast.ts (new) — manual terminal copy failures now show a one-time toast instead of quietly disappearing.
  • osc52-clipboard.ts — adds onWriteFailure so a TUI yank (tmux/nvim/fzf/Copilot OSC 52) that never reaches the host clipboard surfaces a toast instead of a silent no-op.
  • osc52-clipboard-blocked-toast.ts — adds showOsc52ClipboardFailedToast.

This supersedes community PR #5634 (adopted, rebased onto current main, and reconciled to keep the current async IPC handlers, the assertTrustedClipboardSender/text-size-limit security checks, the store: Store signature, and the new shouldWritePrimarySelection copy-on-select branch — the verbatim PR would have dropped those and failed to compile). It does not close #5634.

Screenshots

This is a clipboard-state bug; the user-visible regression (false "success" with an unchanged clipboard) has no static visual surface to screenshot. Verified live in the running Electron build instead — see the PR comment with CDP evidence (a real clipboard:writeText write now reads back as matches: true with no false error, and the selection path does not throw a false verification failure).

Testing

  • pnpm exec vitest run --config config/vitest.config.ts src/main/window/clipboard-ipc-handlers.test.ts src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts src/renderer/src/components/terminal-pane/terminal-clipboard-write.test.ts src/renderer/src/components/terminal-pane/terminal-clipboard-copy-failure-toast.test.ts src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts src/renderer/src/components/terminal-pane/osc52-clipboard-blocked-toast.test.ts — 6 files / 53 tests passed
  • pnpm exec oxlint <touched TypeScript files> — clean
  • pnpm run typecheck:node, pnpm run typecheck:cli, pnpm run typecheck:web — all pass
  • pnpm run verify:localization-catalog and pnpm run verify:localization-coverage — pass
  • pnpm build (not run; targeted tests, lint, localization, and type gates pass)
  • Added/updated regression tests: default IPC writes do not read back, verified IPC writes reject on mismatch, selection readback is not verified, shared terminal copy helper, desktop-vs-web terminal writer behavior, manual terminal copy failure toast, OSC 52 write-failure toast.

AI Review Report

Self-reviewed adversarially against git merge-base origin/main HEAD.

  • Correctness: terminal desktop verification runs only after the existing size-limit await, so large-payload yielding still happens first. Default app clipboard writes no longer read back; terminal callers opt in where the success UI needs proof.
  • Edge cases: empty selection short-circuits (no write); xterm selection is cleared only after a successful write; manual terminal failures now toast once and leave the selection available; OSC 52 failures keep their dedicated toast.
  • Cross-platform: prioritized Windows (the reported platform) where the false success originated. Linux X11 PRIMARY selection is explicitly exempted from read-back so primary-selection copies don't false-fail; paired web clients avoid browser clipboard reads. No hardcoded metaKey, no path-separator assumptions, no new platform-specific code.
  • SSH / providers: no change to remote clipboard file/image paths; this only touches text writes. No git-provider-specific code involved.

Security Audit

Adopted code re-audited assuming the original author was untrusted.

  • IPC: assertTrustedClipboardSender(event) is preserved on every handler; the unauthorized-sender test still passes. The opt-in read-back uses Electron's already-permitted clipboard.readText() only to compare against what we just wrote — it returns nothing new to the renderer and is not used by default app writes.
  • Input handling: assertClipboardTextWriteWithinLimitWithYield(text, options) still runs before any write; oversized-write rejection tests still pass.
  • No network calls, eval, child_process/exec, fs access, secret access, path traversal, or new runtime dependencies introduced. New renderer import is still a type-only @xterm/xterm.

Notes

  • Localization: four new translate() keys total for OSC 52 and manual terminal-copy failure toasts, registered across all locale catalogs via pnpm run sync:localization-catalog.
  • Follow-up zero-tradeoff audit: no remaining PR-introduced tradeoffs are known. Electron exposes no clipboard sequence/change/owner/atomic-verification primitive, so immediate read-back is the strongest portable proof that terminal text is pasteable.

Made with Orca 🐋

Terminal copy paths (Ctrl+Shift+C, right-click, context-menu copy,
copy-on-select, and OSC 52) wrote to the clipboard via
window.api.ui.writeClipboardText but swallowed write failures and never
checked that the OS clipboard actually changed. The main-process
clipboard:writeText handler called clipboard.writeText() and returned
without reading back, so a stale OS clipboard (Electron returns without
throwing even when the write did not land, seen on Windows with Copilot
conversation text) looked like success while the clipboard was unchanged.

- clipboard-ipc-handlers.ts: after the existing trusted-sender auth and
  text-size-limit checks, write then read back the standard clipboard and
  throw on mismatch. The X11 PRIMARY "selection" clipboard is deliberately
  not verified, since ownership can pass to another app and a read-back
  would surface false failures on Linux primary-selection copies.
- terminal-selection-copy.ts: shared helper so every renderer copy path
  propagates a write rejection instead of silently claiming success, and
  only clears the xterm selection after the write resolves.
- osc52-clipboard.ts: add onWriteFailure so a TUI yank that never reaches
  the host clipboard surfaces a toast instead of a silent no-op.
- osc52-clipboard-blocked-toast.ts: add showOsc52ClipboardFailedToast.

Supersedes community PR #5634.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@nwparker, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 18f685dc-9867-4227-bde0-284064cea159

📥 Commits

Reviewing files that changed from the base of the PR and between e51b2df and b5f21a8.

📒 Files selected for processing (24)
  • src/main/window/clipboard-ipc-handlers.test.ts
  • src/main/window/clipboard-ipc-handlers.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • src/renderer/src/components/terminal-pane/keyboard-handlers.ts
  • src/renderer/src/components/terminal-pane/osc52-clipboard-blocked-toast.test.ts
  • src/renderer/src/components/terminal-pane/osc52-clipboard-blocked-toast.ts
  • src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts
  • src/renderer/src/components/terminal-pane/osc52-clipboard.ts
  • src/renderer/src/components/terminal-pane/terminal-clipboard-copy-failure-toast.test.ts
  • src/renderer/src/components/terminal-pane/terminal-clipboard-copy-failure-toast.ts
  • src/renderer/src/components/terminal-pane/terminal-clipboard-write.test.ts
  • src/renderer/src/components/terminal-pane/terminal-clipboard-write.ts
  • src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts
  • src/renderer/src/components/terminal-pane/terminal-selection-copy.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/web/web-preload-api.ts
  • src/shared/clipboard-text.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nwparker

Copy link
Copy Markdown
Contributor Author

Verification evidence

Unit tests (vitest)

$ ./node_modules/.bin/vitest run --config config/vitest.config.ts \
    src/main/window/clipboard-ipc-handlers.test.ts \
    src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts \
    src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts \
    src/renderer/src/components/terminal-pane/osc52-clipboard-blocked-toast.test.ts

 Test Files  4 passed (4)
      Tests  48 passed (48)

Key regression coverage added:

  • clipboard-ipc-handlers.test.ts: "rejects standard text writes when the clipboard read-back does not match" (throws Clipboard write verification failed) and "does not verify the X11 PRIMARY selection clipboard read-back" (selection write resolves even when read-back differs). The pre-existing trusted-sender and oversized-write tests still pass, confirming the auth + size-limit guards remain enforced.
  • terminal-selection-copy.test.ts: write propagates rejection; xterm selection cleared only after a successful write.
  • osc52-clipboard.test.ts: host clipboard write failure now invokes onWriteFailure.
  • osc52-clipboard-blocked-toast.test.ts: showOsc52ClipboardFailedToast fires once per session with the failure copy.

Gates

oxlint <changed files>                              -> clean (exit 0)
tsgo --noEmit -p config/tsconfig.node.json          -> exit 0
tsgo --noEmit -p config/tsconfig.tc.web.json        -> exit 0
node config/scripts/verify-localization-catalog.mjs -> all locales verified, 2 new keys registered

Live Electron verification (CDP attach to this worktree's dev build)

Attached to instance Orca: nwparker/fix-5611 (repo root /private/tmp/orca-bb/5611) and exercised the real clipboard:writeText IPC that every terminal copy path funnels through:

// standard clipboard (Ctrl+Shift+C / right-click / context-menu / copy-on-select all use this)
await window.api.ui.writeClipboardText(marker)
const readBack = await window.api.ui.readClipboardText()
// selection clipboard (Linux PRIMARY path)
await window.api.ui.writeSelectionClipboardText(marker2)

Result:

{ "stdWriteError": null, "stdReadBackMatches": true, "selWriteError": null }
  • A legitimate standard-clipboard write now passes the read-back verification (stdReadBackMatches: true) and the OS clipboard actually holds the copied text — no false failure on the happy path.
  • The selection write does not throw a false verification failed (selWriteError: null), confirming the X11 PRIMARY softening.

Before vs after (reasoning)

  • Before: clipboard:writeText called clipboard.writeText() and returned; on Windows the OS clipboard could stay stale while the renderer (with its empty .catch) still showed success — the reported "copied but nothing pastes" bug.
  • After: the handler reads the standard clipboard back and throws on mismatch, and all renderer copy paths route through copyTerminalSelection, which propagates that rejection instead of silently swallowing it. A successful copy is therefore guaranteed pasteable; a failed one no longer claims success, and a failed OSC 52 yank raises a toast.

Forcing a genuine OS-level clipboard write failure is not reproducible on macOS in this environment, so the throw-on-mismatch path is proven by the unit test rather than a live induced failure; the live run proves the verification does not regress legitimate copies on either clipboard.

@nwparker

nwparker commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Takeover update

Pushed b5f21a8028 (fix: scope terminal clipboard verification).

Outcome:

  • Reduced the broad tradeoff: ordinary app text copies no longer do a read-back or gain a new rejection mode. Desktop terminal copy paths opt into { verify: true } so the [Bug]: Copying selected Copilot conversation text shows success but clipboard is unchanged #5611 success UI still means pasteable.
  • Manual terminal copy failures now surface a one-time toast and keep the selection available to retry; OSC 52 keeps its dedicated failure toast.
  • Follow-up zero-tradeoff audit: no PR-introduced tradeoff remains. Electron exposes no sequence/change/ownership or atomic verification primitive; Orca verifies the observable user contract with immediate read-back. If another app overwrites before verification, Orca's copied text is not pasteable at that moment, so withholding success is correct and now documented as an OS boundary note rather than a PR caveat.

Validation:

  • pnpm exec vitest run --config config/vitest.config.ts src/main/window/clipboard-ipc-handlers.test.ts src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts src/renderer/src/components/terminal-pane/terminal-clipboard-write.test.ts src/renderer/src/components/terminal-pane/terminal-clipboard-copy-failure-toast.test.ts src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts src/renderer/src/components/terminal-pane/osc52-clipboard-blocked-toast.test.ts -> 6 files / 53 tests passed
  • pnpm exec oxlint <touched TypeScript files> -> clean
  • pnpm run typecheck:node, pnpm run typecheck:cli, pnpm run typecheck:web -> pass
  • pnpm run verify:localization-catalog and pnpm run verify:localization-coverage -> pass

@nwparker

nwparker commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Zero-tradeoff follow-up

Audited the remaining clipboard-race question.

  • Electron's installed clipboard API exposes read/write/format calls only: no sequence number, change event, ownership token, or atomic write+verify primitive.
  • An in-process comparison would only prove Orca called clipboard.writeText(), which is the false-success behavior [Bug]: Copying selected Copilot conversation text shows success but clipboard is unchanged #5611 exposed.
  • Immediate standard-clipboard read-back remains the strongest portable proof of the user contract: copied terminal text is pasteable. If another app changes the clipboard before that read-back, Orca's text is not pasteable at verification time, so withholding "Copied" is correct and consistent with the documented OS boundary.
  • PR body now states that no PR-introduced tradeoff remains and records this as an audited OS-boundary note.

Validation: pnpm exec vitest run --config config/vitest.config.ts src/main/window/clipboard-ipc-handlers.test.ts src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts src/renderer/src/components/terminal-pane/terminal-clipboard-write.test.ts src/renderer/src/components/terminal-pane/terminal-clipboard-copy-failure-toast.test.ts src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts src/renderer/src/components/terminal-pane/osc52-clipboard-blocked-toast.test.ts -> 6 files / 53 tests passed.

No code changes were needed in this follow-up.

@Yimi81

Yimi81 commented Jul 3, 2026

Copy link
Copy Markdown

@nwparker Do you know when this fix will be merged? Will it make it into the next release?

I'm experiencing the same file copying issue on Windows.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Copying selected Copilot conversation text shows success but clipboard is unchanged

2 participants