Tmux control
Agent skills collection: session management, TypeScript patterns, Chrome CDP, tmux, Apple integrations
npx -y skills add eins78/agent-skills --skill tmux-controlAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when sending commands to tmux panes, reading pane output, creating windows/panes, or monitoring tmux sessions. Covers reliable targeting, synchronization, and output capture patterns.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
12.8 KB, as published. Nobody here has run it
tmux Control Patterns
Reliable patterns for programmatic tmux interaction. The #1 source of bugs is targeting — follow these rules strictly.
Golden Rules
- Use unique IDs for targeting:
%N(pane),@N(window),$NAME(session) — never bare indexes - Use
wait-forinstead ofsleepfor synchronization - Pass commands to
new-window/split-windowdirectly — avoid send-keys to freshly created panes (race condition) - Always use
-d(detached) withnew-windowandsplit-window— never steal focus from the user's active window - Verify targets exist before sending — use
list-panes -ato check panes,list-windowsfor windows,has-sessionfor sessions - Use full binary paths in commands sent to panes — the pane's environment may differ from yours
Targeting
The format is SESSION:WINDOW.PANE. All three parts are optional but be explicit.
Discover targets first — always
# List all panes with their IDs and human-readable positions
tmux list-panes -a -F '#{pane_id} #{session_name}:#{window_index}.#{pane_index} #{pane_current_command}'
# List sessions
tmux list-sessions -F '#{session_id} #{session_name}'
# List windows in current session
tmux list-windows -F '#{window_id} #{window_index} #{window_name}'
Targeting syntax
| Target | Meaning | Reliability |
|---|---|---|
%42 | Pane ID (unique, stable) | Best |
@3 | Window ID (unique, stable) | Good |
$main | Session by name | Good |
main:2.0 | Session:Window.Pane by index | Fragile — indexes shift |
:2 | Window 2 in current session | OK for interactive use |
-t 2 | Ambiguous (session? window?) | Avoid |
Capture pane ID at creation
# Best pattern: capture the ID when you create the pane
# Pass the command directly to avoid the send-keys race condition
PANE_ID=$(tmux split-window -d -P -F '#{pane_id}' 'echo hello; exec bash')
# or
PANE_ID=$(tmux new-window -d -P -F '#{pane_id}' 'echo hello; exec bash')
Creating Panes and Windows
Preferred: pass command directly (avoids race condition)
# Command runs as soon as shell is ready — no race (-d = detached, don't steal focus)
tmux new-window -d 'echo "hello from new window"; exec bash'
tmux split-window -d 'npm run dev; exec bash'
# Capture the ID too
PANE_ID=$(tmux new-window -d -P -F '#{pane_id}' 'npm test; exec bash')
Why send-keys to new panes is fragile
send-keys fires immediately, but the shell in a new pane may not be initialized yet (especially with heavy shell configs like oh-my-zsh). The command arrives before the prompt is ready and gets lost.
If you must use send-keys on a new pane, add a brief delay:
PANE_ID=$(tmux split-window -d -P -F '#{pane_id}')
sleep 0.5 # let shell initialize — fragile but sometimes necessary
tmux send-keys -t "$PANE_ID" 'your command' Enter
Sending Commands
Basic pattern
tmux send-keys -t "$TARGET" 'your command here' Enter
Quoting rules
- Outer quotes: use single quotes around the command to prevent local shell expansion
- Inner quotes: if the command itself needs quotes, use double quotes inside singles, or escape
- Pipes and redirects: work fine inside quotes
# Simple
tmux send-keys -t "$PANE" 'ls -la /tmp' Enter
# With pipes (single-quoted, so pipe is literal)
tmux send-keys -t "$PANE" 'ps aux | grep node' Enter
# With inner quotes
tmux send-keys -t "$PANE" 'echo "hello world" > /tmp/out.txt' Enter
# Complex: write to temp file and source it
echo 'complex command "with" pipes | and stuff' > /tmp/tmux-cmd.sh
tmux send-keys -t "$PANE" 'bash /tmp/tmux-cmd.sh' Enter
Nested Claude Code sessions
Running claude inside an existing Claude Code session fails due to a guard variable. Workaround:
tmux send-keys -t "$PANE" 'env -u CLAUDECODE claude -p "your prompt"' Enter
Reading Output
capture-pane (visible buffer)
# Last screenful
tmux capture-pane -t "$PANE" -p
# Last N lines
tmux capture-pane -t "$PANE" -p -S -20
# Full scrollback, joined wrapped lines
tmux capture-pane -t "$PANE" -p -S - -J
# With ANSI colors preserved
tmux capture-pane -t "$PANE" -p -e
File-based pattern (for long output)
When output exceeds scrollback or you need the complete result:
# Use a unique channel to avoid collisions with concurrent commands
CHAN="cmd-done-$$-$RANDOM"
tmux send-keys -t "$PANE" "your-command > /tmp/result.out 2>&1; tmux wait-for -S $CHAN" Enter
tmux wait-for "$CHAN"
# Read the complete output
cat /tmp/result.out
Prompt detection (is the pane idle?)
tmux capture-pane -t "$PANE" -p | tail -1 | grep -qE '(\$|>|#|%)\s*$' && echo "idle" || echo "busy"
Caution: capture-pane returns empty if the pane hasn't rendered yet. Don't check immediately after creating a pane.
Synchronization with wait-for
wait-for provides channel-based synchronization — far more reliable than sleep.
Pattern: run command and wait for completion
# Generate a unique channel name
CHAN="done-$$-$RANDOM"
# Send command with signal on completion
tmux send-keys -t "$PANE" "your-command; tmux wait-for -S $CHAN" Enter
# Block until command completes
tmux wait-for "$CHAN"
# Now safely capture output or proceed
tmux capture-pane -t "$PANE" -p -S -20
Pattern: timeout with wait-for
CHAN="done-$$-$RANDOM"
tmux send-keys -t "$PANE" "long-command; tmux wait-for -S $CHAN" Enter
# Use `timeout` utility — exit 0 = command completed, exit 124 = timed out
if timeout 30 tmux wait-for "$CHAN"; then
echo "Command completed"
else
echo "Command timed out"
fi
Monitoring
pipe-pane: stream output to file
# Start logging pane output to a file
tmux pipe-pane -t "$PANE" -o 'cat >> /tmp/pane-log.txt'
# Stop logging
tmux pipe-pane -t "$PANE"
# Stream through a filter (e.g., watch for errors)
tmux pipe-pane -t "$PANE" -o 'grep --line-buffered ERROR >> /tmp/errors.txt'
Note: only one pipe per pane. Setting a new pipe replaces the old one.
monitor-silence: detect idle pane
# Alert after 10 seconds of no output (window-level option)
tmux set-option -t "$WINDOW" monitor-silence 10
# Check if silence alert triggered
tmux display-message -t "$WINDOW" -p '#{window_silence_flag}'
monitor-activity: detect new output
# Flag when any output appears in a background window
tmux set-option -t "$WINDOW" monitor-activity on
macOS-Specific
Full Disk Access (FDA)
tmux sessions started from a GUI terminal (Terminal.app, iTerm2) inherit FDA. Sessions started over SSH do not. This matters for accessing ~/Documents/, ~/Desktop/, and NFS mounts.
Rule: for file system operations requiring FDA, send commands to a tmux pane that was created from a GUI terminal — never to an SSH-initiated session.
PATH and environment
The pane's shell environment may differ from yours — don't assume your aliases, functions, or PATH are available:
- Don't rely on aliases — use full commands (e.g.,
/opt/homebrew/bin/tmuxnottmux) - PATH may differ — use absolute paths for non-standard binaries
- Shell functions may not exist — write scripts to temp files if needed
Recipes
Run command and get output
# 1. Discover target
PANE=$(tmux list-panes -F '#{pane_id}' | head -1)
# 2. Run with wait-for
CHAN="cmd-$$"
tmux send-keys -t "$PANE" "whoami; tmux wait-for -S $CHAN" Enter
tmux wait-for "$CHAN"
# 3. Capture result
tmux capture-pane -t "$PANE" -p -S -5
Start long process and check later
# 1. Create dedicated window
PANE=$(tmux new-window -d -P -F '#{pane_id}' -n 'build')
# 2. Send the long-running command (with completion marker)
tmux send-keys -t "$PANE" 'npm run build > /tmp/build.out 2>&1; echo "BUILD_DONE" >> /tmp/build.out' Enter
# 3. Later: check if done
grep -q "BUILD_DONE" /tmp/build.out 2>/dev/null && echo "finished" || echo "still running"
# 4. Read full output
cat /tmp/build.out
Monitor pane for completion
# 1. Start logging
tmux pipe-pane -t "$PANE" -o 'cat >> /tmp/pane-watch.txt'
# 2. Send command
tmux send-keys -t "$PANE" 'make all' Enter
# 3. Poll for completion marker (or use wait-for instead)
while ! grep -q '\$' <(tail -1 /tmp/pane-watch.txt 2>/dev/null); do
sleep 2
done
echo "Command completed"
# 4. Clean up
tmux pipe-pane -t "$PANE"
Helper Scripts
Two helper scripts in ${CLAUDE_SKILL_DIR}/scripts/ wrap common patterns into single commands. Always use the full ${CLAUDE_SKILL_DIR}/scripts/ path — these scripts are bundled with this skill, not in the project being worked on.
tmux-run.sh — run command and get output
# Send command, wait for completion, return output
${CLAUDE_SKILL_DIR}/scripts/tmux-run.sh -t %42 'npm test'
# With timeout (default: 120s)
${CLAUDE_SKILL_DIR}/scripts/tmux-run.sh -t %42 -T 60 'make build'
# Capture output in a variable
output=$(${CLAUDE_SKILL_DIR}/scripts/tmux-run.sh -t %42 -q 'git status')
# Exit code is forwarded from the remote command
${CLAUDE_SKILL_DIR}/scripts/tmux-run.sh -t %42 'npm test' || echo "tests failed"
- stdout: command output only
- stderr: status messages (suppress with
-q) - Exit code: mirrors remote command (128 = timeout)
- Requires pane ID (
%Nformat)
tmux-watch.sh — wait for pattern in pane output
# Wait for a build to finish
${CLAUDE_SKILL_DIR}/scripts/tmux-watch.sh -t %42 'BUILD_DONE'
# With timeout and poll interval
${CLAUDE_SKILL_DIR}/scripts/tmux-watch.sh -t %42 -T 300 -i 5 'Tests:.*passed'
# Wait for shell prompt (agent finished)
${CLAUDE_SKILL_DIR}/scripts/tmux-watch.sh -t %42 '\$\s*$'
- Pattern is extended regex (grep -E)
- stdout: matching line(s)
- Exit 0 = found, 1 = timeout, 2 = pane not found
- Default: 300s timeout, 2s poll interval
Multi-Agent Patterns
Patterns for running multiple Claude Code instances (or any agents) in parallel tmux panes.
Spawn parallel agents
# Create a multi-pane layout
# -d on new-window prevents stealing focus from the user
PANE1=$(tmux new-window -d -n 'agents' -P -F '#{pane_id}')
PANE2=$(tmux split-window -d -h -t "$PANE1" -P -F '#{pane_id}')
PANE3=$(tmux split-window -d -v -t "$PANE2" -P -F '#{pane_id}')
# Launch agents in each pane
# env -u CLAUDECODE allows nested Claude instances
for PANE in $PANE1 $PANE2 $PANE3; do
tmux send-keys -t "$PANE" \
"env -u CLAUDECODE claude -p 'your task...' --output-format json > /tmp/agent-${PANE}.json 2>&1; echo DONE > /tmp/agent-${PANE}.signal" \
Enter
done
# Wait for all agents to complete
for PANE in $PANE1 $PANE2 $PANE3; do
while [ ! -f "/tmp/agent-${PANE}.signal" ]; do sleep 5; done
done
# Collect results
for PANE in $PANE1 $PANE2 $PANE3; do
echo "=== Agent $PANE ==="
jq -r '.result' "/tmp/agent-${PANE}.json"
done
Monitor agents with tmux-watch.sh
# Watch each pane for shell prompt (agent finished)
for PANE in $PANE1 $PANE2 $PANE3; do
${CLAUDE_SKILL_DIR}/scripts/tmux-watch.sh -t "$PANE" -T 600 -q '\$\s*$' &
done
wait # blocks until all watchers return
echo "All agents complete"
File-based coordination (proven pattern)
The ralph-sprint system proves this coordination model at scale:
- Git as shared memory — agents coordinate via branches, PRs, committed files
- File-based signals — write marker files, poll for existence
- Promise signals — use
<promise>COMPLETE</promise>in agent output for unambiguous completion detection (plain-text markers get false-positives from echoed prompt text) - Stateless iterations — each agent starts fresh from git state, avoiding context compaction issues
Anti-patterns
- Don't send-keys to a pane running Claude Code — it reads stdin, your keystrokes become input to the agent
- Don't use
monitor-activityfor coordination — it's window-level, not pane-level - Don't share a pane between agents — one agent per pane, always
- Don't use sequential loops for independent tasks — use parallel panes instead
- Don't rely on
sleepfor inter-agent timing — use file signals orwait-forchannels
Self-Improvement
If you encounter a tmux pattern that fails, a missing recipe, or incorrect guidance in this skill, don't just work around it — fix the skill:
- Create a PR from a fresh worktree of
eins78/agent-skillson a new branch, fixing the issue directly - Or file an issue on
eins78/agent-skillswith: what failed, the actual tmux behavior, and the suggested fix
Never silently work around a skill gap. The fix benefits all future sessions.