This skill should be used when the user asks to "run commands on a remote pane", "check remote pane output", "capture output from remote", "send commands to remote server", "tail logs on prod", "check disk space on staging", "run uptime across servers", "is my SSH connection alive", "read what's on the remote pane", or needs to interact with already-established SSH connections in tmux panes. Also activates when you are about to suggest the user manually SSH into a remote host to run a command, when a command needs to run on a remote machine, when troubleshooting requires executing something on a known remote host, or when you would otherwise tell the user to "run this on the server" or "SSH in and do X". Not for creating new connections (use the /remote command for that).
From claude-tmuxnpx claudepluginhub hex/claude-marketplace --plugin claude-tmuxThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Orchestrates subagents to execute phased plans: deploys for implementation, verification, anti-pattern checks, code quality review, and commits only after passing checks.
The claude-tmux plugin manages SSH connections via tmux panes. This skill documents patterns for interacting with those connections. Each remote connection lives in a dedicated tmux pane, tagged with a custom @remote option set to the connection name (e.g., mac-mini, prod-web). The /remote command handles establishing connections; this skill covers the patterns for interacting with those connections once established.
All remote interaction follows a consistent model: find the target pane by its @remote tag, send commands using tmux send-keys, and read output using tmux capture-pane.
When you determine that a command needs to run on a remote host -- instead of telling the user to SSH in and run it manually -- check for an active remote pane first.
Before suggesting manual SSH commands, check if a remote pane is connected to the target host:
for pane_id in $(tmux list-panes -a -F '#{pane_id}'); do
name=$(tmux show-options -p -t "$pane_id" -v @remote 2>/dev/null) && [ -n "$name" ] && echo "$pane_id $name"
done
Also check saved hosts to see if the target machine has a known entry:
bash ${CLAUDE_PLUGIN_ROOT}/scripts/hosts.sh ${CLAUDE_PLUGIN_ROOT}/remote-hosts.json list
If an active pane exists for the target host, use AskUserQuestion to offer running the command through it:
<host>. Want me to run it through the remote pane, or would you prefer to do it manually?"If no pane is connected but the host is saved, offer to connect first:
<host>. Want me to connect and run it?"<host> and send the command"tmux panes have a real TTY, so sudo and interactive commands work through them. For sudo commands, send the command normally -- the remote pane's TTY handles password prompts. Use prompt detection (not marker polling) to wait for sudo's password prompt or completion:
tmux send-keys -t "$PANE" "sudo systemctl restart nginx" Enter
After sending a sudo command, capture the pane output to check whether it's waiting for a password. If it is, inform the user so they can type the password in the pane directly.
Do not offer remote execution when:
$TMUX is unset)Remote panes are tagged with the @remote custom pane option. List all remote panes:
for pane_id in $(tmux list-panes -a -F '#{pane_id}'); do
name=$(tmux show-options -p -t "$pane_id" -v @remote 2>/dev/null) && [ -n "$name" ] && echo "$pane_id $name"
done
To find a specific named remote pane:
for pane_id in $(tmux list-panes -a -F '#{pane_id}'); do
name=$(tmux show-options -p -t "$pane_id" -v @remote 2>/dev/null) && [ "$name" = "prod-web" ] && echo "$pane_id"
done
tmux send-keys -t <pane_id> "<command>" Enter
The Enter argument (unquoted) sends a keypress to execute the command. Without it, the text appears in the pane but does not execute.
Send commands one at a time with a brief pause between them to allow each to begin executing:
tmux send-keys -t %5 "cd /var/log" Enter
sleep 0.5
tmux send-keys -t %5 "tail -n 50 syslog" Enter
Escape double quotes and dollar signs within the command string:
tmux send-keys -t %5 "echo \"hello world\"" Enter
tmux send-keys -t %5 "echo \$HOME" Enter
For commands with complex quoting, use single quotes in the outer layer:
tmux send-keys -t %5 'grep "error" /var/log/app.log | wc -l' Enter
After sending a command, wait for it to finish before capturing output. Prefer the marker-polling pattern over fixed sleeps -- it adapts to actual command duration instead of guessing.
Marker polling (preferred): Append a unique marker after the command, then poll until it appears:
MARKER="__DONE_$$_$(date +%s)__"
tmux send-keys -t %5 "ls -la /etc; echo ${MARKER}" Enter
for i in $(seq 1 30); do
sleep 0.5
tmux capture-pane -t %5 -p -S -50 | grep -q "$MARKER" && break
done
tmux capture-pane -t %5 -p -S -50
The marker is unique per invocation (PID + timestamp), so it won't collide with command output. The loop polls every 0.5s for up to 15 seconds.
Prompt detection (when markers aren't possible): For commands already in flight, interactive programs, or REPLs where appending a marker would change behavior, poll for the shell prompt to reappear:
# Snapshot the prompt before sending
PROMPT_CHAR=$(tmux capture-pane -t %5 -p | grep -v '^$' | tail -1 | grep -oE '[#$%>❯] *$')
tmux send-keys -t %5 "long-running-command" Enter
for i in $(seq 1 60); do
sleep 0.5
LAST=$(tmux capture-pane -t %5 -p | grep -v '^$' | tail -1)
echo "$LAST" | grep -qE '[#$%>❯] *$' && break
done
tmux capture-pane -t %5 -p -S -50
This works by detecting when a prompt-ending character ($, #, %, >, ❯) appears at the end of the last non-empty line -- meaning the shell is waiting for input again. Use this when you cannot modify the command string.
Fixed delay (simple commands only): For commands that reliably complete within a known time:
tmux send-keys -t %5 "uptime" Enter
sleep 1
Use fixed delays only for trivial commands (uptime, whoami, pwd). For anything that touches disk, network, or processes, use marker polling or prompt detection.
Print the current visible contents of a remote pane to stdout:
tmux capture-pane -t <pane_id> -p
Retrieve the last N lines of output (including scrollback beyond the visible area):
tmux capture-pane -t <pane_id> -p -S -20
The -S -20 flag starts capture 20 lines before the current bottom of the pane.
Retrieve all available scrollback history:
tmux capture-pane -t <pane_id> -p -S -
The -S - flag starts from the very beginning of the scrollback buffer.
Redirect captured output to a local file for processing:
tmux capture-pane -t <pane_id> -p > /tmp/remote-output.txt
Combine capture with grep to find specific output:
tmux capture-pane -t %5 -p -S - | grep "ERROR"
Determine whether a remote pane is still alive:
tmux list-panes -a -F '#{pane_id} #{pane_title} #{pane_dead}'
A value of 1 in the pane_dead column indicates the pane's process has exited (SSH session terminated or crashed).
Iterate panes and check for the @remote tag:
for pane_id in $(tmux list-panes -a -F '#{pane_id}'); do
name=$(tmux show-options -p -t "$pane_id" -v @remote 2>/dev/null) && [ -n "$name" ] && \
dead=$(tmux display-message -t "$pane_id" -p '#{pane_dead}') && echo "$pane_id $name dead=$dead"
done
Terminate a remote connection by killing its pane:
tmux kill-pane -t <pane_id>
To gracefully disconnect first, send an exit command before killing:
tmux send-keys -t %5 "exit" Enter
sleep 1
tmux kill-pane -t %5
If an SSH connection drops (pane still alive but shell returned to local), send the SSH command again to the same pane:
tmux send-keys -t %5 "ssh user@host" Enter
If the pane is dead, create a new connection using the plugin's connect script. The plugin uses SSH by default. Hosts configured with "use_et": true in remote-hosts.json will use Eternal Terminal (et) instead, which survives network changes and rarely drops.
Never use mosh for remote tmux pane connections — it is not designed for tunneling traffic and will cause reliability issues.
Check whether the remote pane is at a remote shell or has fallen back to a local shell by inspecting the pane output:
tmux capture-pane -t %5 -p -S -3
Look for the remote hostname in the prompt to confirm the SSH session is active.
The most frequent pattern -- execute a remote command and retrieve the result:
PANE=$(for p in $(tmux list-panes -a -F '#{pane_id}'); do n=$(tmux show-options -p -t "$p" -v @remote 2>/dev/null) && [ "$n" = "prod-web" ] && echo "$p"; done)
MARKER="__DONE_$$_$(date +%s)__"
tmux send-keys -t "$PANE" "df -h; echo ${MARKER}" Enter
for i in $(seq 1 30); do sleep 0.5; tmux capture-pane -t "$PANE" -p -S -50 | grep -q "$MARKER" && break; done
tmux capture-pane -t "$PANE" -p -S -50
Check whether a remote command succeeded by appending an exit code marker:
MARKER="__EXIT_$$_$(date +%s)__"
tmux send-keys -t "$PANE" "some-command && echo ${MARKER}:0 || echo ${MARKER}:1" Enter
for i in $(seq 1 30); do sleep 0.5; tmux capture-pane -t "$PANE" -p -S -20 | grep -q "$MARKER" && break; done
tmux capture-pane -t "$PANE" -p -S -20 | grep "$MARKER"
A result ending in :0 indicates success; :1 indicates failure. The unique marker prevents collisions with command output.
Use scp or rsync from a local pane (not the remote pane) to transfer files:
scp user@host:/path/to/remote/file /tmp/local-copy
rsync -avz user@host:/var/log/app/ /tmp/remote-logs/
Do not attempt file transfers through send-keys on a remote pane -- use a separate local command.
Include the -L flag in the SSH command when establishing the connection:
ssh -L 8080:localhost:3000 user@host
This forwards local port 8080 to port 3000 on the remote host. Set this up during connection creation, not after the session is established.
send-keys handles interactive CLI tools running on the remote host. Send keystrokes as individual arguments:
# Open a file in vim
tmux send-keys -t %5 "vim /etc/nginx/nginx.conf" Enter
# Type in insert mode
tmux send-keys -t %5 "i" "new content" Escape
# Save and quit
tmux send-keys -t %5 ":wq" Enter
For Python/Node REPL sessions:
tmux send-keys -t %5 "python3" Enter
sleep 1
tmux send-keys -t %5 "import os; print(os.uname())" Enter
For jobs that run for minutes or longer (backups, migrations, batch processing), redirect output to a log file on the remote host so progress is checkable without keeping the shell blocked:
tmux send-keys -t "$PANE" "nohup /opt/backup.sh > /tmp/backup.log 2>&1 &" Enter
Check progress later:
tmux send-keys -t "$PANE" "tail -5 /tmp/backup.log" Enter
Check if the job is still running:
tmux send-keys -t "$PANE" "jobs -l" Enter
This keeps the remote shell available for other commands while the job runs in the background.
Run the same command across multiple remote panes. Use a unique marker per pane so that polling each pane's output only matches its own completion signal -- a shared marker could cause false positives if one pane's output bleeds into another's scrollback during rapid polling.
# Collect remote pane IDs
REMOTE_PANES=()
for pane_id in $(tmux list-panes -a -F '#{pane_id}'); do
name=$(tmux show-options -p -t "$pane_id" -v @remote 2>/dev/null) && [ -n "$name" ] && REMOTE_PANES+=("$pane_id")
done
# Send command to all panes with unique markers (fire all first, then poll)
declare -A MARKERS
for pane in "${REMOTE_PANES[@]}"; do
MARKERS[$pane]="__DONE_${pane}_$$_$(date +%s)__"
tmux send-keys -t "$pane" "uptime; echo ${MARKERS[$pane]}" Enter
done
# Poll each pane for its unique marker, then capture
for pane in "${REMOTE_PANES[@]}"; do
for i in $(seq 1 20); do sleep 0.5; tmux capture-pane -t "$pane" -p -S -20 | grep -q "${MARKERS[$pane]}" && break; done
echo "=== $pane ==="
tmux capture-pane -t "$pane" -p -S -20
done
When executing a sequence of remote commands (deployments, migrations, diagnostics), use TodoWrite to track progress through each step. This prevents losing track of where you are if a step fails or the conversation is interrupted.
Pattern:
Example -- deploying to a remote host:
send-keys, poll for completion with marker, capture output, check for errors, mark todo complete:1), stop and report which step failed with the captured outputThis pattern is especially valuable for multi-host operations where you run the same sequence on several hosts -- track each host's progress independently.
The plugin provides helper scripts for connection and host management:
${CLAUDE_PLUGIN_ROOT}/scripts/connect.sh -- Create new SSH connections in tmux panes. Uses SSH by default; hosts with "use_et": true in their config use Eternal Terminal instead. Supports a command field in host config to run a command on connect (e.g., tmux new -A -s main to attach to a persistent remote tmux session).${CLAUDE_PLUGIN_ROOT}/scripts/hosts.sh -- Manage saved remote host configurations. Add, remove, and list known hosts.${CLAUDE_PLUGIN_ROOT}/remote-hosts.json -- Persistent storage for saved host entries (hostname, user, port, identity file, and custom options).Use these scripts rather than manually constructing tmux and SSH commands when creating new connections. For interacting with already-established connections, use the send-keys and capture-pane patterns described above.