Expose tmux spawn provenance: a typed libtmux.env (TmuxEnv, spawn_session, is_inside_tmux)
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 45/100
Research direction
Start by reading Server.from_env() in server.py and Pane.from_env() in pane.py to understand the existing environment parsing. Add the proposed libtmux.env public API with strict TMUX and TMUX_PANE handling, provenance fields, and session resolution. Done means the documented TmuxEnv, spawn_session(), and is_inside_tmux() behaviors are implemented without changing existing from_env() behavior.
Written by the indexing model from the issue text.
Description
Summary
libtmux can tell you where a process is now (the from_env() family resolves TMUX_PANE against the live server). It has no way to tell you where a process was spawned — the provenance tmux freezes into $TMUX at exec time. These are different facts, they can disagree, and today a caller who wants the second one has to hand-roll a parse of $TMUX that is easy to get wrong.
This proposes a small public module, libtmux.env, that types tmux's environment contract and exposes spawn provenance under its own name.
Background: what tmux actually exports, and when
tmux writes exactly two variables into the environment of every process it spawns inside a pane, and never revises either one afterwards.
TMUX is set in environ.c:277-282:
if (s != NULL)
idx = s->id;
else
idx = -1;
environ_set(env, "TMUX", 0, "%s,%ld,%d", socket_path, (long)getpid(), idx);
So TMUX is "<socket_path>,<server_pid>,<session_id>", where the session id is the bare integer (47, not $47 as libtmux spells it), and is -1 when the pane was spawned without a session.
TMUX_PANE is set in spawn.c:325:
environ_set(child, "TMUX_PANE", 0, "%%%u", new_wp->id);
Both are written once, at spawn. Grepping tmux 3.7b for every other reference to these names turns up only reads — cmd-find.c:93, server-client.c:228, tmux.c:491. tmux never reaches into a running child to update them.
The consequence: the session id in $TMUX is provenance, not location. Move the pane's window to another session (move-window) and that id is simply wrong about where the process is now. It may even name a session tmux has since destroyed.
Why libtmux should expose this, rather than leave it to callers
The from_env() family deliberately reads only the socket path out of $TMUX and resolves everything else from live tmux state — that is the right default, and it is why Session.from_env() can be trusted to answer "which session am I in now?".
But "which session launched me?" is a real, separate question, and there is no supported way to ask it. Callers who need it (a supervisor tagging work by originating session; an agent reporting where it was started; a tool that wants to detect that it has been moved) reach for os.environ["TMUX"] and parse it themselves. The naive parse is wrong in at least three ways:
split(",")instead ofrsplit(",", 2)— a socket path containing a comma is legal, and splits from the left into garbage.- No handling of
-1, so "spawned without a session" silently becomes the string"-1"and then a lookup failure. - No normalisation of the bare id to libtmux's
$-prefixed spelling, so it cannot be fed toSession.from_session_id()without a fixup the caller has to know about.
There is a fourth trap that is worse because it fails silently: TMUX_PANE must keep its % sigil. tmux's cmd_find_target routes a target by sigil (cmd-find.c:1078-1083 — $→session, @→window, %→pane), so a sigil-less "3" is interpreted as a pane index and cheerfully resolves to a different pane. A caller who strips or reconstructs the value gets a wrong answer with no error.
Every one of these is a parse libtmux already has to do internally. Making it public costs almost nothing and removes a footgun.
Proposed API
A new module, libtmux.env:
class TmuxEnv(t.NamedTuple):
"""tmux's environment contract, as frozen into this process at spawn."""
socket_path: str
"""Absolute path to the tmux server's socket."""
server_pid: int
"""PID of the tmux server process."""
spawn_session_id: str | None
"""Session this process was spawned in, e.g. ``"$47"``. ``None`` when tmux wrote ``-1``.
Provenance, not location. Makes no claim about the present: the session may since have been
renamed or killed, and it may no longer hold :attr:`pane_id`.
"""
pane_id: str
"""Pane this process runs in, e.g. ``"%3"``. Stable — a pane is never reparented into another pane."""
@classmethod
def from_env(cls, env: t.Mapping[str, str] | None = None) -> TmuxEnv: ...
@property
def server(self) -> Server:
"""Server named by the socket path. Runs no tmux command."""
def spawn_session(self) -> Session | None:
"""Session this process was spawned in, if it still exists.
``None`` when tmux wrote ``-1``, or when the spawn session has since been killed.
A server that is unreachable raises rather than collapsing to ``None`` — "server is gone"
and "session is gone" are different facts.
"""
def is_inside_tmux(env: t.Mapping[str, str] | None = None) -> bool:
"""True when this process runs inside a tmux pane."""
Design notes:
- Parsing is strict. A malformed
TMUX, or aTMUX_PANEwithout its%sigil, raisesNotInsideTmuxrather than being guessed at. Guessing here produces a silently wrong pane, which is the failure mode the module exists to prevent. spawn_session_idis a string id, not aSession. ASessionobject invites.windows/.active_paneand so looks like a location. A bare"$47"cannot be mistaken for one. Resolving it is opt-in viaspawn_session(), whose name still says "spawn" at the call site.str | None, not a sentinel. tmux's-1gets its own value in the type.- A module, not classmethods on
Server. These are not server facts. Hanging a frozen, possibly-stale value off the class whose whole job is live state would re-create the exact conflation this is meant to avoid. libtmux already splits concerns into small modules (neo,formats,constants,exc), andenvsits naturally beside them.
The parse itself is not net-new work — Server.from_env() and Pane.from_env() already need it. The only question is whether it stays private or becomes a supported surface.
Detecting that you have been moved
With the module in place, "am I still where I started?" is one honest line, and the caller can see what it costs:
env = TmuxEnv.from_env()
moved = env.spawn_session_id != Session.from_env().session_id
A dedicated is_stale() / moved() helper was considered and rejected: it hides three tmux round-trips behind a property that reads like an attribute.
Scope
Purely additive. No existing behaviour changes; from_env() keeps resolving through the live server and keeps ignoring the session field of $TMUX.
- Dominant language
- Python
- Stars
- 1.2k
- Forks
- 127
- Avg merge
- 2h 13m
- Merged PRs (30d)
- 1
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.
More from tmux-python/libtmux
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
tmux-python/libtmux#759 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
tmux-python/libtmux#745 · 2 comments ·
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
tmux-python/libtmux#744 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
tmux-python/libtmux#731 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
tmux-python/libtmux#654 ·
All issues in tmux-python/libtmux
Similar issues
-
essnmx good first issue
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
[Feature] 奇物选择添加优先级 Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
syfoud/Simulated_Scepter#174 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Giskard-AI/giskard-oss#2840 · 1 comment ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Openarea: repo bug perceived difficulty: 2
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
yeti-platform/yeti#1380 ·