Repository navigation
is_caller annotation gives false positives across tmux sockets #19
Description
Summary
The is_caller boolean returned on every pane serialization (PaneInfo.is_caller) compares pane IDs without considering the tmux socket. A caller whose TMUX_PANE is %1 on socket default will see is_caller: true for any pane %1 on any other socket — the wrong pane, on the wrong server. The 0.1.0a2 caller-identity-scoping work fixed this for the self-kill gate but not for the informational annotation.
Evidence
src/libtmux_mcp/_utils.py:773 — inside _serialize_pane:
is_caller=pane.pane_id == caller_pane_id if caller_pane_id else None,Bare equality. No check against _caller_is_on_server(server, caller) (which lives a few hundred lines up at _utils.py:147 and does do the socket comparison — realpath, then basename fallback). The 0.1.0a2 CHANGES entry reads "Caller-identity scoping: self-protection is scoped to the caller's tmux socket rather than global" — accurate, but "self-protection" is specifically the kill-path gate, not this annotation.
_serialize_pane is used everywhere: list_panes, split_window, create_session, the tmux:// resource tree in resources/hierarchy.py, and every other tool that returns PaneInfo / PaneSnapshot. So the false positive surfaces across most of the read-path tool surface.
Reproduction
Environment: libtmux-mcp==0.1.0a2. Caller is an MCP agent running inside TMUX_PANE=%1 on socket default (e.g. an agent invoked via Claude Code or Cursor from inside a tmux pane).
- Create an isolated tmux server and split it so pane
%1exists there too:tmux -L iscaller_repro new-session -ds repro tmux -L iscaller_repro split-window -t repro
- From the MCP client, call
list_paneswithsocket_name="iscaller_repro". - Observed: one of the returned panes has
is_caller: true, matching whichever pane happens to be numbered%1on the isolated server. - Expected: all panes on
iscaller_reprohaveis_caller: false(ornull) because the caller's actual pane%1is ondefault, not oniscaller_repro. - Tear down:
tmux -L iscaller_repro kill-server.
In my smoke test the 0.1.0a1 stable MCP surface (before the 0.1.0a2 self-kill scoping fix) produced exactly this false positive, which I initially mis-attributed to a fix in 0.1.0a2. A code read confirms the informational annotation path was never updated.
Suggested fix
_serialize_pane should consult _caller_is_on_server(server, _get_caller_identity()) before falling back to the pane-ID equality check. Something like:
def _serialize_pane(pane: Pane, server: Server | None = None) -> PaneInfo:
caller = _get_caller_identity()
on_server = _caller_is_on_server(server, caller) if server is not None else False
is_caller: bool | None
if caller is None:
is_caller = None
elif on_server and caller.pane_id == pane.pane_id:
is_caller = True
else:
is_caller = False
...The one wrinkle is that _serialize_pane today takes only a Pane. Either:
- Thread the
Serverthrough from every caller (larger refactor, many call sites), or - Recover it via
pane.server/pane.window.server(libtmux exposes this and most call sites already have a livePanewith a linked server).
The second option is the smaller change.
Tests
Extend tests/test_utils.py and/or tests/test_pane_tools.py with a test that:
- Starts two libtmux servers (
libtmux_test_A,libtmux_test_B) via the existing pytest fixtures. - Creates a pane on server B that matches the caller's
TMUX_PANE(e.g.%1). - Calls
_serialize_pane(orlist_panesend-to-end) against server B and assertsis_caller is False.
Mock or monkeypatch _get_caller_identity to return a deterministic caller if running outside tmux.
Related
CHANGES0.1.0a2 › Fixes › macOSTMUX_TMPDIRself-kill guard — the guarded code path.src/libtmux_mcp/_utils.py:147_caller_is_on_server— the correct socket-scoped comparator; reuse it here.- Affected tools (non-exhaustive):
list_panes,split_window,create_session,search_panes,snapshot_pane,select_pane,swap_pane,tmux://panes/<id>resource.
Environment
- libtmux-mcp: 0.1.0a2
- tmux: 3.6a
- Python: 3.13
- OS: Linux (WSL2)
Activity
fix(_utils): scope is_caller annotation to caller's socket
fix(_utils): scope is_caller annotation to caller's socket
docs(CHANGES): cite PR #22 instead of issue #19 on is_caller fix entry
fix(_utils): scope is_caller annotation to caller's socket
docs(CHANGES): cite PR #22 instead of issue #19 on is_caller fix entry
fix(_utils): scope is_caller annotation to caller's socket (#22)