Skip to content

OSC 52 clipboard read wired into IClipboardService#10

Merged
mstrobel merged 1 commit into
developfrom
worktree-osc52-clipboard-read
Jul 9, 2026
Merged

OSC 52 clipboard read wired into IClipboardService#10
mstrobel merged 1 commit into
developfrom
worktree-osc52-clipboard-read

Conversation

@mstrobel

@mstrobel mstrobel commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Adds system-clipboard read via the OSC 52 query/response round-trip and hooks it into the UI layer's IClipboardService (the write side shipped earlier).

Core

  • DeviceResponseKind.Clipboard + the interpreter's OSC 52 response arm (payload <targets>;<base64>, consumer decodes — the PaletteColor precedent).
  • ClipboardWriter.WriteQuery emits OSC 52 ; c ; ? ST.
  • Family-gated ClipboardRead capability. Not probed at startup — probing would flash the per-read permission prompt supporting terminals gate the read behind; the cap means "worth attempting", and a denied/unsupported read simply never replies.

UI

  • TerminalClipboardService.TryGetTextAsync: emits the query out-of-band, registers a one-shot device-response waiter, arms a frame-aligned UITimer for the timeout, subscribes BeginShutdown; the first of {reply, timeout, shutdown} completes and tears down all three (single-threaded on the UI loop). Decodes targets;base64 — empty selection → "", echoed ? → null.
  • TextBox Ctrl+V / Shift+Insert kick the read (2 s timeout for the human-scale permission prompt) and insert via the FromPaste path; bracketed paste stays the primary inbound path. An in-flight guard coalesces a held/double chord (OSC 52 has no request id, so one reply completes every pending read); the completion routes faults through the app exception funnel.

Adversarial-review fixes (mutation-verified)

  • SecurityEmitDeviceResponse drops every device response while _inPaste. The classifier is a pure framing layer with no paste awareness, so an ESC embedded in pasted content reaches the OSC dispatcher mid-paste — a hidden ESC]52;c;… in copied web content would otherwise forge a Clipboard reply and satisfy a pending read, bypassing the terminal's permission gate (clipboard poisoning). The guard also closes the pre-existing OSC 4/10/11/12 color-forgery variant.
  • Decode tolerates whitespace-wrapped / unpadded base64 (tmux, line-wrapping terminals) so a real reply isn't misread as "no clipboard"; a DEBUG breadcrumb separates decode-failure from no-reply.

Tests

Core interpreter + writer; ClipboardReadTests (round-trip, timeout, late reply, no-cap, empty, echoed query, UTF-8, malformed, whitespace/unpadded, multi-target, concurrent-reads fan-out, shutdown); TextBox C11_23a/b/c; the paste-forgery + outside-paste interpreter rows. Core 962 / UI 2948 / Bars 296 green.

🤖 Generated with Claude Code

…dService

The write side (OSC 52 set) shipped earlier; this adds the read side and hooks
it into the UI layer's clipboard interface.

Core:
- DeviceResponseKind.Clipboard + the interpreter's OSC 52 response arm (payload
  is "<targets>;<base64>", the consumer decodes — the PaletteColor precedent).
- ClipboardWriter.WriteQuery emits `OSC 52 ; c ; ? ST`.
- Family-gated ClipboardRead capability (Kitty/Ghostty/Rio/WezTerm/Foot/iTerm2/
  Alacritty/Xterm/Tmux). Deliberately NOT probed at startup — probing would flash
  the per-read permission prompt these terminals gate the read behind; the cap
  means "worth attempting", and a denied/unsupported read simply never replies.

UI:
- TerminalClipboardService.TryGetTextAsync: emits the query out-of-band, registers
  a one-shot device-response waiter, arms a frame-aligned UITimer for the timeout,
  and subscribes BeginShutdown; the first of {reply, timeout, shutdown} completes
  and tears down all three (single-threaded on the UI loop, so TrySetResult is the
  only race guard). Decodes targets;base64 — empty selection => "", echoed `?` =>
  null. CanRead now reflects the negotiated cap.
- TextBox Ctrl+V / Shift+Insert kick the read (2 s timeout for the human-scale
  permission prompt) and insert via the FromPaste path; bracketed paste stays the
  primary inbound path. An in-flight guard coalesces a held/double chord (OSC 52
  has no request id, so one reply completes every pending read) and the completion
  routes faults through the app exception funnel.

Adversarial review fixes (mutation-verified):
- SECURITY: EmitDeviceResponse drops every device response while _inPaste. The
  classifier is a pure framing layer with no paste awareness, so an ESC embedded
  in pasted content reaches the OSC dispatcher mid-paste — a hidden `ESC]52;c;..`
  in copied web content would otherwise forge a Clipboard reply and satisfy a
  pending read, bypassing the terminal's permission gate (clipboard poisoning).
  The guard also closes the pre-existing OSC 4/10/11/12 color-forgery variant.
- Decode tolerates whitespace-wrapped and unpadded base64 (tmux/line-wrapping
  terminals) so a real reply isn't misread as "no clipboard"; a DEBUG breadcrumb
  separates decode-failure from no-reply.

Tests: Core interpreter + writer, ClipboardReadTests (round-trip, timeout, late
reply, no-cap, empty, echoed query, UTF-8, malformed, whitespace/unpadded,
multi-target, concurrent-reads fan-out, shutdown), TextBox C11_23a/b/c, and the
paste-forgery + outside-paste interpreter rows. Core 962 / UI 2948 / Bars 296 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175wtwXrafD4fWAZ3i5THu6
@mstrobel mstrobel merged commit a2a263c into develop Jul 9, 2026
3 checks passed
@mstrobel mstrobel deleted the worktree-osc52-clipboard-read branch July 9, 2026 22:02
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.

1 participant