| name | paired-branches |
| description | Spin up two Claude Code sessions on different branches of the same repo (typically main + develop), coordinating through a shared JSONL chat file in ~/Downloads. The human (U) types directives in plain English into either tab. This file is the canonical, self-contained recipe: clone-Mac, follow it top to bottom, you have the workflow running in under five minutes. |
/multi-claude:paired-branches: two Claudes, two branches, one repo
A reference recipe for running two Claude Code sessions on the same repository but different branches, with the human steering both in natural language. Validated on cupertino on 2026-05-15: shipped 8 PRs in roughly one hour with one false alarm and one CI-guard miss, both caught by the workflow.
Use this when:
- You have one stable branch (typically
main) you want to keep clean.
- You have a second branch (typically
develop) where active refactors and feature work happen.
- You want one Claude doing the coding and one Claude doing the verification, with you as moderator.
- You want a written audit trail of every decision and action.
Do NOT use this when:
- You only have one task in flight, one Claude is enough.
- You want autonomous agent teams that coordinate without you (use Claude Code's native
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS instead).
- The repo is a monorepo with many simultaneous feature branches (this is a two-branch pattern).
- You're not the only human touching either branch right now (the FF-push step is destructive to anyone else's work).
Architecture in one paragraph
Two git worktrees, one per branch. Two Claude Code sessions, one per worktree. A shared JSONL log file at ~/Downloads/claude-chat.jsonl. Each Claude runs an infinite bash polling loop that wakes when the file grows, reads any new line not from itself, and replies by appending its own line. The human types into either Claude's input box. That Claude prefaces by appending the human's words to the file as from:"U", then resumes the loop. A third terminal tab runs tail -f for a live readable transcript. No daemons, no sockets, no servers.
Why a shared file (and not a named pipe, socket, ntfy, or MCP server)
- POSIX guarantees
write() with O_APPEND is atomic for writes under PIPE_BUF (4 KB on macOS), so two Claudes appending one-line JSON simultaneously never interleave. No locking required as long as each message is one line under 4 KB.
- File survives either Claude restarting. Pipes don't, sockets don't.
- Late joiners read history for free. Pipes and sockets erase past traffic.
tail -f gives you a free human-readable transcript view.
- No new process, no port, no auth, no failure modes.
Named pipes look faster, but Claude's turn cycle is the real bottleneck (seconds), not the IPC (microseconds either way). The pipe's only advantage evaporates.
Prerequisites
- macOS with
git, python3, bash (all default).
- A git repository with at least two long-lived branches (e.g.
main and develop).
- Two Claude Code tabs you can open in Terminal.app (or iTerm2, Ghostty, anything).
- Optional: a third tab for the live tail. Strongly recommended.
Setup (one time per session)
Run once in any terminal, with $REPO, $STABLE, $FEATURE replaced:
REPO=/path/to/cupertino
STABLE=main
FEATURE=develop
cd "$REPO"
git fetch --all --prune
[ -d "$REPO-$FEATURE" ] || git worktree add "$REPO-$FEATURE" "$FEATURE"
touch ~/Downloads/claude-chat.jsonl
Now you have:
$REPO checked out to $STABLE (the original).
$REPO-$FEATURE checked out to $FEATURE (the new sibling worktree).
~/Downloads/claude-chat.jsonl ready for both Claudes to write to.
To tear down at the end:
git -C "$REPO" worktree remove "$REPO-$FEATURE"
Open the tabs
- Tab 1 (stable):
cd "$REPO" && claude
- Tab 2 (feature):
cd "$REPO-$FEATURE" && claude
- Tab 3 (tail): see "Live transcript" below.
The two Claude tabs need their identity prompt pasted. The exact text is in the next two sections.
Paste into Tab 1 (the stable-branch Claude)
You are Claude-{STABLE} running in the worktree at {REPO} on branch {STABLE}. Your peer is Claude-{FEATURE} running in {REPO}-{FEATURE} on branch {FEATURE}. The human user is U; she types into either tab.
Your communication channel is ~/Downloads/claude-chat.jsonl, one JSON record per line. Two schemas are valid:
- Conversational:
{"ts":"...","from":"{STABLE}|{FEATURE}|U","msg":"...","to":"{STABLE}|{FEATURE}"?}
- Structured (for per-action logs):
{"ts":"...","from":"{STABLE}|{FEATURE}","action":"<kebab>","detail":"..."}
Your role on the stable branch is read + verify + promote, not implement. You may:
- Read any file in your worktree.
- Run
git log, git diff, git status, git fetch, git pull, swift build, swift test, any non-mutating shell command.
- Open a PR via
gh against the feature branch ONLY for hotfixes initiated by U.
- On U's explicit
promote command: fast-forward push the feature branch tip into the stable branch (git push origin {FEATURE}:{STABLE}). No squash, no merge commit. If FF is impossible, fall back to opening a release/vX.Y.Z PR with release/vX.Y.Z as the head (NEVER {FEATURE} as head; see below).
You must NOT:
- Modify source files on the stable branch directly (no commits except FF-push).
- Tag releases unless U explicitly says
tag vX.Y.Z.
- Push to a GitLab remote, ever. If
git remote -v shows GitLab in any form, stop and ask U. GitHub is fine.
- Promote without first retesting the new feature-branch tip on your own checkout.
- Promote without an explicit
promote directive from U recorded in the chat file.
The audit-trail rule. Every time U types in this tab, the FIRST thing you do is append her words to the chat file as from:"U", THEN act on them. Parse natural-language targeting: "tell {FEATURE} X" → "to":"{FEATURE}"; "{FEATURE}, ..." or "ask {FEATURE} ..." → "to":"{FEATURE}"; otherwise broadcast (omit ). Strip the natural-language prefix from . Don't ask U to confirm. Example:
Replace {STABLE}, {FEATURE}, {REPO} literals with your values before pasting. The skill argument-hint <repo-path> [stable-branch=main] [feature-branch=develop] is a reminder.
Paste into Tab 2 (the feature-branch Claude)
You are Claude-{FEATURE} running in the worktree at {REPO}-{FEATURE} on branch {FEATURE}. Your peer is Claude-{STABLE} running in {REPO} on branch {STABLE}. The human user is U; she types into either tab.
Your communication channel is ~/Downloads/claude-chat.jsonl, one JSON record per line. Two schemas are valid (see Tab 1 prompt for definitions).
Your role on the feature branch is implement, not verify. You may:
- Read, write, edit, rename, delete files in your worktree.
- Run
git checkout -b, commit, push, open PRs via gh against {FEATURE}.
- Run
swiftformat, swiftlint, tests, builds, smoke checks.
- Save memory entries to
~/.claude/projects/<slug>/memory/ when you discover a non-obvious rule (and mirror per the user's global mirror rule if it applies in this repo).
You must NOT:
- Touch the sibling worktree at
{REPO} (that's the stable-branch Claude's territory). No git -C {REPO} calls.
- Merge into stable directly. Promotion is the stable Claude's job, gated on U's
promote.
- Open a PR with
{FEATURE} as the head branch. Use feature branches off {FEATURE} (e.g. fix/<issue>-<slug>), then PR them with base {FEATURE}.
- Push to a GitLab remote, ever.
The audit-trail rule, the polling loop, and the message schemas are identical to Tab 1's prompt. When U types in this tab, append her words to the chat file as from:"U" FIRST, then act.
Per-action logging cadence (you can suggest U promote this for the stable Claude too): when you are doing real coding work (writing files, running tests, opening PRs), emit one structured log line per discrete action, not just at PR-open milestones. Action vocabulary: branch-create, file-write, test-suite-run, swiftformat-clean, commit, push, pr-open, pr-merge, stash-park, stash-pop. Detail field carries the specific info (SHA, filename, count).
Live transcript (Tab 3)
Plain raw view:
tail -f ~/Downloads/claude-chat.jsonl
Pretty-printed view (handles both message schemas):
tail -f ~/Downloads/claude-chat.jsonl | python3 -u -c "
import sys, json
for line in sys.stdin:
line = line.strip()
if not line: continue
try:
d = json.loads(line)
except Exception:
print('[parse-error]', line, flush=True); continue
ts = d.get('ts','')[11:19]
f = d.get('from','?')
t = d.get('to','')
arrow = f'->{t}' if t else ''
if 'msg' in d:
body = d['msg']
elif 'action' in d:
body = f\"[{d['action']}] {d.get('detail','')}\"
else:
body = json.dumps({k:v for k,v in d.items() if k not in ('ts','from','to')})
print(f'{ts} [{f}{arrow}] {body}', flush=True)
"
Output line example:
07:10:00 [main->develop] post-promote retest on main @ 5b9e288 ... 1667/1667 ...
07:14:24 [main] [cadence-change] U directive: log ANY action to jsonl
How U steers (vocabulary the Claudes already understand)
Type any of these into either tab. The receiving Claude logs your words to the chat as from:"U" then acts.
| You type | What happens |
|---|
start fixing now | Broadcast directive. Whichever Claude has work to do, does it. |
tell {FEATURE} <thing> | Targeted at the feature Claude. Stable Claude relays via chat. |
ask {STABLE} to <thing> | Targeted at the stable Claude. |
promote | Stable Claude FF-pushes {FEATURE}:{STABLE} and retests. |
test all | Stable Claude runs full retest battery on the current stable tip. |
status | Whichever tab you typed in reports current state (idle / mid-test / waiting). |
stop | Both Claudes exit their polling loops and wait. Posts {"from":"U","msg":"END"}. |
wipe | Clears the chat file: > ~/Downloads/claude-chat.jsonl. |
no <X> rule lifted | Remove a constraint they were carrying (e.g. "no disk changes stops now"). |
You can address either Claude in plain English. They parse "tell A...", "B,...", "ask main...", "develop: ..." patterns and route accordingly.
Gotchas (lessons from the cupertino session, 2026-05-15)
These are real bugs and almost-bugs we hit. The skill prompts above already encode the fixes, but knowing the why helps you debug when the workflow drifts.
-
Typed input during a polling bash is queued, not delivered. Claude Code holds your input behind the running tool. The timeout 540 bash poll runs up to 9 minutes; your typed directive doesn't reach Claude until that exits. Fix: press Esc in the tab to interrupt the polling bash, then your queued line flows through. Or wait for the timeout. The prompts above use a 540-second cap precisely so a stuck loop doesn't strand U for more than 9 minutes.
-
Main may FF-push without explicitly logging U's promote. The audit-trail rule (log U input as from:"U" BEFORE acting) is easy for Claude to forget under load. If you find a promote-ff action with no preceding U-line, your audit trail has a hole. Fix is in the prompt but worth occasional spot-checks: grep '"from":"U"' ~/Downloads/claude-chat.jsonl | grep promote should show every promote authorization.
-
Main may promote a tip it never retested. Step 3 of the workflow (retest the new feature tip) precedes step 4 (FF-push on U's promote). When the chat is busy, main can skip step 3 and go straight from "previous tip green" to "FF-push." Fix is in the prompt ("Promote without first retesting the new feature-branch tip... is forbidden"). When in doubt, type status to force a state report before promoting.
-
Hardcoded numbers in printf templates. Action-log printf strings tend to be copy-pasted between runs. The cupertino main Claude posted test=1646/1646 for three rounds after the count had moved to 1664 because the bash heredoc had 1646 literal. Fix: parse the actual count from the test output, never hardcode. The retest action's detail field must be derived from the run, not from the template.
-
JSON malformed by manual concatenation. Main's printf produced "action":"promote-ff":"detail":"..." (colon instead of comma) once. Fix: if Claude composes JSON with shell echo, double-check the separators. Better, use python3 -c 'import json; print(json.dumps({...}))' so the language enforces validity.
-
Stale binaries cause false-alarm hangs. Cupertino main reported the mock-ai-agent integration test "hung at 31s" after a promote. Investigation: the mock-ai-agent binary on disk was from May 11; rebuilding at HEAD made it run in 9s. Fix: any integration test client that lives outside the repo's normal build must be rebuilt against HEAD before each retest cycle, or you'll chase ghosts.
Terminal recipes
Pick one. All three host the same workflow; only the visual layout differs.
A. Apple Terminal.app (default macOS, no install)
Three tabs in one window. Cmd+T to add tabs. Cmd+1/2/3 to switch. No splits, no multiplexer.
- Tab 1:
cd "$REPO" && claude, then paste the Tab 1 prompt.
- Tab 2:
cd "$REPO-$FEATURE" && claude, then paste the Tab 2 prompt.
- Tab 3: tail script from "Live transcript" above.
This is the recipe used to validate the workflow on 2026-05-15. No setup required.
B. iTerm2 native splits (already installed in /Applications)
One window, three panes side-by-side. Cmd+D for vertical split, Cmd+Shift+D for horizontal. iTerm2 is one of Claude Code's primary rendering targets, so no TUI flicker.
- Pane 1 (left): Claude-stable.
- Pane 2 (center): Claude-feature.
- Pane 3 (right): tail script.
Use this if you want both Claudes visible at the same time without Cmd+Tab-ing tabs.
C. tmux (only if you SSH into another machine to run this)
tmux new-session -d -s claudes -c "$REPO"
tmux send-keys -t claudes "claude" C-m
tmux split-window -h -t claudes -c "$REPO-$FEATURE"
tmux send-keys -t claudes "claude" C-m
tmux split-window -v -t claudes
tmux send-keys -t claudes "tail -f ~/Downloads/claude-chat.jsonl" C-m
tmux attach -t claudes
Caveat: Claude Code has documented TUI rendering bugs in tmux: flicker on streaming output, broken mouse/scrolling, fullscreen clipping under tmux status bar. Live with it if you must (SSH), avoid it locally.
When NOT to use this skill
- Single-Claude task. If you're refactoring within one branch and don't need a "verifier", one tab is simpler.
- Trunk-based workflow with no long-lived branches. Pattern relies on a stable/feature split.
- Repo with multiple humans pushing concurrently to either branch. The FF-push step assumes you own both branches' state.
- Repo where
main has commits the feature branch lacks. FF-push will fail; fall back to release/vX.Y.Z PRs, but the simpler version of this skill assumes feature is always a strict descendant of stable.
- You want full autonomy (no per-action gate). Use Claude Code's built-in agent teams (
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) instead. That feature uses git for coordination and has Anthropic's official support, but it's a different paradigm.
Cross-references
- Anthropic agent teams (the official multi-Claude path, experimental): https://code.claude.com/docs/en/agent-teams
- Claude Code TUI + tmux known bugs: github.com/anthropics/claude-code issues #37076, #37283, #38810, #51497
- A sibling pattern exists for inter-machine coordination via ntfy or a shared file. Different problem (two Macs, audio plus video pipeline), same insight (a file-based bus survives restarts and gives free transcripts).
- The shared engineering-discipline rules apply to both Claudes regardless of branch.
Validation provenance
This skill is the artifact of one productive session. The cupertino-refactor session on 2026-05-15 from approximately 05:25 to 07:25 UTC shipped:
- PR #571 (develop → main hotfix for
resources/list returning 55,915 entries instead of ≤1000)
- PR #572 (Crawler.AppleDocs.State closure-purge → 7 named actor methods)
- PR #573 (Sample.Core.Downloader closure-purge)
- PR #574 (CLIImpl namespace anchor move to CLI target root)
- PR #575 (drop 3 pre-existing test warnings)
- PR #577 (issue #253: concurrent save detection via SaveSiblingGate, +18 tests)
- PR #578 (issue #280:
--no-reap flag + env var)
- PR #579 (docs-drift fixup for #578)
- PR #580 (issue #293: URI scheme middle path segments fix)
Test count grew 1639 → 1669, build warnings dropped 3 → 0, the broken v1.1.0 resources/list symptom (the integration test that hung "forever") now finishes in 9 seconds. Two real MCP server bugs were found mid-session (issues #581, #582) and the audit-trail rule + per-action logging cadence were established by U directive at 07:14:24.
If this workflow ever feels too heavy for the task at hand, remember: it earned its weight in one session.