Paste images & receive notifications across SSH — remote Claude Code & Codex CLI feel local.
Install → setup → paste. Clipboard works over SSH.
Table of Contents
When running Claude Code or Codex CLI on a remote server via SSH, image paste often doesn't work and notifications don't reach you. The remote clipboard is empty — no screenshots, no diagrams. And when Claude finishes a task or needs approval, you have no idea unless you're staring at the terminal.
Image paste:
Claude Code (macOS): Mac clipboard → cc-clip daemon → SSH tunnel → xclip shim → Claude Code
Claude Code (Windows): Windows clipboard → cc-clip hotkey → SSH/SCP → remote file path → Claude Code
Codex CLI: Mac clipboard → cc-clip daemon → SSH tunnel → x11-bridge/Xvfb → Codex CLI
Notifications:
Claude Code hook → cc-clip-hook → SSH tunnel → local daemon → macOS/cmux notification
Codex notify → cc-clip notify → SSH tunnel → local daemon → macOS/cmux notification
One tool. No changes to Claude Code or Codex. Clipboard and notifications both work over SSH.
- Local machine: macOS 13+ or Windows 10/11
- Remote server: Linux (amd64 or arm64) accessible via SSH
- SSH config: You must have a Host entry in
~/.ssh/configfor your remote server
If you don't have an SSH config entry yet, add one:
# ~/.ssh/config
Host myserver
HostName 10.0.0.1 # your server's IP or domain
User your-username
IdentityFile ~/.ssh/id_rsa # optional, if using key auth
If you are on Windows and want the SSH/Claude Code workflow, use the dedicated guide:
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/ShunmeiCho/cc-clip/main/scripts/install.sh | shWindows:
Follow the dedicated guide:
On macOS / Linux, add ~/.local/bin to your PATH if prompted:
# Add to your shell profile (~/.zshrc or ~/.bashrc)
export PATH="$HOME/.local/bin:$PATH"
# Reload your shell
source ~/.zshrc # or: source ~/.bashrcVerify the installation:
cc-clip --versionmacOS "killed" error? If you see
zsh: killed cc-clip, macOS Gatekeeper is blocking the binary. Fix:xattr -d com.apple.quarantine ~/.local/bin/cc-clip
cc-clip setup myserverThis single command handles everything:
- Installs local dependencies (
pngpaste) - Configures SSH (
RemoteForward,ControlMaster no) - Starts the local daemon (via macOS launchd)
- Deploys the binary and shim to the remote server
Also use Codex CLI? Add --codex
cc-clip setup myserver --codexThis additionally installs Xvfb and the x11-bridge on the remote. If Xvfb is not found and auto-install fails, you'll see the exact command to run:
ssh myserver
sudo apt install xvfb # Debian/Ubuntu
sudo dnf install xorg-x11-server-Xvfb # RHEL/FedoraThen re-run cc-clip setup myserver --codex.
Open a new SSH session to your server (the tunnel activates on SSH connection):
ssh myserverThen use Claude Code or Codex CLI as normal — Ctrl+V now pastes images from your Mac clipboard.
Important: The image paste works through the SSH tunnel. You must connect via
ssh myserver(the host you set up). The tunnel is established on each SSH connection.
# Copy an image to your Mac clipboard first (Cmd+Shift+Ctrl+4), then:
cc-clip doctor --host myserverOn Windows, the equivalent quick check is:
| Approach | Works over SSH? | Any terminal? | Image support? | Setup complexity |
|---|---|---|---|---|
| Native Ctrl+V | Local only | Some | Yes | None |
| X11 Forwarding | Yes (slow) | N/A | Yes | Complex |
| OSC 52 clipboard | Partial | Some | Text only | None |
| cc-clip | Yes | Yes | Yes | One command |
graph LR
subgraph local ["Local Mac"]
A["Clipboard"] --> B["pngpaste"]
B --> C["cc-clip daemon<br/>127.0.0.1:18339"]
end
subgraph win ["Local Windows"]
J["Clipboard"] --> K["cc-clip hotkey / send"]
end
subgraph remote ["Remote Linux"]
F["Claude Code"] -- "Ctrl+V" --> E["xclip shim"]
E -- "curl" --> D["127.0.0.1:18339"]
K -- "ssh/scp upload" --> L["~/.cache/cc-clip/uploads"]
L -- "paste path" --> F
G["Codex CLI"] -- "Ctrl+V / arboard" --> H["Xvfb CLIPBOARD"]
H -- "SelectionRequest" --> I["x11-bridge"]
I -- "HTTP" --> D
end
C == "SSH RemoteForward" ==> D
style local fill:#1a1a2e,stroke:#e94560,color:#eee
style remote fill:#1a1a2e,stroke:#0f3460,color:#eee
style A fill:#e94560,stroke:#e94560,color:#fff
style F fill:#0f3460,stroke:#0f3460,color:#fff
style G fill:#0f3460,stroke:#0f3460,color:#fff
- macOS Claude path: the local daemon reads your Mac clipboard via
pngpaste, serves images over HTTP on loopback, and the remotexclipshim fetches images through the SSH tunnel - Windows Claude path: the local hotkey reads your Windows clipboard, uploads the image over SSH/SCP, and pastes the remote file path into the active terminal
- Codex CLI path: x11-bridge claims CLIPBOARD ownership on a headless Xvfb, serves images on-demand when Codex reads the clipboard via X11
- Notification path: remote Claude Code hooks and Codex notify pipe events through
cc-clip-hook→ SSH tunnel → local daemon → macOS Notification Center or cmux
When Claude Code or Codex CLI runs on a remote server, notifications don't work over SSH — TERM_PROGRAM isn't forwarded, hooks execute on the remote where terminal-notifier doesn't exist, and tmux swallows OSC sequences.
cc-clip solves this by acting as a notification transport bridge: remote hook events travel through the same SSH tunnel used for clipboard, and the local daemon delivers them to your macOS notification system (or cmux if installed).
| Event | Notification | Example |
|---|---|---|
| Claude finishes responding | "Claude stopped" + last message preview | Claude stopped: I've implemented the notification bridge... |
| Claude needs tool approval | "Tool approval needed" + tool name | Tool approval needed: Claude wants to Edit cmd/main.go |
| Codex task completes | "Codex" + completion message | Codex: Added error handling to fetch module |
| Image pasted via Ctrl+V | "cc-clip #N" + fingerprint + dimensions | cc-clip #3: a1b2c3d4 . 1920x1080 . PNG |
| Duplicate image detected | Same as above + duplicate marker | cc-clip #4: Duplicate of #2 |
Image paste notifications help you track what was pasted without leaving your workflow:
- Sequence number (#1, #2, #3...) lets you detect gaps (e.g., #1 → #3 means #2 was lost)
- Duplicate detection alerts when the same image is pasted twice within 5 images
- Click notification to open the full image in Preview.app (macOS, requires
terminal-notifier)
Step 1: Make sure cc-clip serve is running locally (or use cc-clip service install for auto-start).
Step 2: Configure your remote Claude Code hooks. The easiest way is to ask Claude Code itself to do it. SSH into your server, start Claude Code, and paste this prompt:
Please add cc-clip-hook to my Claude Code hooks configuration. Add it to both Stop and Notification hooks in ~/.claude/settings.json. The command is just "cc-clip-hook" (it's already in PATH at ~/.local/bin/). Keep any existing hooks (like ralph-wiggum) — just append cc-clip-hook alongside them. Show me the diff before and after.
Claude Code will read your current settings.json, add the hooks correctly, and show you the changes.
Alternatively, you can configure manually:
Manual hook configuration
Edit ~/.claude/settings.json on the remote server and add cc-clip-hook to the Stop and Notification hook arrays:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "cc-clip-hook" }
]
}
],
"Notification": [
{
"hooks": [
{ "type": "command", "command": "cc-clip-hook" }
]
}
]
}
}If you already have hooks (e.g., ralph-wiggum-stop.sh), add a new entry to the array — don't replace existing ones.
Restart Claude Code after editing (hooks are read at startup).
Step 3 (Codex only): Codex notification is auto-configured by cc-clip connect if ~/.codex/ exists on the remote. No manual steps needed.
Step 4: Generate and register a notification nonce (if you haven't used cc-clip connect):
# On local Mac — generate nonce and write to remote
NONCE=$(openssl rand -hex 32)
curl -s -X POST -H "Authorization: Bearer $(head -1 ~/.cache/cc-clip/session.token)" \
-H "User-Agent: cc-clip/0.1" -H "Content-Type: application/json" \
-d "{\"nonce\":\"$NONCE\"}" http://127.0.0.1:18339/register-nonce
ssh myserver "mkdir -p ~/.cache/cc-clip && echo '$NONCE' > ~/.cache/cc-clip/notify.nonce && chmod 600 ~/.cache/cc-clip/notify.nonce"Note:
cc-clip connecthandles steps 2-4 automatically. Manual setup is only needed if you use plainsshinstead ofcc-clip connect.
Notifications don't appear
Step-by-step verification (on the remote server):
# 1. Is the tunnel working?
curl -sf --connect-timeout 2 http://127.0.0.1:18339/health
# Expected: {"status":"ok"}
# 2. Is the hook script the correct version?
grep "curl" ~/.local/bin/cc-clip-hook
# Expected: a curl command with --connect-timeout
# 3. Is the nonce file present?
cat ~/.cache/cc-clip/notify.nonce
# Expected: a 64-character hex string
# 4. Manual test:
echo '{"hook_event_name":"Stop","stop_hook_reason":"stop_at_end_of_turn","last_assistant_message":"test"}' | cc-clip-hook
# Expected: local Mac shows notification popup
# 5. Check health log for failures:
cat ~/.cache/cc-clip/notify-health.log
# If exists: shows timestamps and HTTP error codesCommon issues:
| Problem | Fix |
|---|---|
| Tunnel down (step 1 fails) | Kill stale sshd: sudo kill $(sudo lsof -ti :18339), then reconnect SSH |
| Old hook script (step 2 empty) | Reinstall: cc-clip connect myserver or manually copy the script |
| Missing nonce (step 3 fails) | Register nonce (see Step 4 above) |
| Daemon running old binary | Rebuild (make build) and restart (cc-clip serve) |
| Layer | Protection |
|---|---|
| Network | Loopback only (127.0.0.1) — never exposed |
| Clipboard auth | Bearer token with 30-day sliding expiration (auto-renews on use) |
| Notification auth | Dedicated nonce per-connect session (separate from clipboard token) |
| Token delivery | Via stdin, never in command-line args |
| Notification trust | Hook notifications marked verified; generic JSON shows [unverified] prefix |
| Transparency | Non-image calls pass through to real xclip unchanged |
After initial setup, your daily workflow is:
# 1. SSH to your server (tunnel activates automatically)
ssh myserver
# 2. Use Claude Code or Codex CLI normally
claude # Claude Code
codex # Codex CLI
# 3. Ctrl+V pastes images from your Mac clipboardThe local daemon runs as a macOS launchd service and starts automatically on login. No need to re-run setup.
On Windows, some Windows Terminal -> SSH -> tmux -> Claude Code combinations do not trigger the remote xclip path when you press Alt+V or Ctrl+V. cc-clip therefore provides a Windows-native workflow that does not depend on remote clipboard interception.
For first-time setup and day-to-day usage, use:
The Windows workflow uses a dedicated remote-paste hotkey (default: Alt+Shift+V) so it does not collide with local Claude Code's native Alt+V.
| Command | Description |
|---|---|
cc-clip setup <host> |
Full setup: deps, SSH config, daemon, deploy |
cc-clip setup <host> --codex |
Full setup with Codex CLI support |
cc-clip connect <host> |
Deploy to remote (incremental) |
cc-clip connect <host> --token-only |
Sync token only (fast) |
cc-clip connect <host> --force |
Full redeploy ignoring cache |
cc-clip notify --title T --body B |
Send a notification through the tunnel |
cc-clip notify --from-codex-stdin |
Read Codex JSON from stdin and notify |
cc-clip doctor --host <host> |
End-to-end health check |
cc-clip status |
Show local component status |
cc-clip service install |
Install macOS launchd service |
cc-clip service uninstall |
Remove launchd service |
cc-clip send [<host>] --paste |
Windows: upload clipboard image and paste remote path |
cc-clip hotkey [<host>] |
Windows: register the remote upload/paste hotkey |
All commands
| Command | Description |
|---|---|
cc-clip setup <host> |
Full setup: deps, SSH config, daemon, deploy |
cc-clip setup <host> --codex |
Full setup including Codex CLI support |
cc-clip connect <host> |
Deploy to remote (incremental) |
cc-clip connect <host> --codex |
Deploy with Codex support (Xvfb + x11-bridge) |
cc-clip connect <host> --token-only |
Sync token only (fast) |
cc-clip connect <host> --force |
Full redeploy ignoring cache |
cc-clip serve |
Start daemon in foreground |
cc-clip serve --rotate-token |
Start daemon with forced new token |
cc-clip service install |
Install macOS launchd service |
cc-clip service uninstall |
Remove launchd service |
cc-clip service status |
Show service status |
cc-clip send [<host>] |
Upload clipboard image to a remote file |
cc-clip send [<host>] --paste |
Windows: paste the uploaded remote path into the active window |
cc-clip hotkey [<host>] |
Windows: run a background remote-paste hotkey listener |
cc-clip hotkey --enable-autostart |
Windows: start the hotkey listener automatically at login |
cc-clip hotkey --disable-autostart |
Windows: remove hotkey auto-start at login |
cc-clip hotkey --status |
Windows: show hotkey status |
cc-clip hotkey --stop |
Windows: stop the hotkey listener |
cc-clip notify --title T --body B |
Send a generic notification through the tunnel |
cc-clip notify --from-codex "$1" |
Parse Codex JSON arg and notify |
cc-clip notify --from-codex-stdin |
Read Codex JSON from stdin and notify |
cc-clip doctor |
Local health check |
cc-clip doctor --host <host> |
End-to-end health check |
cc-clip status |
Show component status |
cc-clip uninstall |
Remove xclip shim from remote |
cc-clip uninstall --codex |
Remove Codex support (local) |
cc-clip uninstall --codex --host <host> |
Remove Codex support from remote |
All settings have sensible defaults. Override via environment variables:
| Setting | Default | Env Var |
|---|---|---|
| Port | 18339 | CC_CLIP_PORT |
| Token TTL | 30d | CC_CLIP_TOKEN_TTL |
| Debug logs | off | CC_CLIP_DEBUG=1 |
All settings
| Setting | Default | Env Var |
|---|---|---|
| Port | 18339 | CC_CLIP_PORT |
| Token TTL | 30d | CC_CLIP_TOKEN_TTL |
| Output dir | $XDG_RUNTIME_DIR/claude-images |
CC_CLIP_OUT_DIR |
| Probe timeout | 500ms | CC_CLIP_PROBE_TIMEOUT_MS |
| Fetch timeout | 5000ms | CC_CLIP_FETCH_TIMEOUT_MS |
| Debug logs | off | CC_CLIP_DEBUG=1 |
| Local | Remote | Status |
|---|---|---|
| macOS (Apple Silicon) | Linux (amd64) | Stable |
| macOS (Intel) | Linux (arm64) | Stable |
| Windows 10/11 | Linux (amd64/arm64) | Experimental (send / hotkey) |
Local (macOS): macOS 13+ (pngpaste, auto-installed by cc-clip setup)
Local (Windows): Windows 10/11 with PowerShell, ssh, and scp available in PATH
Remote: Linux with xclip, curl, bash, and SSH access. The macOS tunnel/shim path is auto-configured by cc-clip connect; the Windows upload/hotkey path uses SSH/SCP directly.
Remote (Codex --codex): Additionally requires Xvfb. Auto-installed if passwordless sudo is available, otherwise: sudo apt install xvfb (Debian/Ubuntu) or sudo dnf install xorg-x11-server-Xvfb (RHEL/Fedora).
# One command to check everything
cc-clip doctor --host myserver"zsh: killed" after installation
Symptom: Running any cc-clip command immediately shows zsh: killed cc-clip ...
Cause: macOS Gatekeeper blocks unsigned binaries downloaded from the internet.
Fix:
xattr -d com.apple.quarantine ~/.local/bin/cc-clipOr reinstall (the latest install script handles this automatically):
curl -fsSL https://raw.githubusercontent.com/ShunmeiCho/cc-clip/main/scripts/install.sh | sh"cc-clip: command not found"
Cause: ~/.local/bin is not in your PATH.
Fix:
# Add to your shell profile
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrcReplace ~/.zshrc with ~/.bashrc if you use bash.
Ctrl+V doesn't paste images (Claude Code)
Step-by-step verification:
# 1. Local: Is the daemon running?
curl -s http://127.0.0.1:18339/health
# Expected: {"status":"ok"}
# 2. Remote: Is the tunnel forwarding?
ssh myserver "curl -s http://127.0.0.1:18339/health"
# Expected: {"status":"ok"}
# 3. Remote: Is the shim taking priority?
ssh myserver "which xclip"
# Expected: ~/.local/bin/xclip (NOT /usr/bin/xclip)
# 4. Remote: Does the shim intercept correctly?
# (copy an image to Mac clipboard first)
ssh myserver 'CC_CLIP_DEBUG=1 xclip -selection clipboard -t TARGETS -o'
# Expected: image/pngIf step 2 fails, you need to open a new SSH connection (the tunnel is established on connect).
If step 3 fails, the PATH fix didn't take effect. Log out and back in, or run: source ~/.bashrc
New SSH tab says "remote port forwarding failed for listen port 18339"
Symptom: A newly opened SSH tab warns remote port forwarding failed for listen port 18339, and image paste in that tab does nothing.
Cause: cc-clip uses a fixed remote port (18339) for the reverse tunnel. If another SSH session to the same host already owns that port, or a stale sshd child is still holding it, the new tab cannot establish its own tunnel.
Fix:
# Inspect the remote port without opening another forward:
ssh -o ClearAllForwardings=yes myserver "ss -tln | grep 18339 || true"- If another live SSH tab already owns the tunnel, use that tab/session, or close it before opening a new one.
- If the port is stuck after a disconnect, follow the stale
sshdcleanup steps below. - If you truly need multiple concurrent SSH sessions with image paste, give each host alias a different
cc-clipport instead of sharing18339.
Ctrl+V doesn't paste images (Codex CLI)
Most common cause: DISPLAY environment variable is empty. You must open a new SSH session after setup — existing sessions don't pick up the updated shell rc file.
Step-by-step verification (run these on the remote server):
# 1. Is DISPLAY set?
echo $DISPLAY
# Expected: 127.0.0.1:0 (or 127.0.0.1:1, etc.)
# If empty → open a NEW SSH session, or run: source ~/.bashrc
# 2. Is the SSH tunnel working?
curl -s http://127.0.0.1:18339/health
# Expected: {"status":"ok"}
# If fails → open a NEW SSH connection (tunnel activates on connect)
# 3. Is Xvfb running?
ps aux | grep Xvfb | grep -v grep
# Expected: a Xvfb process
# If missing → re-run: cc-clip connect myserver --codex --force
# 4. Is x11-bridge running?
ps aux | grep 'cc-clip x11-bridge' | grep -v grep
# Expected: a cc-clip x11-bridge process
# If missing → re-run: cc-clip connect myserver --codex --force
# 5. Does the X11 socket exist?
ls -la /tmp/.X11-unix/
# Expected: X0 file (matching your display number)
# 6. Can xclip read clipboard via X11? (copy an image on Mac first)
xclip -selection clipboard -t TARGETS -o
# Expected: image/pngCommon fixes:
| Step fails | Fix |
|---|---|
| Step 1 (DISPLAY empty) | Open a new SSH session. If still empty: source ~/.bashrc |
| Step 2 (tunnel down) | Open a new SSH connection — tunnel is per-connection |
| Steps 3-4 (processes missing) | cc-clip connect myserver --codex --force from local |
| Step 6 (no image/png) | Copy an image on Mac first: Cmd+Shift+Ctrl+4 |
Note: DISPLAY uses TCP loopback format (
127.0.0.1:N) instead of Unix socket format (:N) because Codex CLI's sandbox blocks access to/tmp/.X11-unix/. If you previously set up cc-clip with an older version, re-runcc-clip connect myserver --codex --forceto update.
SSH ControlMaster breaks RemoteForward
Symptom: Tunnel works during connect, but curl http://127.0.0.1:18339/health hangs in your SSH session.
Cause: An existing SSH ControlMaster connection was reused without RemoteForward.
Fix: cc-clip setup auto-configures this. If you set up SSH manually, add to ~/.ssh/config:
Host myserver
ControlMaster no
ControlPath none
Stale sshd process blocks port 18339
Symptom: Warning: remote port forwarding failed for listen port 18339
Fix: Kill the stale process on remote:
sudo ss -tlnp | grep 18339 # find the PID
sudo kill <PID> # kill itThen reconnect: ssh myserver
Token expired after 30+ days of inactivity
Fix: cc-clip connect myserver --token-only
Token uses sliding expiration — auto-renews on every use. Only expires after 30 days of zero activity.
Launchd daemon can't find pngpaste
Fix: cc-clip service uninstall && cc-clip service install (regenerates plist with correct PATH).
Setup fails: "killed" during re-deployment
Symptom: cc-clip setup was working before, but now shows zsh: killed when re-running.
Cause: The launchd service is running the old binary. Replacing the binary while the daemon holds it open can cause conflicts.
Fix:
cc-clip service uninstall
curl -fsSL https://raw.githubusercontent.com/ShunmeiCho/cc-clip/main/scripts/install.sh | sh
cc-clip setup myserverMore issues
See Troubleshooting Guide for detailed diagnostics, or run cc-clip doctor --host myserver.
Contributions welcome! For bug reports and feature requests, open an issue.
For code contributions:
git clone https://github.com/ShunmeiCho/cc-clip.git
cd cc-clip
make build && make test- Bug fixes: Open a PR directly with a clear description of the fix
- New features: Open an issue first to discuss the approach
- Commit style: Conventional Commits (
feat:,fix:,docs:, etc.)
Claude Code — Clipboard:
- anthropics/claude-code#5277 — Image paste in SSH sessions
- anthropics/claude-code#29204 — xclip/wl-paste dependency
Claude Code — Notifications:
- anthropics/claude-code#19976 — Terminal notifications fail in tmux/SSH
- anthropics/claude-code#29928 — Built-in completion notifications
- anthropics/claude-code#36885 — Notification when waiting for input (headless/SSH)
- anthropics/claude-code#29827 — Webhook/push notification for permission requests
- anthropics/claude-code#36850 — Terminal bell on tool approval prompt
- anthropics/claude-code#32610 — Terminal bell on completion
- anthropics/claude-code#40165 — OSC-99 notification support assumed, not queried
Codex CLI — Clipboard:
- openai/codex#6974 — Linux: cannot paste image
- openai/codex#6080 — Image pasting issue
- openai/codex#13716 — Clipboard image paste failure on Linux
- openai/codex#7599 — Image clipboard does not work in WSL
Codex CLI — Notifications:
- openai/codex#3962 — Play a sound when Codex finishes (34 comments)
- openai/codex#8929 — Notify hook not getting triggered
- openai/codex#8189 — WSL2: notifications fail for approval prompts
Terminal / Multiplexer:
- manaflow-ai/cmux#833 — Notifications over SSH+tmux sessions
- manaflow-ai/cmux#559 — Better SSH integration
- ghostty-org/ghostty#10517 — SSH image paste discussion


