Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

The caller's tmux socket is advertised in the instructions but never targeted: an MCP inside `tmux -L work` queries `default`

オープン
#94 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

@tony がすでに取り組んでいます。

2026年7月13日 から。

  • #99 @tony による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
静か
技術スタック
python
領域
backend, tooling

調査の方向性

Start in src/libtmux_mcp/_utils.py at _get_server(), _get_caller_identity(), and _effective_socket_path(), then inspect server_tools.py and _build_instructions() in server.py. Reproduce with a non-default tmux socket and run the existing test suite before adding coverage for socket targeting, caller detection, instruction coherence, and the environment snapshot race. Done means the queried socket and advertised socket agree and caller detection remains correct.

索引モデルが issue の本文から書いたものです。

説明

bug

Summary

_build_instructions() parses $TMUX at import time to tell the agent "Agent context: this MCP runs inside tmux pane %N (socket work)". That socket name is cosmetic. _get_server() resolves the target socket only from LIBTMUX_SOCKET / LIBTMUX_SOCKET_PATH — never from $TMUX. With neither env var set it constructs a bare Server(), which targets tmux's default socket.

So an MCP server launched inside tmux -L work will:

  1. advertise (socket work) in its instructions,
  2. query default on every single tool call,
  3. compute is_caller: false on every pane (the check demands a confirmed realpath socket match, and the caller's pane isn't on default),
  4. and therefore make the instructions' own advertised recovery — "filter list_panes for is_caller=true" — return nothing.

The agent is told where it is, told how to confirm it, and both are wrong. Every self-referential and destructive-guard code path silently degrades.

Evidence

Advertised (display only):

  • server.py#L195 — _build_instructions reads os.environ["TMUX_PANE"]
  • server.py#L201 — hand-parses $TMUX to recover the socket name for the prose line

Targeted (the real behaviour):

  • _utils.py#L502 — _get_server() consults only LIBTMUX_SOCKET / LIBTMUX_SOCKET_PATH
  • _utils.py#L523 — with neither set, a bare Server() → socket_name and socket_path both None → tmux resolves default

The consequence:

  • _utils.py#L186 — _compute_is_caller requires _caller_is_strictly_on_server, a realpath socket match. Caller is on work, panes are on default → False for every row.

Upstream contributor: libtmux's socket_path auto-derivation in Server.__init__ is unreachable dead code — its conditions are mutually contradictory, so Server(socket_name=...) and bare Server() always leave socket_path is None:

This is exactly why libtmux-mcp had to grow _effective_socket_path() (_utils.py#L191), whose fallback shells out to tmux display-message -p '#{socket_path}'.

Reproduction

  1. Start a non-default tmux server and a shell in it:
    tmux -L work new-session -d -s repro
    
  2. Attach, and from inside a pane on that server launch an MCP client whose libtmux-mcp server inherits the environment ($TMUX will point at the work socket; LIBTMUX_SOCKET unset).
  3. Inspect the server instructions delivered at initialize.
    • Observed: Agent context: this MCP runs inside tmux pane %N (socket work).
  4. Call list_panes().
    • Observed: panes from the default socket (or [] if no default server exists) — not the work server the agent is actually sitting in.
    • Observed: is_caller is false on every returned row.
    • Expected: panes from work, with exactly one row is_caller: true.
  5. Teardown: tmux -L work kill-server.

Suggested fix

1. Make _get_server() follow the caller's socket. When LIBTMUX_SOCKET / LIBTMUX_SOCKET_PATH are unset and $TMUX is present, default to the caller's socket path:

caller = _get_caller_identity()
if caller is not None and caller.socket_path:
    return Server(socket_path=caller.socket_path)   # -S <path>

Using the socket path (-S) rather than the name (-L) sidesteps both libtmux's unreachable socket_path derivation and the macOS/launchd TMUX_TMPDIR divergence. Server(socket_path=...) is supported and re-injects -S on every command:

If the behaviour change is a concern, gate it behind LIBTMUX_FOLLOW_TMUX_SOCKET=1 initially — but note that the current behaviour is already incoherent with what the server tells the agent, so "no change" is not a neutral option.

2. Failing that, stop advertising a socket the tools don't use. If _get_server() will keep defaulting to default, the instructions must not claim (socket work). Say (querying socket default; set LIBTMUX_SOCKET to target your own).

3. Fix ServerInfo so the effective socket is knowable. get_server_info echoes server.socket_name / server.socket_path, which are both None for a bare Server() — even against a live server. Populate them from _effective_socket_path() so the agent can at least discover which socket it just queried.

Bonus defect found while investigating: os.environ["TMUX"] race

libtmux's new_session deletes os.environ["TMUX"] process-globally (restoring it in a finally) so a nested new-session doesn't inherit the parent server:

fastmcp runs sync tools in a threadpool (run_in_thread=True by default, never overridden here):

And _get_caller_identity() re-reads os.environ on every call rather than snapshotting at startup:

So a list_panes or kill_server running concurrently with a create_session can observe TMUX unset — degrading is_caller to None and, more seriously, stripping the self-kill guard's primary socket signal.

Fix: snapshot CallerIdentity once at process start. It cannot change for the life of the process, so there is no reason to re-read the environment per call — and doing so removes the race entirely.

Tests

  1. With LIBTMUX_SOCKET unset and $TMUX pointing at a non-default socket, assert _get_server() targets that socket (not default).
  2. On a non-default caller socket, assert list_panes() returns exactly one row with is_caller is True.
  3. Assert the instructions' advertised socket equals the socket _get_server() actually resolves (a coherence test — this is the invariant that is broken today).
  4. Race regression: with _get_caller_identity snapshotted, assert it still returns the caller's identity while os.environ["TMUX"] is transiently deleted.

Related

  • #19 — is_caller false positives across sockets. This is the mirror-image defect: #19 was false positives; this is false negatives on every row.
  • Companion bug: filters={"is_caller": true} silently returns [].
  • Companion feature: whoami — which, if built on Server(socket_path=caller.socket_path), is immune to this bug by construction.
  • Upstream libtmux: unreachable socket_path derivation in Server.__init__.

Environment

  • libtmux-mcp: 0.1.0a17
  • libtmux: 0.61.0
  • fastmcp: 3.4.3 / mcp: 1.28.1
  • tmux: 3.7b
  • Python: 3.14.0

Cross-references — filed together from a single audit of v0.1.0a17. The self-location gap is the root cause; the rest are what it exposed.

  • tmux-python/libtmux-mcp#92 — [bug] documented self-location recipe is unexecutable (filters={"is_caller": true} → [])
  • tmux-python/libtmux-mcp#93 — [feat] where_am_i self-location primitive + self-first relational routing (root fix)
  • tmux-python/libtmux-mcp#94 — [bug] caller's socket advertised but never targeted; TMUX env race vs. the threadpool
  • tmux-python/libtmux-mcp#95 — [perf] list_* unbounded, unscoped, unprojected, excluded from the response limiter
  • tmux-python/libtmux-mcp#96 — [feat] invocation context (in tmux? server running?) + explicit tmux-server auto-start
  • tmux-python/libtmux#704 — upstream: no self-location API (TMUX_PANE); unreachable socket_path derivation

(← this issue: #94)

主要言語
Python
スター
13
フォーク
0
平均マージ
22分
マージ済み PR(30日)
4

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

tmux-python/libtmux-mcp のほかの issue

tmux-python/libtmux-mcp の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。