Skip to content

Instantly share code, notes, and snippets.

@jimmc414
Created January 16, 2026 23:33
Show Gist options
  • Select an option

  • Save jimmc414/c45d5e54b540d5782560ec99e884142a to your computer and use it in GitHub Desktop.

Select an option

Save jimmc414/c45d5e54b540d5782560ec99e884142a to your computer and use it in GitHub Desktop.
Fork Claude agents into observable tmux panes
# Spec: tmux Agent Panes
> Fork Claude agents into observable tmux panes
## Overview
This skill enables Claude to spawn parallel agents, each working in their own tmux pane. The user can observe all agents working simultaneously in a single terminal window.
```
┌─────────────────────┬─────────────────────┬─────────────────────┐
│ Agent 1: Tests │ Agent 2: Linting │ Agent 3: Build │
│ │ │ │
│ $ pytest -v │ $ ruff check . │ $ npm run build │
│ PASSED test_foo │ Fixed 3 issues │ Building... │
│ PASSED test_bar │ All clean! │ Done! │
│ │ │ │
└─────────────────────┴─────────────────────┴─────────────────────┘
User watches all three agents work in real-time
```
## Core Concepts
### 1. Session Architecture
```
claude-agents (tmux session)
├── window 0: "orchestrator" (optional - for status)
├── window 1: "workers"
│ ├── pane 0: agent-1 (e.g., running tests)
│ ├── pane 1: agent-2 (e.g., linting)
│ └── pane 2: agent-3 (e.g., building)
```
### 2. Agent Isolation
Each agent:
- Runs in its own tmux pane
- Has independent bash context
- Can be captured independently
- Works on assigned task without interference
### 3. Orchestrator Role
The main Claude instance:
- Creates/manages the tmux session
- Spawns panes for each agent task
- Monitors progress via `capture-pane`
- Aggregates results when complete
## tmux Commands Reference
### Session Management
```bash
# Create session
tmux new-session -d -s claude-agents
# Kill session when done
tmux kill-session -t claude-agents
```
### Pane Management
```bash
# Split horizontally (side by side)
tmux split-window -h -t claude-agents
# Split vertically (stacked)
tmux split-window -v -t claude-agents
# Create 3 equal columns
tmux select-layout -t claude-agents even-horizontal
# Create grid (2x2)
tmux select-layout -t claude-agents tiled
```
### Targeting Specific Panes
```bash
# Pane addressing: session:window.pane
tmux send-keys -t claude-agents:0.0 "echo agent 1" Enter
tmux send-keys -t claude-agents:0.1 "echo agent 2" Enter
tmux send-keys -t claude-agents:0.2 "echo agent 3" Enter
# Capture specific pane output
tmux capture-pane -t claude-agents:0.0 -p
tmux capture-pane -t claude-agents:0.1 -p
```
### Pane Identification
```bash
# List all panes with IDs
tmux list-panes -t claude-agents -F "#{pane_index}: #{pane_title}"
# Set pane title (visible in status)
tmux select-pane -t claude-agents:0.0 -T "Tests"
tmux select-pane -t claude-agents:0.1 -T "Lint"
```
## Skill Invocation
### Skill Name
`/tmux-agents` or `/parallel-panes`
### Arguments
```
/tmux-agents spawn <name> "<command>" # Create new agent pane
/tmux-agents list # Show all agent panes
/tmux-agents capture <name> # Get output from pane
/tmux-agents cleanup # Kill session
```
### Example Usage
```
User: Run tests, lint, and build in parallel so I can watch
Claude: I'll spawn three agents in separate panes.
/tmux-agents spawn tests "pytest -v"
/tmux-agents spawn lint "ruff check ."
/tmux-agents spawn build "npm run build"
A window should open showing all three. Let me check their progress...
/tmux-agents capture tests
→ 15 passed, 1 failed
/tmux-agents capture lint
→ All checks passed
/tmux-agents capture build
→ Build complete
```
## Implementation
### Phase 1: Session Setup
```bash
#!/bin/bash
# tmux_agents_setup.sh
SESSION="claude-agents"
# Kill existing if any
tmux kill-session -t $SESSION 2>/dev/null
# Create new session
tmux new-session -d -s $SESSION
# Open observer window (WSL)
if [[ -d "/mnt/c" ]]; then
/mnt/c/Users/*/AppData/Local/Microsoft/WindowsApps/wt.exe \
wsl tmux attach -t $SESSION &
fi
```
### Phase 2: Spawn Agent Pane
```bash
#!/bin/bash
# tmux_agents_spawn.sh <name> <command>
SESSION="claude-agents"
NAME=$1
CMD=$2
# Count existing panes
PANE_COUNT=$(tmux list-panes -t $SESSION | wc -l)
# If more than 1 pane, split from last pane
if [ $PANE_COUNT -gt 0 ]; then
tmux split-window -h -t $SESSION
tmux select-layout -t $SESSION even-horizontal
fi
# Get the new pane index
NEW_PANE=$((PANE_COUNT))
# Set title and run command
tmux select-pane -t $SESSION:0.$NEW_PANE -T "$NAME"
tmux send-keys -t $SESSION:0.$NEW_PANE "# Agent: $NAME" Enter
tmux send-keys -t $SESSION:0.$NEW_PANE "$CMD" Enter
echo "Spawned agent '$NAME' in pane $NEW_PANE"
```
### Phase 3: Capture Output
```bash
#!/bin/bash
# tmux_agents_capture.sh <pane_index>
SESSION="claude-agents"
PANE=$1
tmux capture-pane -t $SESSION:0.$PANE -p
```
### Phase 4: Cleanup
```bash
#!/bin/bash
# tmux_agents_cleanup.sh
tmux kill-session -t claude-agents 2>/dev/null
echo "Session cleaned up"
```
## Python Implementation
```python
"""tmux_agent_panes.py - Spawn observable agent panes"""
import subprocess
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class AgentPane:
name: str
pane_index: int
command: str
class TmuxAgentManager:
SESSION = "claude-agents"
def __init__(self):
self.agents: dict[str, AgentPane] = {}
self.pane_counter = 0
def setup(self) -> str:
"""Create session and open observer window."""
# Kill existing
subprocess.run(
["tmux", "kill-session", "-t", self.SESSION],
capture_output=True
)
# Create new
subprocess.run(
["tmux", "new-session", "-d", "-s", self.SESSION]
)
# Open observer (WSL)
if os.path.exists("/mnt/c"):
wt_path = next(
(f"/mnt/c/Users/{u}/AppData/Local/Microsoft/WindowsApps/wt.exe"
for u in os.listdir("/mnt/c/Users")
if os.path.exists(f"/mnt/c/Users/{u}/AppData/Local/Microsoft/WindowsApps/wt.exe")),
None
)
if wt_path:
subprocess.Popen(
[wt_path, "wsl", "tmux", "attach", "-t", self.SESSION],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return f"Session '{self.SESSION}' created. Observer window opened."
def spawn(self, name: str, command: str) -> str:
"""Spawn a new agent in its own pane."""
if self.pane_counter > 0:
# Split to create new pane
subprocess.run([
"tmux", "split-window", "-h", "-t", self.SESSION
])
# Rebalance layout
subprocess.run([
"tmux", "select-layout", "-t", self.SESSION, "even-horizontal"
])
pane_id = f"{self.SESSION}:0.{self.pane_counter}"
# Set pane title
subprocess.run([
"tmux", "select-pane", "-t", pane_id, "-T", name
])
# Send command
subprocess.run([
"tmux", "send-keys", "-t", pane_id,
f"# === Agent: {name} ===", "Enter"
])
subprocess.run([
"tmux", "send-keys", "-t", pane_id, command, "Enter"
])
# Track agent
self.agents[name] = AgentPane(
name=name,
pane_index=self.pane_counter,
command=command
)
self.pane_counter += 1
return f"Agent '{name}' spawned in pane {self.pane_counter - 1}"
def capture(self, name: str) -> str:
"""Capture output from an agent's pane."""
if name not in self.agents:
return f"Agent '{name}' not found"
pane_id = f"{self.SESSION}:0.{self.agents[name].pane_index}"
result = subprocess.run(
["tmux", "capture-pane", "-t", pane_id, "-p"],
capture_output=True,
text=True
)
return result.stdout
def capture_all(self) -> dict[str, str]:
"""Capture output from all agents."""
return {name: self.capture(name) for name in self.agents}
def cleanup(self):
"""Kill the session."""
subprocess.run(
["tmux", "kill-session", "-t", self.SESSION],
capture_output=True
)
self.agents.clear()
self.pane_counter = 0
return "Session cleaned up"
# Singleton for skill usage
_manager: Optional[TmuxAgentManager] = None
def get_manager() -> TmuxAgentManager:
global _manager
if _manager is None:
_manager = TmuxAgentManager()
return _manager
```
## Skill File
```markdown
# /tmux-agents skill
## Description
Spawn parallel Claude agents in observable tmux panes.
## Commands
### setup
Create the tmux session and open observer window.
```
/tmux-agents setup
```
### spawn <name> "<command>"
Create a new agent pane running the given command.
```
/tmux-agents spawn tests "pytest -v tests/"
/tmux-agents spawn lint "ruff check src/"
```
### capture <name>
Get the current output from an agent's pane.
```
/tmux-agents capture tests
```
### capture-all
Get output from all agent panes.
```
/tmux-agents capture-all
```
### cleanup
Kill the session and all agent panes.
```
/tmux-agents cleanup
```
## Example Workflow
```
User: Run the full CI pipeline and let me watch
Claude: I'll set up parallel agents for each CI step.
/tmux-agents setup
→ Session created, observer window opened
/tmux-agents spawn typecheck "npx tsc --noEmit"
/tmux-agents spawn tests "pytest -v"
/tmux-agents spawn lint "ruff check ."
/tmux-agents spawn build "npm run build"
You should see 4 panes now. I'll monitor their progress...
[waits, then captures]
/tmux-agents capture-all
→ typecheck: No errors
→ tests: 42 passed
→ lint: All checks passed
→ build: Build successful
All CI steps complete! Cleaning up...
/tmux-agents cleanup
```
## Layout Options
For many agents, use tiled layout:
```bash
tmux select-layout -t claude-agents tiled
```
Layout options:
- `even-horizontal` - side by side columns
- `even-vertical` - stacked rows
- `tiled` - grid pattern
- `main-horizontal` - one big, rest below
- `main-vertical` - one big, rest to the right
## Integration with Claude's Task Tool
### The Problem: Invisible Delegation
When Claude uses the Task tool today, sub-agents are invisible:
```
You ←→ Claude (main) ──Task──→ [Sub-agent works invisibly] ──→ Result
└── You see NOTHING here
It's a black box
```
The sub-agent runs, uses tools (Bash, Read, Write), and returns results. But the user has **zero visibility** into what it's actually doing.
### The Vision: Observable Delegation
```
You ←→ Claude (main) ──Task──→ Sub-agent in Pane 1 ──→ Result
──Task──→ Sub-agent in Pane 2 ──→ Result
──Task──→ Sub-agent in Pane 3 ──→ Result
└── You WATCH all of them work
```
### The Challenge
**Task agents don't run in a terminal.** They make API calls.
When a Task agent uses the Bash tool, it's not typing into a shell you can see - it's making an API request that executes a command and returns output.
```
Task Agent Reality
─────────── ───────
"I'll run pytest" API call to Bash tool
subprocess.run(["pytest"])
Returns stdout to agent
User sees: nothing
```
### Two Types of Delegation
| Invisible (current) | Visible (proposed) |
|--------------------|-------------------|
| "Go figure it out" | "Work while I watch" |
| Trust the agent | Verify the agent |
| Results matter | Process matters |
| Background work | Collaborative work |
| Fire and forget | Observe and intervene |
Both are valid use cases. Sometimes you want to delegate and get results. Sometimes you want to watch and potentially intervene.
### Architecture Options
#### Option 1: Mirror/Log Approach (Easiest)
```
Task Agent runs normally (invisible)
└──→ We intercept tool calls
└──→ Mirror to tmux pane (display only)
└──→ User sees what agent is doing
(read-only observation)
```
The agent still works invisibly via API, but we **log** its actions to a visible pane. User watches a stream of what's happening.
**Pros:**
- Doesn't change how Task tool works
- Easy to implement
- No performance impact on agent
**Cons:**
- Not truly "live" - slight delay
- User can't intervene
- Read-only observation
**Implementation:**
```python
class TaskLogger:
"""Logs Task agent activity to a tmux pane."""
def __init__(self, pane_manager: TmuxAgentManager):
self.pane_manager = pane_manager
self.pane_id = None
def start_task(self, name: str, prompt: str):
"""Called when Task tool is invoked."""
self.pane_id = self.pane_manager.spawn(name, "# Task agent starting...")
self._log(f"Task: {name}")
self._log(f"Prompt: {prompt[:80]}...")
self._log(f"Started: {datetime.now()}")
self._log("")
def log_tool_call(self, tool: str, params: dict):
"""Called when agent uses a tool."""
self._log(f"[{tool}] {self._summarize(params)}")
def log_tool_result(self, tool: str, result: str):
"""Called when tool returns."""
self._log(f" → {result[:100]}...")
def end_task(self, result: str):
"""Called when Task completes."""
self._log("")
self._log("=== Task Complete ===")
self._log(f"Result: {result[:200]}...")
def _log(self, msg: str):
subprocess.run([
"tmux", "send-keys", "-t", self.pane_id,
f"echo '{msg}'", "Enter"
])
```
#### Option 2: Redirect Bash Through tmux (Medium Difficulty)
```
Task Agent wants to run "pytest"
└──→ Instead of subprocess.run()
└──→ tmux send-keys "pytest"
└──→ User sees command run live
└──→ tmux capture-pane gets output
└──→ Returns to agent
```
The agent's Bash commands actually execute in the visible tmux pane. Real terminal, real visibility.
**Pros:**
- True live observation
- Real terminal environment
- User sees exactly what happens
**Cons:**
- Requires modifying Bash tool behavior
- Slightly slower (tmux overhead)
- More complex error handling
**Implementation:**
```python
class ObservableBashTool:
"""Bash tool that executes in visible tmux pane."""
def __init__(self, pane_id: str):
self.pane_id = pane_id
self.marker = "___CMD_DONE___"
def execute(self, command: str, timeout: int = 120) -> str:
"""Execute command in tmux pane and capture output."""
# Clear and send command with end marker
full_cmd = f"{command}; echo '{self.marker}'"
subprocess.run([
"tmux", "send-keys", "-t", self.pane_id,
full_cmd, "Enter"
])
# Poll for completion
start = time.time()
while time.time() - start < timeout:
output = subprocess.run(
["tmux", "capture-pane", "-t", self.pane_id, "-p"],
capture_output=True, text=True
).stdout
if self.marker in output:
# Extract output between command and marker
return self._extract_output(output, command)
time.sleep(0.5)
raise TimeoutError(f"Command timed out: {command}")
```
#### Option 3: Custom Agents via Agent SDK (Most Control)
Build agents from scratch using Claude Agent SDK, designed to be observable from the ground up:
```python
from claude_code_sdk import Agent, Tool
import asyncio
class ObservableAgent:
"""An agent that does all work in a visible tmux pane."""
def __init__(self, name: str, pane_manager: TmuxAgentManager):
self.name = name
self.pane_manager = pane_manager
self.pane_id = None
async def run(self, prompt: str) -> str:
"""Run agent with all work visible in tmux pane."""
# Create dedicated pane
self.pane_id = self.pane_manager.spawn(
self.name,
f"echo '=== Agent: {self.name} ==='"
)
# Create agent with custom Bash tool
agent = Agent(
model="claude-sonnet-4-20250514",
tools=[
self._observable_bash_tool(),
self._observable_read_tool(),
self._observable_write_tool(),
]
)
# Run and return result
result = await agent.run(prompt)
self._log("=== Agent Complete ===")
return result
def _observable_bash_tool(self) -> Tool:
"""Bash tool that executes in our pane."""
def execute(command: str) -> str:
self._send(command)
return self._capture_after(command)
return Tool(
name="bash",
description="Execute shell commands",
function=execute
)
def _send(self, cmd: str):
subprocess.run([
"tmux", "send-keys", "-t", self.pane_id,
cmd, "Enter"
])
def _capture_after(self, cmd: str, timeout: int = 60) -> str:
# Wait for prompt to return, then capture
time.sleep(1) # Simple approach
return subprocess.run(
["tmux", "capture-pane", "-t", self.pane_id, "-p"],
capture_output=True, text=True
).stdout
```
#### Option 4: Hybrid Workers (Pragmatic)
For tasks that are primarily terminal work (tests, builds, linting), skip the Task tool entirely and just use tmux panes with shell commands:
```python
async def parallel_ci_pipeline(pane_manager: TmuxAgentManager):
"""Run CI steps as parallel shell jobs, not AI agents."""
pane_manager.setup()
# These are just shell commands, not AI agents
jobs = [
("tests", "pytest -v && echo DONE"),
("lint", "ruff check . && echo DONE"),
("typecheck", "mypy . && echo DONE"),
("build", "npm run build && echo DONE"),
]
for name, cmd in jobs:
pane_manager.spawn(name, cmd)
# Monitor completion
while True:
outputs = pane_manager.capture_all()
if all("DONE" in out for out in outputs.values()):
break
await asyncio.sleep(2)
return outputs
```
**When to use this:**
- Tasks are well-defined shell commands
- No AI reasoning needed during execution
- Just need parallelism and visibility
### Implementation Phases
#### Phase 1: Task Lifecycle Logging (MVP)
Minimal visibility - just show when tasks start/end:
```python
def observable_task(name: str, prompt: str, pane_manager):
"""Wrap Task tool with basic visibility."""
# Create pane showing task info
pane_id = pane_manager.spawn(name, "# Task starting...")
pane_manager.send(pane_id, f"echo 'Task: {name}'")
pane_manager.send(pane_id, f"echo 'Started: $(date)'")
pane_manager.send(pane_id, f"echo 'Status: Running...'")
# Run actual task (invisible)
result = Task(prompt) # Standard Task tool
# Show completion
pane_manager.send(pane_id, f"echo ''")
pane_manager.send(pane_id, f"echo 'Status: Complete'")
pane_manager.send(pane_id, f"echo 'Finished: $(date)'")
return result
```
**User sees:**
```
┌─────────────────────┬─────────────────────┐
│ Task: research │ Task: implement │
│ Started: 15:30:00 │ Started: 15:30:00 │
│ Status: Running... │ Status: Complete │
│ │ Finished: 15:32:15 │
└─────────────────────┴─────────────────────┘
```
#### Phase 2: Tool Call Streaming
Show what tools the agent is using:
```python
def observable_task_v2(name: str, prompt: str, pane_manager):
"""Task with tool call visibility."""
pane_id = pane_manager.spawn(name, "# Task starting...")
# Hook into tool calls (requires Task tool modification)
def on_tool_call(tool: str, params: dict):
summary = f"[{tool}] {summarize(params)}"
pane_manager.send(pane_id, f"echo '{summary}'")
def on_tool_result(tool: str, result: str):
pane_manager.send(pane_id, f"echo ' → {result[:60]}...'")
result = Task(
prompt,
on_tool_call=on_tool_call,
on_tool_result=on_tool_result
)
return result
```
**User sees:**
```
┌─────────────────────────────────────────────────────────────┐
│ Task: research-auth │
│ Started: 15:30:00 │
│ │
│ [Grep] pattern="auth" path="src/" │
│ → Found 12 matches │
│ [Read] file="src/auth/login.py" │
│ → Read 245 lines │
│ [Read] file="src/auth/session.py" │
│ → Read 189 lines │
│ [Bash] command="grep -r 'JWT' ." │
│ → Found 3 matches in config │
│ │
│ Status: Running... │
└─────────────────────────────────────────────────────────────┘
```
#### Phase 3: Bash Redirection
Agent's Bash commands execute in visible pane:
```python
def observable_task_v3(name: str, prompt: str, pane_manager):
"""Task with Bash commands visible in pane."""
pane_id = pane_manager.spawn(name, "# Task starting...")
# Custom Bash tool that uses tmux
observable_bash = ObservableBashTool(pane_id)
result = Task(
prompt,
tool_overrides={
"bash": observable_bash.execute
}
)
return result
```
**User sees:**
```
┌─────────────────────────────────────────────────────────────┐
│ # Task: run-tests │
│ $ pytest -v tests/ │
│ ========================= test session starts ==============│
│ collected 42 items │
│ │
│ tests/test_auth.py::test_login PASSED │
│ tests/test_auth.py::test_logout PASSED │
│ tests/test_api.py::test_endpoint PASSED │
│ ... │
│ │
│ ========================= 42 passed in 3.21s ===============│
│ $ │
└─────────────────────────────────────────────────────────────┘
```
#### Phase 4: Full Agent SDK Integration
Custom agents built for observability:
```python
async def run_observable_agents(tasks: list[dict], pane_manager):
"""Run multiple observable agents in parallel."""
pane_manager.setup()
async def run_one(task: dict):
agent = ObservableAgent(task["name"], pane_manager)
return await agent.run(task["prompt"])
# Run all in parallel
results = await asyncio.gather(*[
run_one(task) for task in tasks
])
return dict(zip([t["name"] for t in tasks], results))
# Usage
results = await run_observable_agents([
{"name": "research", "prompt": "Find all auth-related code"},
{"name": "tests", "prompt": "Run and fix failing tests"},
{"name": "docs", "prompt": "Update README with new features"},
], pane_manager)
```
### Example: Full Observable Workflow
```
User: Refactor the auth module, run tests, and update docs - let me watch
Claude: I'll spawn three observable agents.
/tmux-agents setup
→ Session created, observer window opened
# User now sees:
┌─────────────────────┬─────────────────────┬─────────────────────┐
│ Agent: Refactor │ Agent: Tests │ Agent: Docs │
│ │ │ │
│ [Read] auth.py │ $ pytest -v │ [Read] README.md │
│ → 245 lines │ test_login PASSED │ → 120 lines │
│ [Grep] "session" │ test_logout PASSED │ [Edit] README.md │
│ → 12 matches │ test_token PASSED │ → Updated section │
│ [Edit] auth.py │ ... │ [Write] CHANGELOG │
│ → Refactored │ 12 passed │ → Added entry │
│ │ │ │
│ Status: Complete │ Status: Complete │ Status: Complete │
└─────────────────────┴─────────────────────┴─────────────────────┘
Claude: All three agents have completed:
- Refactor: Simplified auth flow, removed 45 lines of duplicate code
- Tests: All 12 tests passing
- Docs: README and CHANGELOG updated
Cleaning up...
/tmux-agents cleanup
```
### Inter-Agent Communication
For complex workflows where agents need to coordinate:
```python
class SharedState:
"""File-based shared state for agent coordination."""
STATE_FILE = "/tmp/claude-agents-state.json"
@classmethod
def read(cls) -> dict:
if os.path.exists(cls.STATE_FILE):
return json.loads(Path(cls.STATE_FILE).read_text())
return {}
@classmethod
def write(cls, key: str, value: any):
state = cls.read()
state[key] = value
Path(cls.STATE_FILE).write_text(json.dumps(state, indent=2))
@classmethod
def wait_for(cls, key: str, timeout: int = 300) -> any:
"""Block until key appears in state."""
start = time.time()
while time.time() - start < timeout:
state = cls.read()
if key in state:
return state[key]
time.sleep(1)
raise TimeoutError(f"Timed out waiting for {key}")
# Agent 1: Research
SharedState.write("auth_files", ["src/auth.py", "src/session.py"])
# Agent 2: Waits for research results
auth_files = SharedState.wait_for("auth_files")
for f in auth_files:
# Process files...
```
## User Interaction with Agent Panes
A key advantage of the tmux approach: **the user can interact with any pane at any time**. This isn't a black box - users can observe, intervene, or take over.
### Switching Between Panes
| Keys | Action |
|------|--------|
| `Ctrl-b ←` | Move to left pane |
| `Ctrl-b →` | Move to right pane |
| `Ctrl-b ↑` | Move to pane above |
| `Ctrl-b ↓` | Move to pane below |
| `Ctrl-b o` | Cycle to next pane |
| `Ctrl-b q` | Show pane numbers (then press 0-9 to jump) |
### Pane Management
| Keys | Action |
|------|--------|
| `Ctrl-b z` | Zoom current pane (fullscreen toggle) |
| `Ctrl-b x` | Kill current pane (with confirmation) |
| `Ctrl-b !` | Convert pane to new window |
| `Ctrl-b {` | Swap pane left |
| `Ctrl-b }` | Swap pane right |
### Scrolling and Copy Mode
| Keys | Action |
|------|--------|
| `Ctrl-b [` | Enter scroll/copy mode |
| `q` | Exit scroll mode |
| `↑ ↓` | Scroll line by line (in scroll mode) |
| `PgUp PgDn` | Scroll page by page (in scroll mode) |
| `/` | Search forward (in scroll mode) |
| `?` | Search backward (in scroll mode) |
### Session Management
| Keys | Action |
|------|--------|
| `Ctrl-b d` | Detach from session (keeps it running) |
| `Ctrl-b $` | Rename session |
| `Ctrl-b s` | List/switch sessions |
### Reattaching After Detach
```bash
# List sessions
tmux list-sessions
# Reattach to claude-agents session
tmux attach -t claude-agents
```
### User Intervention Scenarios
#### Scenario 1: Agent is stuck, user takes over
```
1. User sees agent in pane 1 is stuck on a test
2. User presses Ctrl-b → to switch to pane 1
3. User types: Ctrl-C to cancel stuck command
4. User manually runs: pytest -x tests/specific_test.py
5. Claude captures result and continues orchestration
```
#### Scenario 2: User wants to inspect something
```
1. User sees interesting output in pane 2
2. User presses Ctrl-b q, then 2 to jump to pane 2
3. User presses Ctrl-b z to zoom (fullscreen)
4. User presses Ctrl-b [ to scroll up and read history
5. User presses q to exit scroll mode
6. User presses Ctrl-b z to unzoom
```
#### Scenario 3: User adds a helper command
```
1. User switches to pane 0
2. User types: export DEBUG=1
3. All subsequent commands in that pane have DEBUG set
4. Agent continues work with user's environment modification
```
### The Collaboration Model
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ CLAUDE USER │
│ │ │ │
│ │ tmux send-keys ────────────────► │ Watches in real-time │
│ │ │ │
│ │ │ Can switch panes │
│ │ │ Can type commands │
│ │ │ Can Ctrl-C to cancel │
│ │ │ Can scroll history │
│ │ │ │
│ │◄──────────────── tmux capture-pane │
│ │ Sees user's changes │ │
│ │ │ │
│ │ Both working in same terminal │ │
│ │ True collaboration │ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
This is fundamentally different from invisible Task agents. The user isn't just watching - they're a **co-pilot** who can intervene at any moment.
## Advanced tmux Features for Agentic Workflows
These advanced tmux capabilities unlock sophisticated agent coordination patterns for Phase 3 and Phase 4 implementations.
### Output Streaming & Capture
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Pipe-pane** | `tmux pipe-pane -o 'cat >> file.log'` | Stream agent output to file in real-time without polling |
| **Capture full history** | `tmux capture-pane -S -` | Retrieve complete agent session history for analysis |
| **Capture with timestamps** | `tmux capture-pane -p -e` | Capture with escape sequences for colored output preservation |
| **Save buffer to file** | `tmux save-buffer /tmp/out.txt` | Export captured content for external processing |
### Synchronization & Coordination
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Wait-for signal** | `tmux wait-for channel-name` | Block agent until another agent signals completion |
| **Send signal** | `tmux wait-for -S channel-name` | Signal other agents that a prerequisite task is done |
| **Lock/unlock** | `tmux wait-for -L/-U lock-name` | Mutex for shared resources between agents |
| **Synchronized panes** | `setw synchronize-panes on` | Send same command to all agents simultaneously (setup, env vars) |
### Event Hooks
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Pane exited** | `set-hook pane-exited 'run "script"'` | Detect when agent command finishes and trigger next step |
| **Pane focus** | `set-hook pane-focus-in 'run "script"'` | Detect when user switches to a pane (pause agent?) |
| **Window renamed** | `set-hook window-renamed 'run "script"'` | Track agent status changes via window names |
| **Session created** | `set-hook session-created 'run "script"'` | Initialize environment when orchestrator starts |
| **Client attached** | `set-hook client-attached 'run "script"'` | Notify agents when user starts observing |
| **Alert hooks** | `set-hook alert-activity 'run "script"'` | React when monitored pane has new output |
### Monitoring & Alerts
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Monitor activity** | `setw monitor-activity on` | Alert orchestrator when idle agent produces output |
| **Monitor silence** | `setw monitor-silence 30` | Detect stuck agents (no output for N seconds) |
| **Monitor bell** | `setw monitor-bell on` | Agent can ring bell to signal completion/error |
| **Activity action** | `set activity-action other` | Configure how alerts propagate between windows |
### Pane Control
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Respawn pane** | `tmux respawn-pane -k "cmd"` | Restart failed agent with new command without losing pane |
| **Kill pane** | `tmux kill-pane -t target` | Terminate runaway agent |
| **Resize pane** | `tmux resize-pane -t target -x 80` | Adjust agent pane size based on workload |
| **Swap panes** | `tmux swap-pane -s src -t dst` | Reorder agents based on priority |
| **Break pane** | `tmux break-pane -t target` | Promote agent pane to its own window |
| **Join pane** | `tmux join-pane -s src -t dst` | Merge agent into another window |
### Display & UI
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Display popup** | `tmux display-popup -w 80 "cmd"` | Show floating status overlay without disturbing panes |
| **Display message** | `tmux display-message "text"` | Flash notifications to user about agent status |
| **Select pane title** | `tmux select-pane -T "title"` | Update pane title to show agent status/progress |
| **Status line** | `set status-right "#(script)"` | Dynamic status bar showing agent metrics |
| **Pane border status** | `set pane-border-status top` | Show pane titles in borders for agent identification |
| **Pane border format** | `set pane-border-format "#{pane_title}"` | Customize what shows in pane borders |
### Layouts & Windows
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Select layout** | `tmux select-layout tiled` | Auto-arrange agent panes (tiled, even-horizontal, etc.) |
| **Custom layout** | `tmux select-layout "checksum,spec"` | Restore exact pane arrangement for reproducibility |
| **New window** | `tmux new-window -n "name"` | Group related agents in separate windows |
| **Link window** | `tmux link-window -s src -t dst` | Share agent window between sessions |
| **Move window** | `tmux move-window -t target` | Reorganize agent windows |
### Control Mode & Scripting
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Control mode** | `tmux -C attach` | Machine-readable event stream for programmatic control |
| **Command sequences** | `tmux source-file script.tmux` | Execute complex multi-command setup from file |
| **If-shell** | `tmux if-shell "test" "cmd1" "cmd2"` | Conditional execution based on environment |
| **Run-shell** | `tmux run-shell "script"` | Execute external scripts with tmux context |
| **Display menu** | `tmux display-menu -T "title" ...` | Interactive menus for user to select agent actions |
### Session & Client Management
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Session groups** | `tmux new-session -t existing` | Multiple users observe same agents with different views |
| **Detach other clients** | `tmux detach-client -a` | Ensure single observer for critical operations |
| **Switch client** | `tmux switch-client -t session` | Move orchestrator between agent sessions |
| **List clients** | `tmux list-clients` | Track who is observing agent work |
| **Refresh client** | `tmux refresh-client` | Force UI update after layout changes |
### Environment & Context
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Set environment** | `tmux setenv VAR value` | Share configuration across all agent panes |
| **Show environment** | `tmux showenv` | Debug agent environment issues |
| **Update environment** | `tmux setenv -g VAR value` | Global config change affects all agents |
| **Pane environment** | Available in formats | Query individual agent pane environment |
### Buffer & Clipboard
| Feature | Command | Agentic Use Case |
|---------|---------|------------------|
| **Set buffer** | `tmux set-buffer "text"` | Share data between orchestrator and agents |
| **Get buffer** | `tmux show-buffer` | Retrieve shared data |
| **Paste buffer** | `tmux paste-buffer -t target` | Send data to specific agent pane |
| **Multiple buffers** | `tmux set-buffer -b name "text"` | Named buffers for different data channels |
| **Load buffer** | `tmux load-buffer file.txt` | Import external data for agents |
### Formats & Variables
| Feature | Example | Agentic Use Case |
|---------|---------|------------------|
| **Pane PID** | `#{pane_pid}` | Track agent process for external monitoring |
| **Pane current command** | `#{pane_current_command}` | See what command agent is running |
| **Pane dead status** | `#{pane_dead}` | Detect crashed agents |
| **Pane last output** | `#{pane_search_string}` | Pattern matching on agent output |
| **Window activity flag** | `#{window_activity_flag}` | Check which agents have new output |
| **Session alerts** | `#{session_alerts}` | Count of agents needing attention |
### Advanced Patterns
#### Pattern: Agent Completion Detection
```bash
# Agent signals completion by exiting with specific code
tmux set-hook -t claude-agents pane-exited 'run-shell "echo $TMUX_PANE exited >> /tmp/completions.log"'
```
#### Pattern: Agent Output Streaming
```bash
# Stream all agent output to central log
tmux pipe-pane -t claude-agents:0.0 -o 'cat >> /tmp/agent-0.log'
tmux pipe-pane -t claude-agents:0.1 -o 'cat >> /tmp/agent-1.log'
tmux pipe-pane -t claude-agents:0.2 -o 'cat >> /tmp/agent-2.log'
```
#### Pattern: Agent Synchronization
```bash
# Agent 1: Research (signals when done)
tmux send-keys -t :0.0 'research_cmd && tmux wait-for -S research-done' Enter
# Agent 2: Waits for research, then implements
tmux send-keys -t :0.1 'tmux wait-for research-done && implement_cmd' Enter
```
#### Pattern: Stuck Agent Detection
```bash
# Alert if pane silent for 60 seconds
tmux setw -t claude-agents monitor-silence 60
tmux set-hook alert-silence 'run-shell "echo STUCK >> /tmp/alerts.log"'
```
#### Pattern: User Takeover Detection
```bash
# Detect when user focuses a pane
tmux set-hook pane-focus-in 'run-shell "echo USER_FOCUS $TMUX_PANE >> /tmp/events.log"'
# Orchestrator can pause sending commands to that pane
```
#### Pattern: Dynamic Status Display
```bash
# Show agent status in pane borders
tmux set pane-border-status top
tmux set pane-border-format '#{pane_index}: #{pane_title} | #{pane_current_command}'
# Update titles as agents progress
tmux select-pane -t :0.0 -T "Agent 1: RUNNING tests"
tmux select-pane -t :0.1 -T "Agent 2: COMPLETE"
```
## Future Enhancements
1. **Named windows** - Group related agents in windows
2. **Auto-scaling layout** - Adjust pane sizes based on content
3. **Progress indicators** - Show completion % in pane titles
4. **Log persistence** - Save agent output to files
5. **Inter-agent communication** - Shared state via files/redis
6. **User input detection** - Claude detects when user types and pauses
7. **Handoff protocol** - Formal way for user to take over a pane
8. **Pane annotations** - Floating labels showing agent status
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment