Write a directive to a file in one pane, it arrives as a single paste in the other. Scope-keyed so two projects run in parallel without cross-talk. Watcher-managed, launchd-resilient.
Four commands cover 95% of day-to-day usage. Read once, memorise, refer back when something doesn’t paste.
| Switch | Scope | Set by | State lives in |
|---|---|---|---|
| Watcher running | Global (whole Mac) | launchd | OS process table |
| Routing armed | Global | handoff-on / handoff-off | ~/.claude/handoff/active |
| Pane bound | Per pane | handoff-use + handoff-claim-* | ~/.claude/handoff/*-target.<scope>.id |
1. Is the watcher running? — launchctl list | grep handoff
2. Is routing armed? — ls ~/.claude/handoff/active
3. Is the target pane still bound? — handoff-targets
90% of failures are #3. After an iTerm restart, UUIDs change and you must re-claim.
Every command below has a pane tag showing where to run it:
IMPL the pane where Claude implements AUDIT the pane where Claude reviews ANY file-only, anywhere MAC plain Mac Terminal outside Claude
One installer covers everything: dependencies, file placement, launchd registration, and arming. Idempotent — safe to re-run after edits.
Required dependency. Without it the watcher cannot detect file events.
The installer:
To uninstall: ./handoff/install.sh --uninstall
The user-facing API lives in shell functions. Append the block, source ~/.zshrc, you’re done.
You now have these commands available in every pane:
| Command | What it does |
|---|---|
| handoff-use <scope> | Set this pane’s HANDOFF_SCOPE. Required before any other handoff command. |
| handoff-claim-impl | Bind this pane as the IMPL target for the current scope. |
| handoff-claim-audit | Bind this pane as the AUDIT target for the current scope. |
| handoff-on / handoff-off | Arm / disarm routing (creates / removes ~/.claude/handoff/active). |
| handoff-status | Print ARMED or DISARMED. |
| handoff-halt | Write HALT.<scope> (reason from stdin) — pauses only this scope. |
| handoff-resume | Remove HALT.<scope>. |
| handoff-targets | List all current bindings (UUID per role per scope). |
| handoff-scope | Print this pane’s current scope. |
| send-impl / send-audit | Pipe stdin to the scoped inbox. |
Example uses scope name myapp. Substitute your project name (lowercase, no spaces).
Open iTerm, open a new pane in the CC-IMPL profile, cd to the project, start Claude (cc).
Then inside Claude in that pane, set the scope once and claim:
Expected output:
Open another iTerm pane in the CC-AUDIT profile, start Claude there.
Inside Claude, set the scope, type /start-audit to load the audit skill, then claim:
Expected:
Within ~1 second, the IMPL pane should show smoke test <timestamp> as a single paste.
Verify the file was drained AND the watcher logged the delivered-marker:
Expected — two adjacent lines per delivery:
iTerm assigns a new session UUID every time a pane opens. Yesterday’s *.id bindings point at UUIDs that no longer exist. You only need to re-claim.
Two directions, two scoped filenames, one delivery contract: write a file, the watcher pastes it.
Within ~1s, the IMPL pane receives the full multi-line text as one paste.
Multi-line content is preserved. Code blocks, blank lines, and indentation survive because the watcher wraps content in bracketed-paste escape codes.
Don’t add a trailing Enter. The watcher adds the final newline that submits the paste.
Filenames are scoped. Always to-impl.<scope>.txt and to-audit.<scope>.txt. Unscoped writes are refused with a WARN.
HALT is scoped. A halt in myapp does not block any other project.
This writes ~/.claude/handoff/HALT.$HANDOFF_SCOPE. Equivalent explicit form:
Common failure modes and how to diagnose them from /tmp/handoff.log.
How to tell success from failure (since the file looks the same in both cases):
| Log pattern | Meaning |
|---|---|
| -> IMPL scope=X (N lines) followed by delivered to IMPL/X — file truncated to 0 bytes (delivered-marker) | ✓ Success |
| -> IMPL scope=X (N lines) followed by WARN: delivery failed for to-impl.X.txt — file NOT truncated; inspect and retry | ✗ Failure file left intact for inspection |
| No -> IMPL scope=X line at all | ! Silent watcher not running, routing disarmed, or write happened to wrong path |
Three causes, in order of likelihood:
The watcher does NOT hot-reload. After editing ~/.claude/hooks/pane-handoff.sh, restart:
That’s correct. The watcher uses fswatch | while ..., which forks a subshell. Steady state under launchd is 3 processes total: 2 × pane-handoff.sh (main + while-subshell) + 1 × fswatch. Anything more or fewer is a problem.
The clean way to deactivate the entire system while keeping pane bindings + scope + watcher process intact. Use it when you want to stop file→pane delivery temporarily (e.g. taking a break, swapping projects, debugging).
What handoff-off does NOT do:
To fully deactivate everything (rare — usually only for OS troubleshooting):
Every command, every path, every rule in one place.
| Path | What it is |
|---|---|
| ~/.claude/hooks/pane-handoff.sh | The watcher script |
| ~/.claude/hooks/enforce-handback.py | Stop hook — blocks IMPL from claiming “→ handed back” without writing the file |
| ~/.zshrc | Defines handoff-* and send-* shell functions |
| ~/Library/LaunchAgents/com.user.handoff.plist | launchd config — runs watcher on login, restarts on crash |
| /tmp/handoff.log | Watcher stdout + stderr |
| ~/.claude/handoff/active | Flag — must exist for routing to work |
| ~/.claude/handoff/HALT.<scope> | Per-scope pause sentinel |
| ~/.claude/handoff/{impl,audit}-target.<scope>.id | Binding files (one UUID per scope per role) |
| ~/.claude/handoff/to-{impl,audit}.<scope>.txt | Inbox files (auto-truncated on delivery) |
| ~/.claude/handoff_archive/ | Sibling dir for old hand-backs — never inside the watched dir |
Five probes that prove two projects can run concurrently without cross-talk.
Pass: Window A IMPL shows only ping A …; Window B IMPL shows only ping META …; log shows two separate -> IMPL scope=… + delivered-marker pairs. No WARN.
Pass: log shows WARN: refusing unscoped to-impl.txt; file drained; neither pane received the text.
Pass: log shows ⛔ HALT scope=proj_a AND meta delivery succeeds despite the HALT.
Pass: shell prints the error; no file written; no log line generated.
Restart Claude in the AUDIT pane, run handoff-use proj_a + /start-audit. The slash command does NOT pre-flight read the inbox file (it is 0 bytes by design after delivery); it only checks the scoped HALT (HALT.proj_a — absent), and stops cleanly. No foreign-project content surfaces.
The actual flow under the hood — useful for debugging unfamiliar failure modes.
AUDIT pane Watcher IMPL pane (Claude) (pane-handoff.sh) (Claude) | | | | Bash tool: | | | cat > to-impl.myapp.txt | | | ----------------------> | | | fswatch fires | | reads file | | wraps in bracketed-paste | | osascript -> iTerm session id ------> | | <----- ok | | truncates file to 0 bytes | | logs delivered-marker |
Without it, multi-line content with blank lines or code blocks would submit on the first \n. Bracketed paste tells the terminal “this is paste content” — the receiving Claude sees it as a single fresh user message.
So two projects run side-by-side. to-impl.proj_a.txt routes to impl-target.proj_a.id; to-impl.meta.txt routes to impl-target.meta.id. No cross-talk possible.
Without it, the model can claim → handed back in chat without actually writing the file — leaving AUDIT idle and the loop silently broken. enforce-handback.py detects this exact failure mode and forces the model to either write the file or remove the marker.
| Date | Change |
|---|---|
| 2026-06-05 | Integrated into the claude-code-multipane-iterm2 repo. Public installer, sanitised paths, AppleScript-injection fix (argv-passed file/session), symlink refusal, scope regex tightened to ^[A-Za-z0-9_-]+$, legacy plist auto-cleanup, Stop hook auto-merged into ~/.claude/settings.json. |
| 2026-05-15 | Watcher logs delivered to {IMPL,AUDIT}/<scope> after each successful bracketed-paste + file-truncation. Closes the false-alarm where receiving panes saw a 0-byte inbox and misdiagnosed it as a write failure. |
| 2026-05-15 | /start-audit no longer pre-flight-reads to-audit.<scope>.txt. The hand-back arrives via bracketed paste in the chat transcript; the file is transport, not state. |
| 2026-05-13 | Initial scope-keyed handoff. Per-scope HALT sentinels. Multi-project isolation probes. |