Skip to content

Commit 0c51ce6

Browse files
committed
Initial v1.0 release
Paste clipboard images into Claude Code / Codex over SSH, with or without tmux, via iTerm2's Python API. Auto-detects tmux -CC, plain ssh, and local sessions and delivers a real Ctrl+V so the agent reads the remote clipboard natively. Includes the AutoLaunch script, installer, optional config, detection tests, and CI.
0 parents  commit 0c51ce6

8 files changed

Lines changed: 676 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
python: ["3.10", "3.12"]
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: ${{ matrix.python }}
18+
- name: Syntax check
19+
run: python -m py_compile remote_paste.py
20+
- name: Run detection tests
21+
run: |
22+
pip install pytest
23+
pytest -q

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Local config (may contain host names)
2+
config.json
3+
4+
# Python
5+
__pycache__/
6+
*.pyc
7+
8+
# macOS
9+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Jared Atchison
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# remote-paste-iterm2
2+
3+
[![CI](https://github.com/jaredatch/remote-paste-iterm2/actions/workflows/ci.yml/badge.svg)](https://github.com/jaredatch/remote-paste-iterm2/actions/workflows/ci.yml) ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg) ![Requires: iTerm2](https://img.shields.io/badge/requires-iTerm2-black.svg)
4+
5+
Paste a screenshot into Claude Code or Codex running on a remote Mac. Just hit Ctrl+V, the same as you would locally.
6+
7+
Ask any AI assistant how to do this and it'll tell you it's impossible: *"the clipboard can't cross SSH."* It's wrong. Here's the workaround that actually works, tmux and all.
8+
9+
## Quickstart
10+
11+
```sh
12+
git clone https://github.com/jaredatch/remote-paste-iterm2.git
13+
cd remote-paste-iterm2
14+
./install.sh
15+
```
16+
17+
Then two one-time steps in iTerm2: enable the Python API and bind Ctrl+V. Details in [Install](#install).
18+
19+
## The problem
20+
21+
Terminal agents read images from the system clipboard when you paste. Locally that's fine. Over SSH the agent reads the remote clipboard, which is empty, and the image never crosses the wire because a terminal stream carries text, not pixels. So you fall back to scp or typing out a file path, every single time. Claude Code even ships a consolation message for it: *"No image found in clipboard. You're SSH'd; try scp?"*
22+
23+
## How it works
24+
25+
Don't push the image through the terminal. Put it where the agent is already looking, the remote clipboard.
26+
27+
1. You press Ctrl+V. iTerm2 hands the keypress to a small Python script on your Mac.
28+
2. The script grabs the image off your local clipboard and copies it onto the **remote** machine's clipboard over your existing SSH connection.
29+
3. It delivers a genuine Ctrl+V keypress to the remote agent.
30+
4. The agent reads the freshly populated remote clipboard and attaches the image, exactly like a local paste, without touching a file path or scp.
31+
32+
Step 3 is where everyone gets stuck. Under tmux control mode (`-CC`), an injected Ctrl+V shows up as the literal text `0x16` instead of a keystroke, because control mode wraps it in a bracketed paste. The fix: let tmux deliver the key itself with `tmux send-keys`, which counts as a real keypress. remote-paste detects your session type and picks the right delivery on its own.
33+
34+
Your clipboard image only ever travels to the host you're already connected to. Nothing else leaves your machine, and there's no telemetry.
35+
36+
## Requirements
37+
38+
**On your Mac (local):**
39+
40+
- iTerm2 3.3 or newer, with the Python API enabled (Settings → General → Magic → Enable Python API). The scripting API landed in 3.3.
41+
- No Python to install. iTerm2 bundles its own runtime for scripts.
42+
- `pngpaste` is optional but recommended (`brew install pngpaste`). Without it, the clipboard is read with `osascript`.
43+
44+
**On the remote:**
45+
46+
- macOS. The clipboard is loaded with `osascript`, so the box you SSH into has to be a Mac. Linux is on the roadmap.
47+
- A logged-in GUI session under the same user you SSH as, so the clipboard is reachable. A Mac sitting at a logged-in desktop works; a headless box with no console session does not.
48+
- tmux only if you use it. The script handles tmux `-CC`, plain SSH, and local sessions on its own.
49+
50+
If you run terminal agents on a remote Mac over SSH, this kills a daily papercut. If you don't, you don't need it. That's the whole audience, and it's enough.
51+
52+
## Install
53+
54+
```sh
55+
git clone https://github.com/jaredatch/remote-paste-iterm2.git
56+
cd remote-paste-iterm2
57+
./install.sh
58+
```
59+
60+
The installer copies the script into iTerm2's AutoLaunch folder and checks your dependencies. Two one-time manual steps remain:
61+
62+
**1. Enable the Python API**
63+
iTerm2 → Settings → General → Magic → check *Enable Python API*.
64+
65+
**2. Bind Ctrl+V**
66+
iTerm2 → Settings → Keys → Key Bindings → `+`, then:
67+
68+
| Field | Value |
69+
| --- | --- |
70+
| Keyboard Shortcut | `⌃V` |
71+
| Action | Invoke Script Function |
72+
| Function call | `remote_paste(session_id: id)` |
73+
74+
Then start it without restarting iTerm2: **Scripts menu → AutoLaunch → remote_paste.py**. After that it launches with iTerm2.
75+
76+
The binding is global, but it only bridges when you're in a remote session with an image on the clipboard. Everywhere else Ctrl+V behaves normally, so vim's visual-block and shell quoting still work.
77+
78+
## Supported setups
79+
80+
remote-paste figures out your session type and delivers the keypress the right way, with nothing to configure. Detection reads the session's command line and tmux role, so it works from any profile, including the default one.
81+
82+
| Your session | How the keypress is delivered |
83+
| --- | --- |
84+
| SSH + tmux `-CC` (iTerm2 integration) | `tmux send-keys C-v` over SSH |
85+
| SSH, plain or regular tmux | `Ctrl+V` straight down the SSH pty |
86+
| Local | `Ctrl+V` to the local agent |
87+
88+
## Configuration
89+
90+
None required. remote-paste finds your tools and reuses whatever SSH host you connected with.
91+
92+
For overrides (a non-standard tmux path on the remote, a fallback host, a custom temp location), copy `config.example.json` to `~/.config/remote-paste/config.json` and set only the keys you need. Each field is documented inline in the example.
93+
94+
## Troubleshooting
95+
96+
The log is the first place to look:
97+
98+
```sh
99+
tail -f ~/Library/Logs/remote_paste.log
100+
```
101+
102+
A successful paste writes a line like `paste[-CC]: host=my-server tmux=work image=148293 ok=True`.
103+
104+
- **Pasting inserts literal `0x16`.** The script isn't delivering a real keystroke. Confirm the key binding is set to *Invoke Script Function* with exactly `remote_paste(session_id: id)`, and that the script is running (Scripts → Console).
105+
- **"No image found in clipboard."** Your clipboard had no image when you pasted. On macOS, Cmd+Shift+Ctrl+4 copies a screenshot to the clipboard; plain Cmd+Shift+4 saves it to a file instead. If `image=0` shows in the log, the image never reached the script. Installing `pngpaste` helps with unusual clipboard formats.
106+
- **`pngpaste: command not found` in the log.** Harmless. The script falls back to osascript. Install `pngpaste` to silence it.
107+
- **Image lands in the wrong tmux pane.** remote-paste targets the active pane of the focused window. If you split a window and the focus tracking is off, open an issue.
108+
109+
## Limitations and roadmap
110+
111+
Today:
112+
113+
- iTerm2 only. The Python API and tmux `-CC` integration are what make this work, and they're iTerm2 features.
114+
- macOS remote only.
115+
- The remote needs a logged-in GUI session for clipboard access.
116+
- Pane targeting follows the focused window's active pane. The common case is covered; a precise per-pane address isn't there yet.
117+
118+
Planned:
119+
120+
- Linux remote support (`xclip` / `wl-copy`), which terminal agents already read from.
121+
- Precise tmux pane targeting.
122+
- A path-injection fallback for setups without clipboard access.
123+
124+
Contributions welcome, especially Linux testing.
125+
126+
## License
127+
128+
MIT. See [LICENSE](LICENSE).

config.example.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"_comment": "Optional. Copy to ~/.config/remote-paste/config.json. Every key is optional; remote-paste auto-discovers sane defaults without it.",
3+
4+
"pngpaste": null,
5+
"_pngpaste": "Path to pngpaste. null => search PATH + /opt/homebrew/bin + /usr/local/bin. Without pngpaste, the clipboard is read via osascript.",
6+
7+
"ssh": null,
8+
"_ssh": "Path to the ssh binary. null => search PATH, default /usr/bin/ssh.",
9+
10+
"remote_tmux": null,
11+
"_remote_tmux": "Path to tmux on the remote host. null => locate it on the remote at paste time (handles Homebrew vs /usr/bin).",
12+
13+
"remote_tmp": "/tmp/iterm-remote-paste.png",
14+
"_remote_tmp": "Where the image is staged on the remote before being loaded onto its clipboard.",
15+
16+
"fallback_host": null,
17+
"_fallback_host": "SSH host to use when a session is clearly a tmux -CC session but the host can't be parsed from its command line. e.g. \"my-server\".",
18+
19+
"profile_session_regex": null,
20+
"_profile_session_regex": "Optional fallback for resolving the tmux session from the iTerm2 profile name when the command line doesn't contain `-s`. Group 1 must be the session. e.g. \"^Project (.+)$\"."
21+
}

install.sh

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
# remote-paste installer — copies the script into iTerm2's AutoLaunch folder
3+
# and checks prerequisites. Re-runnable.
4+
set -euo pipefail
5+
6+
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
SRC="$REPO_DIR/remote_paste.py"
8+
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
9+
DEST="$AUTOLAUNCH/remote_paste.py"
10+
11+
say() { printf '\033[1;36m%s\033[0m\n' "$*"; }
12+
ok() { printf ' \033[1;32m✓\033[0m %s\n' "$*"; }
13+
warn() { printf ' \033[1;33m!\033[0m %s\n' "$*"; }
14+
15+
say "Installing remote-paste-iterm2"
16+
17+
# 1. iTerm2 present?
18+
if [ ! -d "/Applications/iTerm.app" ] && [ ! -d "$HOME/Applications/iTerm.app" ]; then
19+
warn "iTerm2 not found in /Applications — remote-paste is iTerm2-only."
20+
fi
21+
22+
# 2. Copy the script into AutoLaunch.
23+
mkdir -p "$AUTOLAUNCH"
24+
cp "$SRC" "$DEST"
25+
ok "Installed → $DEST"
26+
27+
# 3. Optional dependency: pngpaste (osascript fallback exists, but pngpaste is nicer).
28+
if command -v pngpaste >/dev/null 2>&1; then
29+
ok "pngpaste found ($(command -v pngpaste))"
30+
else
31+
warn "pngpaste not installed (optional). For best results: brew install pngpaste"
32+
fi
33+
34+
cat <<'EOF'
35+
36+
Two manual steps remain (one time):
37+
38+
1. Enable the Python API:
39+
iTerm2 → Settings → General → Magic → ✓ Enable Python API
40+
41+
2. Bind Ctrl+V:
42+
iTerm2 → Settings → Keys → Key Bindings → +
43+
Keyboard Shortcut: ⌃V
44+
Action: Invoke Script Function
45+
Function call: remote_paste(session_id: id)
46+
47+
Then start it now without restarting iTerm2:
48+
Scripts (menu bar) → AutoLaunch → remote_paste.py
49+
(It auto-starts on every iTerm2 launch from here on.)
50+
51+
Logs: ~/Library/Logs/remote_paste.log
52+
EOF
53+
54+
say "Done."

0 commit comments

Comments
 (0)