Skip to content

Serve clipboard images to exe.dev VMs over a reverse ssh forward - #61

Open
nonreagent wants to merge 4 commits into
mainfrom
clipboard-bridge
Open

nonreagent wants to merge 4 commits into
mainfrom
clipboard-bridge

Conversation

@nonreagent

Copy link
Copy Markdown
Collaborator

Problem

An image on the mac clipboard cannot be pasted into Claude Code running inside tmux on an exe.dev VM. Claude Code on Linux shells out to xclip/wl-paste, the VM has no display server, and tmux swallows the replies of every in-band terminal clipboard protocol. Separately, the tmux y binding pipes to pbcopy on every host, which fails silently on Linux.

Motivation

Agent sessions run remotely in tmux, and a screenshot is the fastest way to show Claude a UI problem. Today that means scp games. exe.dev's gateway was verified to relay reverse ssh forwards, which opens a clean out-of-band path. Design, probes, and the clipfan evaluation: docs/superpowers/specs/2026-09-07-clipboard-bridge-design.md in nonreagent/dotfiles#18 (on main there once that PR merges).

Proposed Solution

Four commits, each independently testable:

  • home/bin.Darwin/clipboard-bridge: a bash responder that answers types and png from the pasteboard via osascript (TIFF + sips fallback) and refuses everything else. No write path, no text path. Tested with stubbed osascript/sips/logger so the suite runs on Linux CI too.
  • home/Library/LaunchAgents/org.nonrational.clipboard-bridge.plist: socket-activated, inetd-style, on 127.0.0.1:2224; nothing runs while idle. Deployed via a Darwin manifest row; make clipboard-bridge bootstraps or reloads it.
  • home/.ssh/config.d/exe.conf: RemoteForward 127.0.0.1:2224 for *.exe.xyz, plus ControlMaster/ControlPersist so windows share one connection and the forward is requested once, plus keepalives. One-time: Include config.d/*.conf at the top of ~/.ssh/config.
  • home/.tmux.conf: y pipes to pbcopy only where pbcopy exists; elsewhere the plain copy binding stands and OSC 52 does the rest. test/test_tmux.sh loads the config in a scratch server under both PATHs.

The VM half (a wl-paste shim) lands in nonreagent/dotfiles#18.

Feedback

  • Trust tradeoff. While a session is connected, and for up to four hours after the last window closes (ControlPersist 4h keeps the master and its forward alive; ssh -O exit <vm>.exe.xyz ends it early, and a shorter ControlPersist shrinks the tail), any VM process can read the mac clipboard image on demand. Text is never served and the bridge has no write path.
  • ssh behaviour change. ControlMaster auto + ControlPersist 4h for every *.exe.xyz host is more than the forward: shared connections, a background master for four hours after the last window closes. Comfortable with that?
  • First-run checks on the mac. Two things could only be verified on macOS: Sequoia's pasteboard privacy prompt for osascript under launchd (Claude Code uses the same call; allow once if asked), and launchctl bootstrap on a symlinked plist. make clipboard-bridge prints a confirmation line when the agent loads; if bootstrap right after bootout ever returns Bootstrap failed: 5: Input/output error, run it again (or put a sleep 1 between the two), and launchctl print gui/$(id -u)/org.nonrational.clipboard-bridge is the richer status view. A failed pasteboard read is logged as clipboard info failed: ... under the clipboard-bridge tag, distinct from a text-only clipboard, which logs nothing.
  • Manual verification after make deploy && make clipboard-bridge: with an image on the clipboard, printf 'types\n' | nc 127.0.0.1 2224 prints image/png and printf 'png\n' | nc 127.0.0.1 2224 | file - reports PNG image data.

nonreagent and others added 4 commits September 7, 2026 20:05
launchd will run it per connection with the socket on stdin/stdout. It
answers `types` (is there an image?) and `png` (the bytes, via the same
osascript coercion Claude Code uses on macOS, with a TIFF and sips
fallback for Preview-style copies) and refuses everything else. No write
path, no text path. Stubbed osascript/sips/logger make the test run on
Linux too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Socket-activated and inetd-style: launchd owns 127.0.0.1:2224 and spawns
clipboard-bridge per connection, so nothing runs while idle. deploy
links the plist on Darwin; `make clipboard-bridge` bootstraps or reloads
it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A managed ssh include for *.exe.xyz: RemoteForward 127.0.0.1:2224 so the
VM's wl-paste shim reaches the launch agent, plus ControlMaster and
ControlPersist so every kitty window shares one connection and the
forward is requested once, and keepalives so a dead VM does not leave a
stale master behind. ~/.ssh/config stays unmanaged; it gains one
Include.

home/.ssh joins .config in test_deploy.sh's coverage exclusions: it
holds an individually deployed file and must never be a whole-directory
target.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On Linux VMs the pipe failed silently on every copy; the earlier
copy-selection-and-cancel binding is what we want there, and tmux's
set-clipboard already hands the selection to the terminal as OSC 52.
test/test_tmux.sh loads the config in a scratch server under both PATHs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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