Dotfiles tmux

Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.

install
source · Clone the upstream repo
git clone https://github.com/megalithic/dotfiles
Claude Code · Install into ~/.claude/skills/
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"
manifest: home/common/programs/ai/pi-coding-agent/skills/tmux/SKILL.md
source content

tmux 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:

  1. Identify your own pane: Run
    tmux display-message -p '#{pane_id}'
    to get the pane pi is running in. Store this — never kill it.
  2. Verify target pane before kill: Before
    kill-pane -t X
    , confirm X is not your own pane ID.
  3. 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
    
  4. 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.
  5. 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
    CLAUDE_TMUX_SOCKET_DIR
    (defaults to
    ${TMPDIR:-/tmp}/claude-tmux-sockets
    ) and use
    tmux -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.0
    if omitted. Keep names short (e.g.,
    claude-py
    ,
    claude-gdb
    ).
  • Use
    -S "$SOCKET"
    consistently to stay on the private socket path. If you need user config, drop
    -f /dev/null
    ; otherwise
    -f /dev/null
    gives a clean config.
  • Inspect:
    tmux -S "$SOCKET" list-sessions
    ,
    tmux -S "$SOCKET" list-panes -a
    .

Finding sessions

  • List sessions on your active socket with metadata:
    ~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/find-sessions.sh -S "$SOCKET"
    ; add
    -q partial-name
    to filter.
  • Scan all sockets under the shared directory:
    ~/.dotfiles/home/common/programs/ai/pi-coding-agent/skills/tmux/scripts/find-sessions.sh --all
    (uses
    CLAUDE_TMUX_SOCKET_DIR
    or
    ${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
    ,
    Escape
    , etc.

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
    tmux wait-for
    (which does not watch pane output).
  • You can also temporarily attach to observe:
    tmux -L "$SOCKET" attach -t "$SESSION"
    ; detach with
    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
    PYTHON_BASIC_REPL=1
    environment 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:
    ~/.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"
    ,
    "Program exited"
    , etc.) before proceeding.

Interactive tool recipes

  • Python REPL:
    tmux ... send-keys -- 'python3 -q' Enter
    ; wait for
    ^>>>
    ; send code with
    -l
    ; interrupt with
    C-c
    . Always with
    PYTHON_BASIC_REPL
    .
  • gdb:
    tmux ... send-keys -- 'gdb --quiet ./a.out' Enter
    ; disable paging
    tmux ... send-keys -- 'set pagination off' Enter
    ; break with
    C-c
    ; issue
    bt
    ,
    info locals
    , etc.; exit via
    quit
    then confirm
    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
    /
    --target
    pane target (required)
  • -p
    /
    --pattern
    regex to match (required); add
    -F
    for fixed string
  • -T
    timeout seconds (integer, default 15)
  • -i
    poll interval seconds (default 0.5)
  • -l
    history 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.