Skip to content

run_command: tmux completion wrapper breaks over SSH panes (error connecting to socket) #132

Open

Description

@datndptit

Description

run_command wraps each authored command with a completion-sync wrapper (see
src/libtmux_mcp/tools/pane_tools/io.py, the status_cmd / signal_cmd
construction):

(
<command>
); s=$?; tmux set-option -p -t <pane_id> @s_<id> "$s"; tmux wait-for -S r_<id>

The tmux set-option and tmux wait-for -S fragments execute inside the
pane's shell
. When the pane's shell is an active SSH session, that shell
lives on a remote host that has no tmux server (or a different socket), so both
calls fail in the pane with:

error connecting to /tmp/tmux-1001/default (No such file or directory)
error connecting to /tmp/tmux-1001/default (No such file or directory)

(the socket path carries the remote user's uid).

Steps to reproduce

  1. In a tmux pane, run ssh user@remote-host and log in.
  2. Have the agent call run_command in that pane (e.g. git status).
  3. Observe the two error connecting to .../default lines printed in the pane.

Actual behavior

  • The user's command still runs and its output is visible, so it looks like it
    "works".
  • But the wait-for channel is never signalled, so the server-side wait runs to
    the LIBTMUX_MCP_WAIT_MAX_SECONDS ceiling and the call reports a
    timeout-based result.
  • The @s_<id> pane option is never set, so no exit status can be read back.
  • The error lines are printed into the pane a human may be watching.

Expected behavior

One or both:

  1. Document the limitation — gotchas.md, troubleshooting.md, and/or the
    run_command tool description: run_command assumes the pane's shell can
    reach the same tmux server the MCP talks to. In SSH panes, compose
    send_keys + a shell-level completion marker + wait_for_text +
    capture_pane instead.
  2. Detect it — e.g. check pane_current_command (or pane_in_mode) before
    wrapping: when the pane is ssh, either skip the wrapper and warn in the
    result, or fail loudly with a hint, rather than sending a wrapper that is
    guaranteed to emit errors on the remote.

Workaround that works (shell marker, no tmux in the payload)

send_keys(pane_id, "git status; echo __MCP_DONE_$?__", enter=True, literal=True)
wait_for_text(pane_id, patterns=["__MCP_DONE_"], timeout=120)
capture_pane(pane_id)

The marker is pure shell and runs on the remote — no tmux call, so no error
lines; the exit code is read from the __MCP_DONE_<n> line in the capture.

Environment

  • libtmux-mcp 0.1.0a22 (FastMCP 4.0.3)
  • tmux 3.4 on WSL2 Ubuntu (local); remote host is a plain Ubuntu 24.04 box
  • MCP server addresses the local tmux server; the affected pane is a shell that
    has logged in over SSH

Activity

  1. isamu commented on Sep 18, 2026

    @isamu

    The same failure reproduces on one host with no SSH. What the wrapper needs is for the pane's shell to resolve the same socket the server does, and a remote filesystem is only one way to lose that. A different $TMUX_TMPDIR in the pane's shell does it too.

    Two arms, identical except for that variable:

    # Both arms of the wrapper, on one host, no SSH.
    SERVER_TMPDIR=$(mktemp -d /tmp/srv.XXXX); PANE_TMPDIR=$(mktemp -d /tmp/pane.XXXX)
    srv() { TMUX_TMPDIR=$SERVER_TMPDIR tmux -L probe "$@"; }
    
    srv new-session -d -s s -x 200 -y 50
    sleep 1
    
    # Arm 1: the pane's shell shares the server's TMUX_TMPDIR.
    srv send-keys -t %0 "tmux -L probe set-option -p -t %0 @arm_control ok" Enter
    
    # Arm 2: only the pane's shell has a different TMUX_TMPDIR.
    srv send-keys -t %0 \
      "TMUX_TMPDIR=$PANE_TMPDIR tmux -L probe set-option -p -t %0 @arm_test ng" Enter
    srv send-keys -t %0 \
      "TMUX_TMPDIR=$PANE_TMPDIR tmux -L probe wait-for -S ch_test" Enter
    sleep 2
    
    echo "--- what the pane printed ---"
    srv capture-pane -p -t %0 | grep 'error connecting'
    echo "--- options that got set ---"
    srv show-options -p -t %0 | grep @arm
    echo "--- does wait-for ever signal? ---"
    TMUX_TMPDIR=$SERVER_TMPDIR timeout 3 tmux -L probe wait-for ch_test
    echo "wait-for exit: $?   (124 = timed out)"
    
    srv kill-server; rm -rf "$SERVER_TMPDIR" "$PANE_TMPDIR"
    --- what the pane printed ---
    error connecting to /private/tmp/pane.vZw4/tmux-501/probe (No such file or directory)
    error connecting to /private/tmp/pane.vZw4/tmux-501/probe (No such file or directory)
    --- options that got set ---
    @arm_control ok
    --- does wait-for ever signal? ---
    wait-for exit: 124   (124 = timed out)
    

    Arm 1 sets its option. Arm 2 prints error connecting, never sets @arm_test, and its wait-for -S never reaches the server, so the matching wait-for on the server side runs to the timeout.

    _tmux_argv carries -L/-S into the pane, which pins which socket is asked for. Resolving that name is still done by the pane's shell, against its own $TMUX_TMPDIR, its own euid, and its own filesystem. Any of the three can differ from the server's:

    • an SSH session: different filesystem (this report)
    • a different $TMUX_TMPDIR: above
    • a shell under another uid, since the path carries tmux-<euid>
    • a container shell (docker exec): same as SSH

    The outcome is the same in each. The authored command runs, its output is visible, and only the completion signal is lost — so run_command waits out the ceiling before reporting anything, and the two error connecting lines are the only clue, sitting in the pane's scrollback rather than in the tool's result.

    One note on detection: #{pane_current_command} catches ssh and docker, but not the $TMUX_TMPDIR case, where the pane's shell is an ordinary local shell.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions