Skip to content

The clipboard is a chain of backends, and OSC 52 is one of them - #2

Merged
ken0nek merged 3 commits into
mainfrom
clip-backends
Sep 2, 2026
Merged

ken0nek merged 3 commits into
mainfrom
clip-backends

Conversation

@ken0nek

@ken0nek ken0nek commented Sep 2, 2026

Copy link
Copy Markdown
Owner

With no pbcopy to reach — a remote host, or inside tmux — recite writes the block to the terminal's own clipboard with an OSC 52 escape, so the text lands on the machine you are sitting at.

What lands

  • A backend chain in recite-clip. pbcopy first, then OSC 52. RECITE_BACKEND forces one and never silently falls back; --which reports the choice without writing. The payload is streamed, so it is never in a variable and never in argv.
  • recite names the backend. OSC 52 is write-only, so it reports sent rather than copied — a claim nothing can back does not get made. --version's clip row carries the backend, and separates a clip that refused ((no backend)) from one predating --which ((no --which)).
  • A refusal leaves the clipboard exactly as it was. A command group exits with its last command's status, so a base64 that existed and then failed put a well-formed OSC 52 with an empty payload on the wire — which the terminal reads as "set the clipboard to nothing". Cleared clipboard, sent, exit 0. The encode now stages into a file, where its status is its own.
  • README and CLAUDE.md cover the remote and tmux cases, the one tmux setting, and the four rules the backends would otherwise cost twice.

Testing

test/clip-tests.sh is new and hermetic — RECITE_TTY for the OSC 52 target, RECITE_BACKEND for selection, so the real clipboard is never touched. Its staging-file cases run under both /bin/sh and /bin/dash, because macOS bash 3.2 runs the EXIT trap even when it dies of an untrapped signal and would green-light deleted TERM/HUP traps.

Every case here was confirmed red against its own bug first. Two that were green against the code they named: the probe is unreachable from the write path (only a --which case can cover it), and a stub ignoring argv left the assertion named for it passing.

test/all.sh green.

pbcopy first, then the terminal's own clipboard over OSC 52. RECITE_BACKEND
forces one and never silently falls back. The payload is streamed, so it is
never in a variable and never in argv.

OSC 52 is write-only, so it reports `sent` rather than `copied` -- recite-clip
names the backend on one line of stdout and recite owns the wording.
--version's clip row carries it, and that --which call runs with stdin CLOSED:
an older sink is `exec pbcopy`, and pbcopy handed a readable /dev/null exits 0
and empties the clipboard.

Openability is an append-mode open on a spare descriptor, not [ -w ]:
access(2) says yes with no controlling tty and the open then fails. And
`true`, not `:`, with stderr silenced first -- a redirection error on a special
builtin kills a non-interactive shell outright.
A command group exits with its LAST command's status, so a base64 that existed
and then failed was invisible to `|| exit 4`: what went out was a well-formed
OSC 52 with an EMPTY payload, which the terminal reads as "set the clipboard to
nothing". Cleared clipboard, `sent`, exit 0.

The encode now stages into a file, where its status is its own, and the emit is
an && chain. The staging file is the user's captured output, so it takes the
same EXIT/INT/TERM/HUP traps recite gives its buffers. have_osc52 still checks
for an encoder: the probe is what --which answers from and what `auto` selects
on, and naming a backend that cannot encode is the dishonesty this tool exists
to avoid. --which also stops answering before the arity check.

The suite was green against every bug above, and each case here was confirmed
red first. Two traps it now pins: the probe is unreachable from the write path,
so only a --which case can cover it -- and a stub that ignores argv leaves the
assertion named for it green.

The prose that made claims the code cannot back goes with it, in the README and
in the headers.
Four situations printed it: nothing resolved, a path that is not there, a file
that is not executable, and a clip that RAN and answered nothing. The path on
the same line separates the first three. It does not separate the fourth, which
is itself two installs.

Status separates them. 4 is this clip refusing: no pbcopy here, and no terminal
to write an escape sequence to. Anything else is a clip predating --which,
which is `exec pbcopy` and aborts on the closed stdin. `(no backend)` and
`(no --which)`. Both tokens carry a space and overflow the column, so the cases
assert the whole row rather than a field.

Also drops shorthand the tree could not explain: the cross-shell gate is now
named for what it gates, and the signal note claims the security hole directly.
@ken0nek ken0nek self-assigned this Sep 2, 2026
@ken0nek
ken0nek merged commit b5481ff into main Sep 2, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 2, 2026
@ken0nek
ken0nek deleted the clip-backends branch September 2, 2026 04:55
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