tmux Skill
Use tmux to control interactive terminal applications when a normal command/API is not enough. Prefer explicit program interfaces first, use tmux control mode for prompt-shaped flows, and reserve screen scraping plus keystrokes for genuinely modal TUIs.
When to Use
- Running REPLs that need a PTY: Python, Node, psql, gdb/lldb.
- Watching long-running interactive commands without blocking the agent.
- Operating a modal prompt or TUI when no documented non-interactive path exists.
- Remote execution where the human may want to attach and observe.
Choose the Interface
-
Use APIs and non-interactive commands first. Prefer flags, config files, JSON output, dry runs, stdin, HTTP APIs, or documented subcommands. This is the only pane-free option.
-
Use tmux control mode for prompt-shaped interactive flows. Control mode still uses tmux panes and PTYs, but the observer is not another visual pane. A control client attaches to a session with
tmux -C attach -t "$session"and receives protocol events such as%output %1 <payload>. The helper filters those events to one stable pane id. -
Use
capture-paneobserve-before-commit for modal TUIs. Full-screen TUIs are stateful. Control mode can stream bytes, but it does not tell you focus, selected rows, or confirmation semantics. Use visible captures and act one step at a time.
Safety Model
Do not treat a terminal UI as a command API. TUIs are stateful and modal: visible labels, numbered rows, highlighted options, and default buttons are evidence, not a stable protocol.
For unfamiliar interactive programs, use this loop:
- Capture the pane.
- Classify the UI state and current focus.
- Send one navigation, editing, or mode-changing action.
- Capture again.
- Send committing keys only after the capture confirms the intended state.
Never combine selection and confirmation in one tmux command for an unfamiliar TUI.
Core Setup Pattern
tmux -L agent new-session -d -s "$SESSION" -x 120 -y 40
tmux -L agent send-keys -t "$SESSION" "python3" Enter
pane=$(tmux -L agent display-message -p -t "$SESSION" '#{pane_id}')
scripts/wait-for-text.sh --control -L agent -t "$pane" -p '^>>> ?$' -T 15
tmux -L agent kill-session -t "$SESSION"
Default to a private socket (-L agent): a session you create for automation
can't then collide with - or crash - the user's live default-socket server
(see Mechanics on socket consistency). Drop -L only when the task is
explicitly about the user's own running session. Use -x 120 -y 40 for
predictable wrapping. Prefer stable pane IDs like %1 over active/default
targets.
Key Risk Classes
- Low risk:
capture-pane,list-panes,list-sessions, read-only probes. - Navigation/editing: arrows,
Tab,BTab,Escape, page keys, literal text input. - Control:
C-c,C-d, EOF, interrupt, quit, cancel. - Submission:
Enter,Space, submit shortcuts. - Confirmation/high risk:
y,Y, approval shortcuts, overwrite/delete/deploy/publish/auth/payment/permission prompts, user-attributed posts or messages, secret/token input.
Treat Enter as context-sensitive. It is not safe by default.
Sending Input
Use literal input for text and real keys separately:
tmux send-keys -lt "$pane" "literal text"
tmux send-keys -t "$pane" Enter
For multiline text, prefer tmux buffers over repeated send-keys. Paste the text, observe it, then submit with a separate key:
tmux load-buffer -b agent-input - <<'EOF'
first line
second line
EOF
tmux paste-buffer -b agent-input -p -t "$pane"
tmux delete-buffer -b agent-input
tmux capture-pane -pt "$pane"
tmux send-keys -t "$pane" Enter
Do not combine the paste and the committing Enter unless the target program is familiar and low risk.
Control Mode
Use control mode when you need to wait for output from a REPL or prompt without polling screenshots of the pane.
scripts/wait-for-text.sh --control -t "$pane" -p '^>>> ?$' -T 15
scripts/control-tail.py -t "$pane" -p 'Password: ?$' -T 30
Useful cases:
- Waiting for prompts after starting a REPL/debugger/database shell.
- Watching output emitted after the watcher starts.
- Filtering output from one pane while the session contains other active panes.
- Driving simple question/answer prompts where visible text is enough evidence.
Limits:
- Control mode is not pane-free. tmux still runs each interactive program in a pane-backed PTY.
- It streams bytes, not semantic UI state.
- It is not a terminal emulator. The helper normalises common ANSI/OSC sequences and carriage returns for matching, but use
capture-panefor modal TUI state. - The helper decodes tmux
%outputand%extended-outputoctal escapes with Python stdlib only.
Capture Mode and Modal TUIs
When driving an unfamiliar TUI:
- Capture before acting.
- Send at most one navigation or mode-changing key at a time.
- Capture again before any committing key.
- Follow on-screen shortcut text over inferred conventions from numbering.
- Use
tmux send-keys -lfor literal text. - Send real keys separately from literal text.
- Stop and ask if focus, selected action, prompt wording, or mode is ambiguous.
Useful capture commands:
tmux capture-pane -pt "$pane" # visible content
tmux capture-pane -pS - -E - -t "$pane" # all available history
tmux capture-pane -pM -t "$pane" # tmux mode content
tmux capture-pane -pa -t "$pane" # alternate-screen content when needed
Poll screen state with capture-pane and display-message -p rather than sleeping blindly.
Example: Modal Feedback Prompt
Unsafe:
tmux send-keys -t "$pane" 4 Enter
This combines selection and confirmation without observing the state between them.
Safer:
tmux capture-pane -pt "$pane"
tmux send-keys -t "$pane" BTab
tmux capture-pane -pt "$pane"
tmux send-keys -lt "$pane" "validate plan and assumptions"
tmux capture-pane -pt "$pane"
# Only submit after confirming the text is in the intended input.
tmux send-keys -t "$pane" Enter
Mechanics
- Fully qualify targets with
-t "$pane"; avoid relying on the active pane. send-keyssends all arguments sequentially.tmux send-keys -t "$pane" 4 Entersends4and then immediately presses Enter.send-keystreats recognised names likeEnter,Escape,Up,C-c, andBTabas keys.- Use
send-keys -lwhen text might look like a key name. - Resolve targets with
tmux display-message -p -t "$target" '#{pane_id}|#{session_id}'. - For private servers, use
tmux -L "$name"ortmux -S "$path"consistently across all commands.
Coding Agents in Panes
When the target pane runs a coding agent (Claude Code, Codex), switch to the
coding-agents skill: it owns the agent CLI
(ls/wait/prompt/name/pick) and the hook-driven #{@agent_state}
vocabulary, so you wait on state instead of scraping the screen. Come back
here the moment the agent is blocked (a modal prompt needing the
observe-before-commit loop) or the pane is untracked (capture-based
observation).
Remote Execution (Codespaces/SSH)
For mise-installed tools, wrap in zsh:
# Non-interactive (won't hang)
ssh host 'zsh -c "source ~/.zshrc; tmux new-session -d -s mysession; tmux send-keys -t mysession python Enter"'
# Interactive (for tmux attach) - needs TTY
ssh host -t 'zsh -ilc "tmux attach -t mysession"'
Critical: Use zsh -c "source ~/.zshrc; ..." not zsh -lc to avoid hangs.
User Notification
After starting a session, print the following so the human can attach and observe the session you started:
To monitor: tmux attach -t $SESSION
To capture: tmux capture-pane -t $SESSION -p
Helper Scripts
wait-for-text.sh
Wait for a text pattern with timeout. Capture-polling is the default; --control follows tmux output events.
scripts/wait-for-text.sh -t session:0.0 -p '^>>>' -T 15
scripts/wait-for-text.sh --control -t %1 -p '^>>> ?$' -T 15
scripts/wait-for-text.sh -L private -t repl -p 'ready'
Options:
-L SOCKET_NAMEor-S SOCKET_PATHtarget a private tmux server.-i INTERVALcontrols capture polling and is ignored in control mode.- Capture mode uses grep ERE; control mode uses Python regex.
control-tail.py
Follow one pane with tmux control mode:
scripts/control-tail.py -t %1
scripts/control-tail.py -t %1 -p 'READY|ERROR' -T 30
scripts/control-tail.py -L private -t repl:0.0 -p '^>>> ?$' --no-seed
Its parsing helpers (decode_tmux_payload octal escapes, parse_output_line
%output/%extended-output handling) are pinned by
tests/test_control_tail.py —
uv run --with pytest -- pytest tests/ from the skill dir.
find-sessions.sh
List tmux sessions, optionally filtered:
scripts/find-sessions.sh -q claude # filter by name
scripts/find-sessions.sh --all # all sessions
scripts/find-sessions.sh -L private # named socket
tmux-common.sh
Internal shared socket-option helpers used by the shell scripts.