Spawn full Claude Code sessions as workers, coordinated through a bash heartbeat and file-based state. Attach to any of them mid-run and take over by hand.
Built March 2026. Reviewed 2026-07-31 against current Claude Code. Still runs, but the reason to use it has changed.
This was built when subagents could not do much. Since then Claude Code absorbed most of it. Per Anthropic's subagent docs, subagent frontmatter now accepts maxTurns, hooks, mcpServers, skills, memory, effort, background and isolation, and each subagent runs in its own context window.
So the original pitch - "subagents are crippled, use full sessions instead" - is no longer true. Try subagents first. They are one file instead of a bash control loop.
| Original claim (March 2026) | Status today |
|---|---|
| Subagents cap at ~25 tool calls | False. maxTurns is a frontmatter field. |
| Subagents get no hook events | False. hooks is configurable per agent; hook events carry agent_id. |
| Subagents cannot reach MCP servers | False. mcpServers is configurable per agent. |
| Subagents share the parent context window | False. Each runs in its own. |
Three things survive, and only three. If none of them matter, use the native tools.
- A session you can attach to.
tmux attach, watch it work, interrupt it, answer its question by hand, let it continue. No lost context. When a long autonomous run goes sideways at minute 40, taking the wheel is worth the whole control loop. - Full session lifecycle. Compaction,
/clear,/resume, slash commands, mid-run model switching viasend-keys. - An orchestration loop you own. Bash and JSON files on disk. Runs on a server, across repos, over SSH, on a schedule. No dependency on a product feature staying put.
Also worth knowing: Agent Teams is the first-party option, still behind CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1, and its split-pane display mode needs tmux or iTerm2 anyway. Dynamic workflows orchestrate subagents at scale from a script, up to 16 concurrent and 1,000 per run.
┌─────────────────────────────────────────────────────────┐
│ tmux Session │
│ │
│ ┌──────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Window 0 │ │ Window 1 │ │ Window 2 │ │
│ │ Orchestrator │ │ Worker 1 │ │ Worker 2 │ ... │
│ │ (Claude Code)│ │ (Claude) │ │ (Claude) │ │
│ │ │ │ Sonnet │ │ Haiku │ │
│ └──────┬───────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └───────────┬───┴──────────────┘ │
│ │ │
│ _orchestrator/ │
│ ├── config.json (rules & intervals) │
│ ├── session.json (runtime state) │
│ ├── log.jsonl (audit trail) │
│ ├── workers/ │
│ │ ├── w1.json (worker 1 status) │
│ │ └── w2.json (worker 2 status) │
│ ├── results/ │
│ │ ├── w1/ (worker 1 output) │
│ │ └── w2/ (worker 2 output) │
│ └── inbox/ │
│ ├── w1/ (escalations) │
│ └── w2/ (escalations) │
└─────────────────────────────────────────────────────────┘
┌──────────────┐
│ heartbeat.sh │ (external bash loop)
│ │
│ sleep → check idle → send /cycle │
│ adaptive: 30s/120s/300s │
└──────────────┘
Every interval, the orchestrator runs:
COLLECT ──→ EVALUATE ──→ ACT ──→ LOG
│ │ │ │
│ Read │ Map to │ Send │ Write
│ worker │ 6 states │ cmds │ audit
│ status │ + check │ merge │ entry
│ files │ escala- │ review│ update
│ + pane │ tions │ gate │ .ready
│ fallback │ │ │
Worker States: SAFE_TO_RESTART · DO_NOT_INTERRUPT · CONTEXT_LOW_CONTINUE · RATE_LIMITED_WAIT · ERROR_STATE · UNKNOWN
- tmux installed
- jq installed
- Claude Code CLI (
claude) installed and authenticated
# 1. Clone this repo
git clone https://github.com/primeline-ai/claude-tmux-orchestration.git
cd claude-tmux-orchestration
# 2. Copy files to your project
cp -r _orchestrator/ /path/to/your/project/
# 3. Make scripts executable
chmod +x /path/to/your/project/_orchestrator/*.sh
# 4. Start the orchestrator
cd /path/to/your/project
./_orchestrator/orch-bootstrap.sh# Set ENV (bootstrap exports these, or set manually)
export SESSION_NAME="orch-your-project"
export ORCH_DIR="$(pwd)/_orchestrator"
export PROJECT_ROOT="$(pwd)"
# Spawn a worker with Sonnet
./_orchestrator/spawn-worker.sh w1 sonnet "Implement the auth module"
# Spawn a second worker with Haiku (for research)
./_orchestrator/spawn-worker.sh w2 haiku "Research OAuth2 best practices"# Attach to the tmux session
tmux attach -t orch-your-project
# Switch between windows
# Ctrl+B, 0 = Orchestrator | Ctrl+B, 1 = Worker 1 | Ctrl+B, 2 = Worker 2
# Watch the heartbeat log
tail -f _orchestrator/heartbeat.log
# Graceful stop
touch _orchestrator/.stopThe entry point checks dependencies (tmux, jq), creates or reuses a tmux session named orch-<project>, initializes state files, and starts the heartbeat as a background process.
A bash loop that keeps the orchestrator alive. It:
- Sleeps for an adaptive interval (30s when stuck, 120s normal, 300s idle)
- Collects worker statuses from
_orchestrator/workers/*.json - Checks if the orchestrator pane is idle (via
tmux capture-pane) - Sends the
/orchestrate-cyclecommand viatmux send-keyswhen idle - Logs every cycle to
log.jsonl
The heartbeat uses a .ready file handshake - it only sends commands when the orchestrator pane is actually idle. This prevents prompt collisions.
A 6-step sequence:
| Step | Action | Detail |
|---|---|---|
| 1 | Create tmux window | tmux new-window -n w1 |
| 2 | Start Claude Code | With ORCHESTRATOR_WORKER_ID ENV var |
| 3 | Wait for boot | Poll capture-pane for idle prompt (max 60s) |
| 4 | Switch model | Send /sonnet or /haiku via send-keys |
| 5 | Inject task prompt | Multiline via tmux load-buffer + paste-buffer |
| 6 | Confirm status | Poll for workers/w1.json (max 90s) |
Workers communicate through files - no custom servers, no daemons, no IPC:
Worker status file (_orchestrator/workers/w1.json):
{
"worker_id": "w1",
"status": "working",
"task": "Implement auth module",
"model": "sonnet",
"branch": "orch/feature/w1",
"started": "2026-03-22T10:00:00Z",
"updated": "2026-03-22T10:15:00Z",
"progress": "Created JWT middleware, writing tests",
"turns": 42,
"blockers": []
}Workers can escalate to the orchestrator:
Escalation file (_orchestrator/inbox/w1/escalation.json):
{
"type": "guidance",
"question": "Should I use RS256 or HS256 for JWT signing?",
"context": "RS256 is more secure but requires key management",
"blocking": true
}The entire architecture depends on reliably delivering prompts to Claude Code via tmux. Three things matter:
# 1. Use send-keys -l (literal mode) + separate Enter
tmux send-keys -t "$SESSION:w1" -l "$PROMPT_TEXT"
sleep 0.5
tmux send-keys -t "$SESSION:w1" Enter
# 2. Strip ANSI before parsing capture-pane output
OUTPUT=$(tmux capture-pane -t "$SESSION:w1" -p | strip_ansi)
# 3. For multiline prompts, use load-buffer + paste-buffer
echo "$PROMPT" | tmux load-buffer -b "buf-w1" -
tmux paste-buffer -p -d -b "buf-w1" -t "$SESSION:w1"
tmux send-keys -t "$SESSION:w1" EnterWhy literal mode? Without -l, tmux interprets special characters. A prompt containing # or ! would break.
Why separate Enter? Combining the message and Enter in one send-keys call can cause race conditions with Claude Code's input buffer.
Why paste-buffer for multiline? send-keys breaks on newlines. load-buffer loads the full text into a tmux buffer, and paste-buffer injects it cleanly.
Claude Code's terminal output contains color codes, cursor movements, and OSC sequences. All regex matching (idle detection, send verification) must strip these first:
strip_ansi() {
sed -E \
-e 's/\x1b\[[0-9;:?<=>]*[a-zA-Z]//g' \
-e 's/\x1b\][^\x07\x1b]*(\x07|\x1b\\)//g' \
-e 's/\x1bP[^\x1b]*(\x1b\\|$)//g' \
-e 's/\x1b[()][0-9A-Za-z]//g' \
-e 's/[\x0e\x0f]//g'
}The heartbeat checks if Claude Code is ready for input by examining the last 12 lines of terminal output:
# Spinner = busy (check first, overrides idle patterns)
grep -qE '(Running|thinking|Searching|Reading|Writing|Editing)'
# Idle patterns
grep -qE '(❯[\s ]*$|>\s*$|waiting for input)'Workers are started with --dangerously-skip-permissions so they can operate without manual approval for every tool call. This is required for unattended operation but means workers can read, write, and execute anything in your project directory without confirmation.
Mitigations:
- Workers run in your project directory only (scoped via
cd $PROJECT_ROOT) - The
ORCHESTRATOR_WORKER_IDENV var lets hooks restrict worker behavior - Workers are instructed not to push code or end their own sessions
- The orchestrator's review gate catches issues before code reaches main
Do not run this on production systems or with access to sensitive credentials. Use on development machines with version-controlled code.
The heartbeat adjusts its polling frequency based on worker state:
| State | Interval | When |
|---|---|---|
| Stuck | 30s | Worker hasn't updated in 3+ normal intervals |
| Normal | 120s | Active workers, regular updates |
| Idle | 300s | No workers or all done |
When running multiple workers, you will hit API rate limits. Without the watchdog, a rate-limited session stops and waits for user input. Worse: if you paste the error back, Claude interprets it as "the command is broken" and uses a workaround instead of retrying.
The watchdog (rate-limit-watchdog.sh) solves this:
- Monitors all tmux panes every 15s for rate limit patterns (
429,Rate limit,overloaded) - Waits 65s for the rate limit to pass (API limits are typically 60s)
- Detects which command failed (e.g.
/commit,/whats-next) from the pane output - Sends an explicit retry message: "This was a TEMPORARY rate limit, not a bug. Run the exact same command again - no workaround."
- Backs off exponentially after repeated failures (up to 10 min cap)
The watchdog starts automatically with orch-bootstrap.sh and stops on the .stop signal.
# Manual control
./_orchestrator/rate-limit-watchdog.sh --status # Check if running
./_orchestrator/rate-limit-watchdog.sh --stop # Kill it
tail -f _orchestrator/watchdog.log # Watch live
# Disable in config.json
{ "watchdog": { "enabled": false } }Why the retry message matters: A simple "continue" causes Claude to skip the failed step or find creative alternatives. The watchdog's message explicitly says "NOT a bug, NO workaround, retry the EXACT command" - this is the key difference that makes it work.
See config.json for all settings:
| Key | Default | Purpose |
|---|---|---|
intervals.stuck_seconds |
30 | Fast polling when workers are stuck |
intervals.normal_seconds |
120 | Standard monitoring interval |
intervals.idle_seconds |
300 | Slow polling, no active workers |
thresholds.max_workers |
6 | Maximum concurrent workers |
thresholds.turns_for_tmux |
25 | When to suggest tmux over sub-agents |
rules.enforce_code_review_after_turns |
30 | Auto-remind for code review |
watchdog.enabled |
true | Auto-start watchdog with bootstrap |
watchdog.check_interval |
15 | How often to scan panes (seconds) |
watchdog.cooldown_seconds |
65 | Wait after rate limit before retry |
watchdog.max_retries |
5 | Consecutive retries before backoff |
watchdog.backoff_multiplier |
2 | Exponential backoff factor |
A worker is a full session, not a lightweight process. Each one loads the system prompt, your CLAUDE.md, every hook and the tool definitions of every MCP server before it does any work, and it pays that once per worker.
| Workers | Practical Use |
|---|---|
| 1 | Long-running single task |
| 2 | Implementation + research in parallel |
| 3 | Multi-phase project execution |
| 4+ | Test rate limits on your plan first |
Recommended: start with 2 and measure your own overhead with /context. The number depends on how much you have loaded, not on a figure in a README. Your plan's rate limits bind as well.
| Approach | Where it stops | tmux Orchestration |
|---|---|---|
| Subagents | Not attachable mid-run. Everything else (turns, hooks, MCP, own context, worktree isolation) is now configurable. Start here. | Attachable session you can take over |
| Agent Teams | Experimental, behind CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1; split-pane needs tmux or iTerm2 |
Not gated by a feature flag |
| Dynamic workflows | Deterministic fan-out, but the agents are still subagents | Full interactive sessions |
--worktree |
Git isolation, which composes with either approach | Git + session + attachability |
| Manual tabs | No monitoring, no coordination, no rules | Automated heartbeat + file state |
- Architecture: absmartly/Tmux-Orchestrator - send-keys literal mode, verify-retry pattern, hub-spoke coordination
- Idle Detection: Dicklesworthstone/ntm - 12-line capture window, spinner override, paste-buffer for multiline, ANSI strip, Double-Enter Protocol
This Gist is a self-contained starter kit. For the full system with adaptive delegation, hook matrix, review gates, and inter-worker communication:
- Deep Dive: Claude Code tmux Orchestration: Parallel AI Sessions - full architecture walkthrough with diagrams
- Video: YouTube walkthrough - see the system in action
- Free Guide: The 3-Pattern System for Claude Code - memory, delegation, and knowledge graphs
| Tool | What It Does | Deep Dive |
|---|---|---|
| Evolving Lite | Self-improving Claude Code plugin - memory, delegation, self-correction | Blog |
| Kairn | Persistent knowledge graph with context routing for AI | Blog |
| tmux Orchestration | Parallel Claude Code sessions with heartbeat monitoring | Blog |
| UPF | 3-stage planning with adversarial hardening | Blog |
| Quantum Lens | 7 cognitive lenses for multi-perspective analysis | Blog |
| PrimeLine Skills | 5 production-grade workflow skills for Claude Code | Blog |
| Starter System | Lightweight session memory and handoffs | Blog |
@PrimeLineAI · primeline.cc · Free Guide
MIT - use it, modify it, build on it. Attribution appreciated.
