⚠️ 0.2.0-beta · work in progress. The status line, hooks,setup-panes.sh, and the orchestrator skills all work today, driven by onepanes.conf. Expect rough edges.
A status line and workflow toolkit for running Claude Code across several tmux panes — one pane per git worktree — with an orchestrator pane that dispatches work to the others and sees, at a glance, which pane is working on which GitHub issue.
~/Documents/xyz_pane2 pane2 [xyz_pane2] #4711 [Opus 4.8] ctx:38% TTS:ON
^^^^^ ← the issue this pane is on
The status line is the visible part; the point is the workflow: you keep N Claude Code sessions open (one per worktree/pane), hand each a task, and the status line tells you who's on what.
statusline-command.sh— the status line. Per pane it shows: working directory, git branch + worktree name, the active GitHub issue (orange#N), a cross-worktree "→" indicator when a pane's work has moved to another worktree, the model, context usage, and optional TTS/STT status. Optional: list noisy path prefixes in~/.claude/statusline-trim-prefixes(one per line,~-notation, e.g.~/Documents/Xcode/) and the directory segment shows justfourlater_pane3instead of the full path.hooks/track-current-issue.sh— aPostToolUsehook that derives the current issue per worktree from thein progresslabel actions you already run: it sets the issue ongh issue edit <N> --add-label "in progress"and clears it ongh issue close <N>/--remove-label "in progress". State lives in/tmp/claude-current-issue-<worktree>.txt; the orchestrator pane can also write that file directly when it dispatches a task.hooks/track-active-context.sh— aPostToolUsehook that records when Claude's recent commands touch a different worktree than the pane's own (e.g. you started inpane1but ran a merge indevelop), which the status line renders as a "→ branch" hint.hooks/pane-inbox-drain.sh— aStophook that delivers queued inter-pane messages: when a pane finishes a turn it drains that pane's inbox and hands any messages to Claude through the hook channel, without ever typing into the input prompt. No-op unless a message is waiting. See Inter-pane messaging.bin/pane-msg— the messaging CLI behind thepane-inboxskill: append a message to another pane's inbox file (send), drain your own (read/peek), check counts + stale flags (status), or run the delivery sweep (sweep). Payload lives in files, so it never clobbers what the user is typing in the target pane.hooks/pane-inbox-watcher.sh+launchagents/com.claude-multipane.pane-inbox-watcher.plist— the event-driven delivery watcher: a launchd agent watches the mailbox trigger file and runspane-msg sweepon each send, retrying the guarded nudge until a busy target frees up. Sets a HomebrewPATH+ UTF-8 locale that launchd otherwise lacks. See Inter-pane messaging.bin/pane-inbox-install.sh— one-shot idempotent installer for the messaging layer (symlinks the skill + Stop hook, installs real copies ofpane-msg/watcher outside~/Documentsfor launchd/TCC, generates + loads the launchd agent).bin/setup-panes.sh— builds the workspace: for a project inpanes.confit creates one git worktree per pane (<repo>_pane1..Non branchespane1..N), opens a tmux session split into those panes,cds each into its worktree, and launches Claude Code in each. Idempotent (skips existing worktrees/session) and supportsDRY_RUN=1.panes.conf.example— the shared config the tooling reads (project name, main worktree path, main branch, pane count, tmux target). Copy to~/.claude/panes.conf.skills/— Claude Code skills (task-specific instruction files Claude loads on demand) for the orchestrator layout, all config-driven frompanes.conf:orchestrate-panes— the full operating protocol for the coordinating pane: dispatch briefs, ping-back rule, independent verification, central merge/close, capacity handover. The distilled lessons from running this daily.send-to-pane— hand a self-contained task to another pane's Claude ("tell pane2 …") via rawsend-keys. Kept for interactive one-offs; for real task handoff preferpane-inbox.pane-inbox— file-based messaging between panes that never clobbers the user's input (see below). Now the fallback channel: Claude Code's native cross-session messaging is preferred where available.merge-to-develop— merge the current worktree's branch into the integration branch (in its own worktree, viagit -C).sync-panes-with-develop— bring every pane worktree up to date with the integration branch, reporting per-pane status.focus-worktree— reset the status line's cross-worktree "→" hint back to the pane you actually want to work in.
Configure your project in ~/.claude/panes.conf (one line — see panes.conf.example), then:
DRY_RUN=1 ~/path/to/claude-multipane/bin/setup-panes.sh xyz # preview
~/path/to/claude-multipane/bin/setup-panes.sh xyz # do it
tmux attach -t xyzYou get N panes, each in its own git worktree, each running Claude Code — one "orchestrator" pane plus workers, the layout this toolkit is built around.
- macOS + Claude Code, tmux
jq,git, and (for the issue segment) the GitHub CLIgh
Clone anywhere, then symlink the scripts into ~/.claude/ so the files you run are exactly these (no second copy to drift):
git clone https://github.com/more-io/claude-multipane.git
cd claude-multipane
mkdir -p ~/.claude/hooks
ln -sf "$PWD/statusline-command.sh" ~/.claude/statusline-command.sh
ln -sf "$PWD/hooks/track-current-issue.sh" ~/.claude/hooks/track-current-issue.sh
ln -sf "$PWD/hooks/track-active-context.sh" ~/.claude/hooks/track-active-context.sh
ln -sf "$PWD/hooks/pane-inbox-drain.sh" ~/.claude/hooks/pane-inbox-drain.sh
ln -sf "$PWD/bin/pane-msg" ~/.claude/pane-msg # inter-pane messaging CLI
cp panes.conf.example ~/.claude/panes.conf # then edit
# orchestrator skills (optional):
for s in orchestrate-panes send-to-pane pane-inbox merge-to-develop sync-panes-with-develop focus-worktree; do
mkdir -p ~/.claude/skills/"$s"
ln -sf "$PWD/skills/$s/SKILL.md" ~/.claude/skills/"$s"/SKILL.md
doneThen register them in ~/.claude/settings.json:
{
"statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" },
"hooks": {
"PostToolUse": [
{ "matcher": "Bash|Edit|Write|NotebookEdit", "hooks": [
{ "type": "command", "command": "bash ~/.claude/hooks/track-active-context.sh", "timeout": 3 },
{ "type": "command", "command": "bash ~/.claude/hooks/track-current-issue.sh", "timeout": 3 }
] }
],
"Stop": [
{ "hooks": [
{ "type": "command", "command": "bash ~/.claude/hooks/pane-inbox-drain.sh", "timeout": 8 }
] }
]
}
}The Stop entry powers pane-inbox (see below) and is a no-op when no message is queued — omit it if you don't use inter-pane messaging. Restart your Claude Code sessions so the hooks register. The status line updates immediately.
Two feeders, both local and fast (no per-render network calls):
- Automatic — the
track-current-issue.shhook watches thegh issuelabel commands you run and writes/clears the per-worktree state file. - Orchestrator — the coordinating pane writes a target pane's file when it hands off a task:
printf '4711' > /tmp/claude-current-issue-<worktree>.txt(clear withrm -f).
Because the state is keyed by worktree basename, every pane reads its own value.
⚠️ Largely superseded by Claude Code's native cross-session messaging (v2.1.224+, macOS/Linux): sessions discover each other withListAgents//list-agentsand message via the built-inSendMessagetool — delivery lands between tool calls in a busy session and starts a new turn in an idle one, which is exactly the wake-a-sleeping-pane problem the machinery below works around. If your setup has it, use it; theorchestrate-panesskill now prefers it. The layer below remains useful as a fallback (older Claude Code, providers without the feature) and for its JSONL audit trail per mailbox. One native-messaging quirk worth knowing: a first-contact send needs thename [ref]form fromListAgents— a bare-name miss fails loudly and names the right ref.
Between machines:
node-msg, notpane-msg. Native messaging only reaches sessions of the same account, andpane-msghas no IDs, receipts or retry. For messages between hosts, and anywhere a delivery/read receipt matters, the maintainer's setup usesnode-msg(lives in a private multi-host repo, not here): one file per message, states queued → sent → delivered → read → done, an outbox with retry and dead letters, a per-host launchd delivery agent that wakes the target through the session's inbox socket.pane-msgstays the tool for tmux panes on the same host. Two rules when you do use it: one watcher per pane —pane-inbox-awaittakes a lock per target, so never start a second one; and the target is a tmux target (session:window.pane, e.g.4later:1.1), never a Claude session name likeproject:pane1(that mailbox never gets mail, andpane-inbox-awaitnow rejects it).
The obvious way to hand work between panes is tmux send-keys into the target's
input line (that's send-to-pane). It has two problems: it overwrites whatever
the user is typing in that pane, and a long brief can stick as an unsubmitted
[Pasted text]. pane-inbox fixes both by moving the payload off the prompt.
Model: pane-msg send appends the message (a JSON line) to the recipient's
inbox file under ~/.claude/tmux-state/mailbox/. The payload is never typed into
the prompt. Three independent mechanisms deliver it — together they give
at-least-once delivery without clobbering input:
- Guarded nudge (send time).
sendchecks the target: if it's idle with a clear prompt, it sends a tinyread-inboxwake viasend-keys. The check reads the prompt with escape sequences (capture-pane -e) so it can tell a real keystroke from Claude Code's dimmed next-step suggestion (ghost text, SGR faint) — otherwise the ever-present suggestion looks like typing and the pane is never woken. If the target is busy or the user is really typing, no keystroke is sent. - launchd watcher (event-driven retry). Every
sendbumps a trigger file (mailbox/.event); a launchd agent (WatchPaths) runs a bounded, self- terminating sweep that retries the guarded nudge until the target is free — this is what delivers to a pane that was busy at send time and then went idle (the "sleeping pane" gap). No perpetual polling: the sweep exits once inboxes are empty or a short window elapses. - Stop-hook drain (turn boundary). When any pane finishes a turn, the
pane-inbox-drain.shStophook drains its own inbox through the hook channel, never touching the input line.
pane-msg status shows unread counts and flags anything unread too long as
⚠ STALE Nmin, so a stuck message is visible to a human.
pane-msg send 4later:1.2 --type task --ref 4711 "Self-contained brief…" # guarded nudge + watcher
pane-msg send 4later:1.2 --type note --silent "FYI, no rush." # no send-time nudge; watcher/Stop-hook deliver
pane-msg read # drain your own inbox (auto-detects your pane via $TMUX_PANE)
pane-msg status # unread counts + STALE flags across all mailboxes--type is task | question | answer | status | note; --ref threads replies
(e.g. an issue number). The orchestrator and worker panes use this as their
default channel — see the orchestrate-panes and send-to-pane skills.
~/path/to/claude-multipane/bin/pane-inbox-install.shThis symlinks the skill and the Stop hook, installs real copies of
pane-msg and the watcher wrapper into ~/.claude (launchd can't execute files
under ~/Documents — TCC — so these can't be symlinks), generates + loads the
launchd watcher, and creates the trigger file. Re-run it after editing pane-msg
or the wrapper. It does not touch settings.json: register the Stop hook
there once by hand —
{ "hooks": { "Stop": [ { "hooks": [
{ "type": "command", "command": "bash ~/.claude/hooks/pane-inbox-drain.sh", "timeout": 8 }
] } ] } }- claude-tts — enables the
TTS:ON/OFFsegment (reads~/.claude/tts-enabled). Without it the segment simply showsTTS:OFF. - claude-apple-bridges — native access to macOS apps (Reminders, Calendar, …) from Claude Code.
MIT — see LICENSE.