Bidirectional clipboard synchronization over SSH, for developers who work inside VMs for security compartmentalization.
Modern JavaScript toolchains are a significant supply-chain attack surface. Infostealers targeting npm packages or bundler plugins can exfiltrate credentials, session tokens, and secrets from developer machines. Working inside a VM limits the blast radius — but moving clipboard content between host and VM via the usual methods (SSH agent forwarding, shared folders, browser extensions) reintroduces the attack surface.
ssh_clipboard provides a narrow, auditable channel:
- No credentials stored on the VM — the remote companion is ephemeral and writes nothing to disk
- No SSH agent forwarding — direct key-file or password auth only; the VM never sees your private key
- Password zeroed from memory immediately after the SSH handshake
- No persistent daemon on the VM — the companion is launched per-session
via
channel.execand exits the moment you disconnect
The same binary runs on both ends.
Local (macOS): polls pbpaste, connects to the VM over SSH, and runs
ssh_clipboard --remote on the other side via an SSH channel exec. Changes
are sent as length-prefixed, base64-encoded frames over the channel's stdio.
Remote (Linux VM or macOS VM): detects the available clipboard backend
(macOS pbcopy/pbpaste via launchctl asuser, X11 xclip, Wayland
wl-clipboard, or tmux), then sends and receives clipboard frames over
stdin/stdout.
The companion binary is deployed automatically on first run and kept up to date — if the local and remote versions differ, a redeploy happens before entering sync. On macOS remotes the binary is ad-hoc code-signed after deploy so the kernel permits execution.
The channel closes when you press Ctrl-C; the remote companion exits immediately. Nothing persists.
git clone <repo>
cd ssh_clipboard
cargo build --release
cp target/release/ssh_clipboard /usr/local/bin/ssh_clipboard deploys itself to ~/.local/bin/ssh_clipboard on the remote
automatically at startup. No manual install step is needed on the VM. The
binary is re-deployed whenever the local and remote versions differ.
A static musl binary has no libc or OpenSSL dependencies on the VM.
On the Linux VM (or any Linux machine):
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl --features vendored-openssl
# Verify it is fully static:
file target/x86_64-unknown-linux-musl/release/ssh_clipboard
# → ELF 64-bit LSB executable, x86-64, statically linked
scp target/x86_64-unknown-linux-musl/release/ssh_clipboard user@vm:~/.local/bin/On macOS (requires musl cross-compiler):
brew install filosottile/musl-cross/musl-cross
rustup target add x86_64-unknown-linux-musl
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
cargo build --release --target x86_64-unknown-linux-musl --features vendored-openssl
scp target/x86_64-unknown-linux-musl/release/ssh_clipboard user@vm:~/.local/bin/Persistent defaults can be stored in ~/.config/ssh_clipboard/config.toml
(or $XDG_CONFIG_HOME/ssh_clipboard/config.toml). A commented template is
created automatically on first run.
host = "192.168.64.5"
user = "dev"
key = "/Users/you/.ssh/id_ed25519"
port = 22
poll_ms = 500
direction = "both"
remote_bin = "~/.local/bin/ssh_clipboard"
trust_host_key = false
debug = falseCLI flags always take precedence over the config file. host and user are
the only fields that must be set (either via CLI or config file).
ssh_clipboard --host <HOST> --user <USER> [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--host <HOST> |
required | Remote SSH hostname or IP |
--port <PORT> |
22 |
SSH port |
--user <USER> |
required | SSH username |
--key <PATH> |
— | Path to SSH private key (omit for password auth) |
--poll-ms <MS> |
500 |
Clipboard poll interval in milliseconds |
--direction <DIR> |
both |
both | local-to-remote | remote-to-local |
--remote-bin <PATH> |
~/.local/bin/ssh_clipboard |
Path to companion binary on the VM |
--trust-host-key |
off | Accept unknown host key — first-time setup only |
--remote |
off | Run as remote companion (invoked automatically, not for manual use) |
--debug |
off | Enable debug logging to stderr and rolling log file |
ssh_clipboard --host 192.168.64.5 --user dev --key ~/.ssh/id_ed25519ssh_clipboard --host 192.168.64.5 --user dev
# Prompts: "Password for dev@192.168.64.5:"
# Read from /dev/tty — safe when stdin is a pipe, never echoed to screen# Push local clipboard into the VM only (never pull from VM)
ssh_clipboard --host 192.168.64.5 --user dev --key ~/.ssh/id_ed25519 \
--direction local-to-remote
# Pull VM clipboard to local only
ssh_clipboard --host 192.168.64.5 --user dev --key ~/.ssh/id_ed25519 \
--direction remote-to-local# Adds the host key to ~/.ssh/known_hosts with a loud warning.
# Verify the fingerprint manually before using in production.
ssh_clipboard --host 192.168.64.5 --user dev --key ~/.ssh/id_ed25519 \
--trust-host-keyDetected automatically on the remote in priority order:
| Platform | Environment variable | Tool required | Notes |
|---|---|---|---|
| macOS | (SSH session) | launchctl + pbcopy |
Preferred; works from SSH |
| macOS | (GUI session) | pbcopy |
Fallback for local GUI sessions |
| Linux | $DISPLAY |
xclip |
X11 — most desktop Linux |
| Linux | $WAYLAND_DISPLAY |
wl-paste + wl-copy |
Wayland (wl-clipboard package) |
| Linux | $TMUX |
tmux |
tmux built-in buffer |
| Any | (none) | — | Receive-only; local→VM still works |
Install the appropriate tool on your Linux VM:
# Debian/Ubuntu
sudo apt install xclip # X11
sudo apt install wl-clipboard # Wayland
# tmux is its own package if not already installedHost key verification is enforced against ~/.ssh/known_hosts. Unknown
or mismatched keys hard-fail with a clear error message. Use --trust-host-key
only on first connection to a host you control, and verify the fingerprint
out-of-band before trusting it.
Password handling: the password is read from /dev/tty (not stdin),
held in a ZeroOnDrop wrapper, passed to libssh2, then immediately
overwritten with zeros before the memory is freed.
Wire protocol: frames carry only clipboard content. There are no exec, eval, or shell-escape message types. Malformed frames are dropped silently — the process never panics on untrusted input.
Clipboard size: capped at 1 MB per sync event, enforced independently on both the local and remote sides.
No agent forwarding: the SSH session uses direct key-file or password auth only. The VM never gains access to your SSH agent.
OpenSSL: statically linked by default (vendored-openssl feature) so
there are no dynamic library dependencies on the remote host.
cargo build # debug binary (vendored OpenSSL, ~30s first build)
cargo build --release # optimised binary
cargo test # 66 unit tests
cargo clippy -- -D warnings # zero warnings enforced
cargo fmt # formatAn integration test script is provided that exercises three character
scenarios against a live running ssh_clipboard session:
# Reads host/user from ~/.config/ssh_clipboard/config.toml
./tests/clipboard_sync_test.sh
# Explicit target
./tests/clipboard_sync_test.sh dev@192.168.64.5
# Test both directions
./tests/clipboard_sync_test.sh dev@192.168.64.5 bothScenarios tested:
| # | Name | Content |
|---|---|---|
| 1 | ASCII | Alphanumeric + punctuation |
| 2 | Unicode | CJK (こんにちは), accented Latin (café), Cyrillic (Москва) |
| 3 | Emoji + mixed | Globe emoji, arrows, math symbols, Japanese + emoji |
Prerequisites: ssh_clipboard must already be running, and ssh key-based
auth must be configured for BatchMode=yes access to the remote.
- ARM64 Linux support (
aarch64-unknown-linux-musl) - macOS → macOS sync (two pbpaste/pbcopy ends, no remote backend needed)
- Configurable clipboard type (primary vs clipboard selection on X11)
- Windows local-side support
MIT — see LICENSE.
This project was developed collaboratively between the repository owner and Claude (Anthropic). The architecture, security requirements, and product direction were specified by the repository owner. Code implementation, debugging, and refinement were carried out by Claude across multiple sessions, including:
- Core protocol design and wire framing (
protocol.rs) - SSH session management and auto-deploy with macOS code-signing (
ssh_conn.rs) - Bidirectional sync loop with exponential-backoff reconnect (
sync.rs) - Remote companion event loop (
remote.rs) - macOS clipboard backend with UTF-8 encoding fix (
clipboard.rs,remote_clip.rs) - Config file support (
file_config.rs) - Diagnostic tooling: stderr capture, exit-status reporting, architecture detection
- Integration test script (
tests/clipboard_sync_test.sh)
Models used: Claude Opus 4.6, Claude Sonnet 4.6.