tmux-python / tmux-python/libtmux
No self-location API (TMUX_PANE), and the socket_path derivation in Server.__init__ is unreachable dead code
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.2k
- Forks
- 127
- Avg merge
- 2h 13m
- Merged PRs (30d)
- 1
Description
Filed against tmux-python/libtmux (v0.61.0). Found while building an MCP server on libtmux; both items force every downstream consumer to re-implement the same two things.
1. Server.__init__'s socket_path derivation is unreachable
The TMUX_TMPDIR-based socket_path auto-derivation block in Server.__init__ can never execute — its conditions are mutually contradictory. A few lines earlier the local is rebound (socket_name = self.socket_name or "default"), so self.socket_name is None implies the local socket_name == "default", and the two guards can never both hold.
Consequence: server.socket_path is always None unless the caller passed socket_path= explicitly. Verified: Server(socket_name="x").socket_path is None, and bare Server().socket_path is None.
This is why downstream code has to re-resolve the socket path by hand (re-deriving TMUX_TMPDIR + euid + socket name, then falling back to shelling out tmux display-message -p '#{socket_path}'). Every consumer that needs to prove which server it is talking to writes this twice.
Suggested fix: either repair the condition so the derivation runs, or delete the dead block and document that socket_path is populated only when supplied. Repairing it is better — a Server that knows its own socket path lets consumers do socket-identity checks without a subprocess.
2. There is no self-location API
TMUX_PANE does not appear anywhere in the package (verified by ripgrep over the whole tree at v0.61.0). There is no Server.from_env(), no current_pane(), no equivalent. The only place libtmux touches $TMUX at all is Server.new_session, which deletes it before shelling out (so a nested new-session doesn't inherit the parent server) and restores it in a finally — it never parses it.
So any process running inside a tmux pane — an agent, an MCP server, a CLI tool — must hand-roll:
pane_id = os.environ.get("TMUX_PANE") # "%65"
socket_path, server_pid, session_id = os.environ["TMUX"].split(",")
…and then find a way to turn that into a Pane. The obvious route is a trap:
| approach | cost | why |
|---|---|---|
Pane.from_pane_id(...) |
28.4 ms | delegates to neo.fetch_obj → full list-panes -a with the wide -F template, then a Python-side linear scan (neo.py#L787) |
server.panes.filter(pane_id=...) |
28.7 ms | QueryList.filter/get are pure-Python predicates over an already-enumerated list; they never push down to tmux (query_list.py#L513) |
Server.search_panes(filter=...) |
9.8 ms | does push tmux's native -f, but still runs list-panes -a with the wide template (server.py#L2563) |
Pane(server=srv, pane_id=...) + display_message |
3.3 ms | dataclass construction is free, then one targeted display-message |
The fast path works only because of two facts a caller has to discover: Pane is a dataclass whose only required field is server (neo.py#L389), and Pane.cmd auto-injects -t <pane_id> (pane.py#L214). Nothing signposts this, so the natural-looking API (from_pane_id) is ~9× slower than the right one.
Note also that Client.attached_pane / Session.active_window / Window.active_pane are not substitutes: they resolve "currently focused" via list-clients / pane_active=1, not "the pane this process is running in". A headless process has no client row at all.
Proposal
libtmux.Server.from_env() # parse $TMUX -> Server(socket_path=...) (no subprocess)
libtmux.current_pane() # read $TMUX_PANE -> Pane(server=..., pane_id=...) (no subprocess)
Both are a handful of lines, need no tmux round-trip, and would let every downstream consumer stop re-implementing the same env parse. Bonus: socket_path and pid are already universal format tokens declared on Obj (neo.py#L146), so a single display-message can return the server's socket path and PID — exactly the first two components of TMUX=<socketpath>,<pid>,<session> — giving a zero-extra-cost proof that a pane really belongs to the caller's server.
3. Two smaller papercuts found alongside
(a) Server.start_server() leaves a stale socket file. It runs tmux start-server, but with exit-empty on (the default) the sessionless daemon exits immediately — is_alive() still returns False, yet a stale socket file remains on disk and survives kill(). So socket-file existence is not a valid liveness test, and start_server() alone cannot "ensure a server".
Suggested: document this, or have start_server() verify liveness and raise/warn.
(b) Error-handling asymmetry between .sessions and .panes. Server.sessions and Server.clients swallow every LibTmuxException and return an empty QueryList — so a permission error is indistinguishable from "no sessions". Server.panes/Server.windows instead go through _fetch_or_empty, which swallows only daemon-not-up errors (classified by _is_daemon_not_up_error) and re-raises everything else. Confirmed: a "File name too long" socket error propagates out of .panes but not out of .sessions.
src/libtmux/server.py#L2372(swallow-all)src/libtmux/server.py#L79(_fetch_or_empty)
Suggested: route .sessions/.clients through _fetch_or_empty too, so emptiness means the same thing everywhere.
(c) _is_daemon_not_up_error is private but universally needed. It is the canonical classifier for both tmux phrasings ("no server running" and "error connecting to … (No such file or directory)"). Downstream consumers currently string-match independently. Consider exporting it, or a public ServerStatus enum alongside is_alive().
(d) EnvironmentMixin.set_environment raises a bare ValueError on tmux stderr, bypassing the LibTmuxException hierarchy. Downstream error mappers key on LibTmuxException, so a routine "no tmux server running" escapes as an unexpected error.
Environment
- libtmux: 0.61.0
- 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_iself-location primitive + self-first relational routing (root fix) - tmux-python/libtmux-mcp#94 — [bug] caller's socket advertised but never targeted;
TMUXenv 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); unreachablesocket_pathderivation
(← this issue: tmux-python/libtmux#704)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the referenced sections of src/libtmux/server.py, then inspect src/libtmux/neo.py, src/libtmux/pane.py, and src/libtmux/_internal/query_list.py to confirm the proposed self-location paths and socket derivation. Done means agreeing on the scope, implementing the selected APIs or fixes, and covering the documented environment, error-handling, and socket behaviors with tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, cli
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100