Repository navigation
Description
Filed against tmux-python/libtmux v0.62.0. Found while writing tested documentation examples that isolate tmux under a temporary TMUX_TMPDIR.
What happens
with Server(...) as server: kills the server on exit whenever it is alive, unconditionally. There is no kill_on_exit=False, no "only kill what I started", and no way to use the context manager purely for scoping.
src/libtmux/server.py#L267-L285—__exit__
if self.is_alive():
self.kill()A Server is a handle, not a connection, and the same handle addresses a server whether or not this process started it. So the context manager cannot tell "the server I just booted" from "the server the user has been working in all day", and it destroys both.
The sharpest form: with Server() as server: — no arguments, so the default socket — kills the reader's running tmux and every session in it. That is a plausible thing to write after reading the class docstring, and the failure is silent and total.
Recreation
import os, tempfile
os.environ["TMUX_TMPDIR"] = tempfile.mkdtemp(prefix="repro-")
from libtmux.server import Server
Server(socket_name="preexisting").new_session(session_name="important-work")
print("before:", [s.session_name for s in Server(socket_name="preexisting").sessions])
with Server(socket_name="preexisting") as server:
print("inside:", server.is_alive())
print("after :", Server(socket_name="preexisting").is_alive(), "<- the session is gone")Observed on libtmux v0.62.0 / tmux 3.7b:
before: ['important-work']
inside: True
after : False <- the session is gone
The with block did nothing except enter and exit, and a session it never created is gone. Drop the TMUX_TMPDIR line and the same script destroys the real default server.
Note on the sibling context managers
Session, Window and Pane have the same shape — __exit__ kills the object if it still exists — but the blast radius there is bounded by what the object is, and killing a session you were handed is at least proportionate. A server is different in kind: it is shared, process-wide, and typically not yours.
Documented in docs/topics/context_managers.md, which describes the kill-on-exit behaviour but not the shared-server hazard.
What a fix needs
- Add
kill_on_exit: bool = TruetoServer.__init__, honoured by__exit__. Keeps the current default, gives callers an out. Cheapest change, no behaviour break. - Better, if a break is acceptable in a pre-1.0 minor: only kill a server this handle started.
Serveralready knows whether it booted the daemon —new_sessionis the call that starts one — so record that and have__exit__respect it. This makes the destructive path opt-in by construction rather than by remembering a keyword. - Either way: document the hazard on
Server.__enter__/__exit__and indocs/topics/context_managers.md, in the same terms as the recreation above, and add a regression test that a pre-existing server survives awithblock.
Option 2 with a kill_on_exit=True escape hatch would cover both audiences. If neither is wanted, the minimum is a loud warning in the docstring — the current one says "killing the server if it exists" without saying whose.
Delivery contract from the rmux helper audit
Keep the destructive context-manager defect as its own fix. Review #728 against the chosen compatibility policy before treating it as resolved.
New owned-resource helpers must carry positive creation ownership and preserve borrowed objects. They must not infer ownership from the fact that a Server handle was constructed inside a context manager.
Acceptance for this bug remains narrow: an existing server and its sessions survive a scope the caller did not authorize to destroy them. Do not expand this ticket into the full workspace or lease protocol.
Delivery links: #804, #805. Track this work under #629 in the libtmux project.
Independent PR delivery
Follow the minimal slice and shared compatibility contract in #629. Build and test against master with every other unmerged feature branch absent. Cross-links above identify semantic contracts and future consumers; use a private adapter where needed instead of introducing a merge dependency. Preserve existing facade behavior and the inspected engine-ops interfaces. Leave deferred acceptance criteria open after an MVP lands.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIn Progress
Activity
[-]Server.__exit__ kills any live server, including one it did not start[/-][+]`Server.__exit__` kills any live server, including one it did not start[/+]Server(fix[__exit__]): Kill only when asked
Server(fix[__exit__]): Kill only when asked
Server(fix[__exit__]): Kill only when asked
Server(fix[__exit__]): Kill only when asked
tony commentedon Nov 1, 2026
The minimal borrowed-server correction is already covered by PR #728; this pass will reuse it rather than open a duplicate.
I replayed that PR's four-file change onto current master and verified:
just build-docsgate passes. An extra strict Sphinx diagnostic reports unresolved references; unchanged master also reports reference warnings and exceeded that diagnostic’s bound. The strict diagnostic is not the project documentation gate.Evidence is scoped to Python 3.15 on Linux/WSL with the local tmux 3.8a build. macOS was not tested and no macOS work is added. PR #728 remains unmerged; keep this implementation issue open.
Source and regression: ownership change, context tests. Baseline: current master source.