Skip to content

gatewaynode/ssh_clipboard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ssh_clipboard

Bidirectional clipboard synchronization over SSH, for developers who work inside VMs for security compartmentalization.


Motivation

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.exec and exits the moment you disconnect

How it works

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.


Installation

Local machine (macOS)

git clone <repo>
cd ssh_clipboard
cargo build --release
cp target/release/ssh_clipboard /usr/local/bin/

Remote VM — auto-deploy (recommended)

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.

Remote VM — manual static binary (Linux)

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/

Configuration

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          = false

CLI 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).


Usage

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

Key authentication

ssh_clipboard --host 192.168.64.5 --user dev --key ~/.ssh/id_ed25519

Password authentication

ssh_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

One-way sync

# 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

First connection to a new host

# 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-key

Remote clipboard backends

Detected 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 installed

Security notes

Host 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.


Building from source

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                    # format

Testing clipboard sync

An 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 both

Scenarios 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.


Roadmap

  • 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

License

MIT — see LICENSE.


Attribution

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.

About

A tool to sync a local clipboard with a remote machine over SSH

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors