Skip to content

About

Claude Code status line + hooks for a multi-pane, multi-worktree workflow — see which pane is working on which GitHub issue.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

28 Commits

Folders and files

Repository files navigation

claude-multipane

⚠️ 0.2.0-beta · work in progress. The status line, hooks, setup-panes.sh, and the orchestrator skills all work today, driven by one panes.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.

What's in here

  • 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 just fourlater_pane3 instead of the full path.
  • hooks/track-current-issue.sh — a PostToolUse hook that derives the current issue per worktree from the in progress label actions you already run: it sets the issue on gh issue edit <N> --add-label "in progress" and clears it on gh 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 — a PostToolUse hook that records when Claude's recent commands touch a different worktree than the pane's own (e.g. you started in pane1 but ran a merge in develop), which the status line renders as a "→ branch" hint.
  • hooks/pane-inbox-drain.sh — a Stop hook 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 the pane-inbox skill: 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 runs pane-msg sweep on each send, retrying the guarded nudge until a busy target frees up. Sets a Homebrew PATH + 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 of pane-msg/watcher outside ~/Documents for launchd/TCC, generates + loads the launchd agent).
  • bin/setup-panes.sh — builds the workspace: for a project in panes.conf it creates one git worktree per pane (<repo>_pane1..N on branches pane1..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 supports DRY_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 from panes.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 raw send-keys. Kept for interactive one-offs; for real task handoff prefer pane-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, via git -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.

Create the panes

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 xyz

You 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.

Requirements

Install

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
done

Then 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.

How the #issue segment stays current

Two feeders, both local and fast (no per-render network calls):

  1. Automatic — the track-current-issue.sh hook watches the gh issue label commands you run and writes/clears the per-worktree state file.
  2. 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 with rm -f).

Because the state is keyed by worktree basename, every pane reads its own value.

Inter-pane messaging (pane-inbox)

⚠️ Largely superseded by Claude Code's native cross-session messaging (v2.1.224+, macOS/Linux): sessions discover each other with ListAgents / /list-agents and message via the built-in SendMessage tool — 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; the orchestrate-panes skill 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 the name [ref] form from ListAgents — a bare-name miss fails loudly and names the right ref.

Between machines: node-msg, not pane-msg. Native messaging only reaches sessions of the same account, and pane-msg has no IDs, receipts or retry. For messages between hosts, and anywhere a delivery/read receipt matters, the maintainer's setup uses node-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-msg stays the tool for tmux panes on the same host. Two rules when you do use it: one watcher per pane — pane-inbox-await takes 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 like project:pane1 (that mailbox never gets mail, and pane-inbox-await now 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:

  1. Guarded nudge (send time). send checks the target: if it's idle with a clear prompt, it sends a tiny read-inbox wake via send-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.
  2. launchd watcher (event-driven retry). Every send bumps 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.
  3. Stop-hook drain (turn boundary). When any pane finishes a turn, the pane-inbox-drain.sh Stop hook 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.

Install the messaging layer

~/path/to/claude-multipane/bin/pane-inbox-install.sh

This 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 }
] } ] } }

Optional companions

  • claude-tts — enables the TTS:ON/OFF segment (reads ~/.claude/tts-enabled). Without it the segment simply shows TTS:OFF.
  • claude-apple-bridges — native access to macOS apps (Reminders, Calendar, …) from Claude Code.

License

MIT — see LICENSE.

About

Claude Code status line + hooks for a multi-pane, multi-worktree workflow — see which pane is working on which GitHub issue.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages