skills/tmux-control/SKILL.md
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.
npx skillsauth add eins78/skills tmux-controlInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reliable patterns for programmatic tmux interaction. The #1 source of bugs is targeting — follow these rules strictly.
%N (pane), @N (window), $NAME (session) — never bare indexeswait-for instead of sleep for synchronizationnew-window/split-window directly — avoid send-keys to freshly created panes (race condition)-d (detached) with new-window and split-window — never steal focus from the user's active windowlist-panes -a to check panes, list-windows for windows, has-session for sessionsThe format is SESSION:WINDOW.PANE. All three parts are optional but be explicit.
# 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}'
| 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 |
# 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')
# 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')
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
tmux send-keys -t "$TARGET" 'your command here' Enter
# 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
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
# 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
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
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.
wait-for provides channel-based synchronization — far more reliable than sleep.
# 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
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
# 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.
# 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}'
# Flag when any output appears in a background window
tmux set-option -t "$WINDOW" monitor-activity on
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.
The pane's shell environment may differ from yours — don't assume your aliases, functions, or PATH are available:
/opt/homebrew/bin/tmux not tmux)# 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
# 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
# 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"
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.
# 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"
-q)%N format)# 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*$'
Patterns for running multiple Claude Code instances (or any agents) in parallel tmux panes.
# 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
# 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"
The ralph-sprint system proves this coordination model at scale:
<promise>COMPLETE</promise> in agent output for unambiguous completion detection (plain-text markers get false-positives from echoed prompt text)monitor-activity for coordination — it's window-level, not pane-levelsleep for inter-agent timing — use file signals or wait-for channelsIf 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:
eins78/agent-skills on a new branch, fixing the issue directlyeins78/agent-skills with: what failed, the actual tmux behavior, and the suggested fixNever silently work around a skill gap. The fix benefits all future sessions.
development
Use when writing or reviewing any TypeScript code. Covers discriminated unions, branded types, Zod at boundaries, const arrays over enums, and safe access patterns.
development
Use when facing technical uncertainty, unproven architecture, or building a large feature where agents or humans risk getting lost in details before confirming the architecture works. Prevents horizontal layer-by-layer building that delays integration feedback.
tools
Use when converting a PDF into a fold-and-print booklet (zine) — A4 sheets, double-sided, short-edge flip, fold to A5. Triggers: make a zine, make a booklet, booklet PDF, imposition, fold-and-print, 2-up booklet, print as booklet, signature imposition, pdf-zine, pdf2zine, bookletimposer. Wraps the `pdf2zine` Docker-based CLI; prefer it over hand-rolled Ghostscript or pdfjam scripts.
development
Use when converting documents between formats — HTML, Markdown, DOCX, PDF, LaTeX, EPUB, reStructuredText, Org, JIRA, CSV, Jupyter notebooks, slides, and 60+ others. Triggers: convert file, export to PDF, make a PDF, turn this into markdown, HTML to markdown, DOCX to markdown, markdown to DOCX, generate slides, create EPUB, format conversion, pandoc, document conversion. Always prefer pandoc over ad-hoc conversion scripts.