"Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output."
Resources
1Install
npx skillscat add samuerio/dotfiles/tmux Install via the SkillsCat registry.
tmux Skill
Use tmux as a programmable terminal multiplexer for interactive work. Works on Linux and macOS with stock tmux; use user's tmux config with a private socket for isolation.
Quickstart (isolated socket)
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets # well-known dir for all agent sockets
mkdir -p "$SOCKET_DIR"
SOCKET="$SOCKET_DIR/claude.sock" # keep agent sessions separate from your personal tmux
SESSION=claude-python # slug-like names; avoid spaces
TMUX=(tmux -S "$SOCKET") # Loads user's ~/.tmux.conf config
"${TMUX[@]}" new -d -s "$SESSION" -n shell
TARGET="$SESSION:0.0"
"${TMUX[@]}" send-keys -t "$TARGET" -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
"${TMUX[@]}" capture-pane -p -J -t "$TARGET" -S -200 # watch output
"${TMUX[@]}" kill-session -t "$SESSION" # clean upAfter starting a session ALWAYS tell the user how to monitor the session by giving them a command to copy paste:
To monitor this session yourself:
tmux -S "$SOCKET" attach -t "$SESSION"
Or to capture the output once:
tmux -S "$SOCKET" capture-pane -p -J -t "$TARGET" -S -200This must ALWAYS be printed right after a session was started and once again at the end of the tool loop. But the earlier you send it, the happier the user will be.
Socket convention
- Agents MUST place tmux sockets under
CLAUDE_TMUX_SOCKET_DIR(defaults to${TMPDIR:-/tmp}/claude-tmux-sockets) and usetmux -S "$SOCKET"so we can enumerate/clean them. Create the dir first:mkdir -p "$CLAUDE_TMUX_SOCKET_DIR". - Default socket path to use unless you must isolate further:
SOCKET="$CLAUDE_TMUX_SOCKET_DIR/claude.sock".
Targeting panes and naming
- Target format:
{session}:{window}.{pane}, defaults to:0.0if omitted. Keep names short (e.g.,claude-py,claude-gdb). - Use
-S "$SOCKET"consistently to stay on the private socket path. By default uses user's tmux config; use-f /dev/nullfor a clean config if needed. - If you use your tmux config (no
-f /dev/null), do not assume:0.0; discover pane ids viatmux -S "$SOCKET" list-panes -t "$SESSION" -F '#{session_name}:#{window_index}.#{pane_index}'. - Inspect:
tmux -S "$SOCKET" list-sessions,tmux -S "$SOCKET" list-panes -a.
Finding sessions
- List sessions on your active socket with metadata:
./scripts/find-sessions.sh -S "$SOCKET"; add-q partial-nameto filter. - For machine parsing, emit JSON:
./scripts/find-sessions.sh -S "$SOCKET" --json. - Scan all sockets under the shared directory:
./scripts/find-sessions.sh --all(usesCLAUDE_TMUX_SOCKET_DIRor${TMPDIR:-/tmp}/claude-tmux-sockets).
Sending input safely
- Prefer literal sends to avoid shell splitting:
tmux -S "$SOCKET" send-keys -t target -l -- "$cmd" - When composing inline commands, use single quotes or ANSI C quoting to avoid expansion:
tmux ... send-keys -t target -- $'python3 -m http.server 8000'. - To send control keys:
tmux ... send-keys -t target C-c,C-d,C-z,Escape, etc.
Watching output
- Capture recent history (joined lines to avoid wrapping artifacts):
tmux -S "$SOCKET" capture-pane -p -J -t target -S -200. - For continuous monitoring, poll with the helper script (below) instead of
tmux wait-for(which does not watch pane output). - You can also temporarily attach to observe:
tmux -S "$SOCKET" attach -t "$SESSION"; detach withCtrl+b d. - When giving instructions to a user, explicitly print a copy/paste monitor command alongside the action don't assume they remembered the command.
Spawning Processes
Some special rules for processes:
- when asked to debug, use lldb by default
- when starting a python interactive shell, always set the
PYTHON_BASIC_REPL=1environment variable. This is very important as the non-basic console interferes with your send-keys.
Synchronizing / waiting for prompts
- Use timed polling to avoid races with interactive tools. Example: wait for a Python prompt before sending code:
./scripts/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15 -l 4000 - For long-running commands, poll for completion text (
"Type quit to exit","Program exited", etc.) before proceeding.
Interactive tool recipes
- Python REPL:
tmux ... send-keys -- 'python3 -q' Enter; wait for^>>>; send code with-l; interrupt withC-c. Always withPYTHON_BASIC_REPL. - gdb:
tmux ... send-keys -- 'gdb --quiet ./a.out' Enter; disable pagingtmux ... send-keys -- 'set pagination off' Enter; break withC-c; issuebt,info locals, etc.; exit viaquitthen confirmy. - Other TTY apps (ipdb, psql, mysql, node, bash): same pattern—start the program, poll for its prompt, then send literal text and Enter.
Cleanup
- Kill a session when done:
tmux -S "$SOCKET" kill-session -t "$SESSION". - Kill all sessions on a socket:
tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t. - Remove everything on the private socket:
tmux -S "$SOCKET" kill-server.
Helper: wait-for-text.sh
./scripts/wait-for-text.sh polls a pane for a regex (or fixed string) with a timeout. Works on Linux/macOS with bash + tmux + grep.
./scripts/wait-for-text.sh -S "$SOCKET" -t session:0.0 -p 'pattern' [-F] [-T 20] [-i 0.5] [-l 2000]-t/--targetpane target (required)-p/--patternregex to match (required); add-Ffor fixed string-S/--socket-pathsocket path (recommended with private sockets), or-L/--socketsocket name-Ttimeout seconds (integer, default 15)-ipoll interval seconds (default 0.5)-lhistory lines to search from the pane (integer, default 1000)- Exits 0 on first match, 1 on timeout. On failure prints the last captured text to stderr to aid debugging.