Dotfiles tmux
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.
git clone https://github.com/megalithic/dotfiles
T=$(mktemp -d) && git clone --depth=1 https://github.com/megalithic/dotfiles "$T" && mkdir -p ~/.claude/skills && cp -r "$T/home/common/programs/ai/pi-coding-agent/skills/tmux" ~/.claude/skills/megalithic-dotfiles-tmux && rm -rf "$T"
home/common/programs/ai/pi-coding-agent/skills/tmux/SKILL.mdtmux Skill
Use tmux as a programmable terminal multiplexer for interactive work. Works on Linux and macOS with stock tmux; avoid custom config by using a private socket.
Script location:
~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/
CRITICAL: Pane Safety Rules
Never kill or disrupt the pane running pi. Before killing, resizing, or replacing ANY pane:
- Identify your own pane: Run
to get the pane pi is running in. Store this — never kill it.tmux display-message -p '#{pane_id}' - Verify target pane before kill: Before
, confirm X is not your own pane ID.kill-pane -t X - Verify target pane before send-keys: Before sending keys to ANY pane, verify the expected process is actually running there:
# Check what's running in target pane tmux display-message -t "$TARGET" -p '#{pane_current_command}' # Or capture last few lines to confirm the right prompt/app tmux capture-pane -p -t "$TARGET" -S -5 - Never send keys blindly. If the expected app (python, gdb, w3m, etc.) is NOT in the target pane, STOP and re-discover the correct pane.
- Never assume pane layout persists. Panes can be rearranged, closed by user, or swapped. Always re-verify before interacting.
Verification pattern (use before every send-keys or kill-pane):
# Get my pane (pi's pane) — do this once at start MY_PANE=$(tmux display-message -p '#{pane_id}') # Before interacting with a target TARGET_CMD=$(tmux display-message -t "$TARGET" -p '#{pane_current_command}' 2>&1) if [ $? -ne 0 ]; then echo "Target pane $TARGET does not exist" elif [ "$TARGET" = "$MY_PANE" ]; then echo "ABORT: target is pi's own pane" else echo "Target running: $TARGET_CMD" fi
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 -S "$SOCKET" new -d -s "$SESSION" -n shell tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'python3 -q' Enter tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 # watch output tmux -S "$SOCKET" kill-session -t "$SESSION" # clean up
After 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 claude-lldb Or to capture the output once: tmux -S "$SOCKET" capture-pane -p -J -t claude-lldb:0.0 -S -200
This 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
(defaults toCLAUDE_TMUX_SOCKET_DIR
) and use${TMPDIR:-/tmp}/claude-tmux-sockets
so we can enumerate/clean them. Create the dir first:tmux -S "$SOCKET"
.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:
, defaults to{session}:{window}.{pane}
if omitted. Keep names short (e.g.,:0.0
,claude-py
).claude-gdb - Use
consistently to stay on the private socket path. If you need user config, drop-S "$SOCKET"
; otherwise-f /dev/null
gives a clean config.-f /dev/null - Inspect:
,tmux -S "$SOCKET" list-sessions
.tmux -S "$SOCKET" list-panes -a
Finding sessions
- List sessions on your active socket with metadata:
; add~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/find-sessions.sh -S "$SOCKET"
to filter.-q partial-name - Scan all sockets under the shared directory:
(uses~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/find-sessions.sh --all
orCLAUDE_TMUX_SOCKET_DIR
).${TMPDIR:-/tmp}/claude-tmux-sockets
Sending input safely
- Always verify the target pane has the expected process before sending keys (see Pane Safety Rules above).
- Prefer literal sends to avoid shell splitting:
tmux -L "$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
, etc.Escape
Watching output
- Capture recent history (joined lines to avoid wrapping artifacts):
.tmux -L "$SOCKET" capture-pane -p -J -t target -S -200 - For continuous monitoring, poll with the helper script (below) instead of
(which does not watch pane output).tmux wait-for - You can also temporarily attach to observe:
; detach withtmux -L "$SOCKET" attach -t "$SESSION"
.Ctrl+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
environment variable. This is very important as the non-basic console interferes with your send-keys.PYTHON_BASIC_REPL=1
Synchronizing / waiting for prompts
- Use timed polling to avoid races with interactive tools. Example: wait for a Python prompt before sending code:
~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/wait-for-text.sh -t "$SESSION":0.0 -p '^>>>' -T 15 -l 4000 - For long-running commands, poll for completion text (
,"Type quit to exit"
, etc.) before proceeding."Program exited"
Interactive tool recipes
- Python REPL:
; wait fortmux ... send-keys -- 'python3 -q' Enter
; send code with^>>>
; interrupt with-l
. Always withC-c
.PYTHON_BASIC_REPL - gdb:
; disable pagingtmux ... send-keys -- 'gdb --quiet ./a.out' Enter
; break withtmux ... send-keys -- 'set pagination off' Enter
; issueC-c
,bt
, etc.; exit viainfo locals
then confirmquit
.y - 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
~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/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.
~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/wait-for-text.sh -t session:0.0 -p 'pattern' [-F] [-T 20] [-i 0.5] [-l 2000]
/-t
pane target (required)--target
/-p
regex to match (required); add--pattern
for fixed string-F
timeout seconds (integer, default 15)-T
poll interval seconds (default 0.5)-i
history lines to search from the pane (integer, default 1000)-l- Exits 0 on first match, 1 on timeout. On failure prints the last captured text to stderr to aid debugging.