Safety tiersΒΆ
libtmux-mcp uses a three-tier safety system to control which tools are available to AI agents.
OverviewΒΆ
Tier |
Label |
Access |
Use case |
|---|---|---|---|
|
readonly |
List, capture, search, info |
Monitoring, browsing |
|
mutating |
+ create, send_keys, rename, resize |
Normal agent workflow |
|
destructive |
+ kill_server, kill_session, kill_window, kill_pane |
Full control |
ConfigurationΒΆ
Set the safety tier via the LIBTMUX_SAFETY environment variable:
{
"mcpServers": {
"libtmux": {
"command": "uvx",
"args": ["libtmux-mcp"],
"env": {
"LIBTMUX_SAFETY": "readonly"
}
}
}
}
How it worksΒΆ
Dual-layer gatingΒΆ
FastMCP tag visibility: Tools are tagged with their tier. Only tags at or below the configured tier are enabled via
mcp.enable(tags=..., only=True).Safety middleware: A secondary middleware layer hides tools from listings and blocks execution with clear error messages if a tool above the tier is somehow invoked.
Fail-closed designΒΆ
Tools without a recognized tier tag are denied by default. This prevents accidentally exposing new tools without explicit safety classification.
Self-kill protectionΒΆ
Destructive tools include safeguards against self-harm:
kill_serverdestructive refuses to run if the MCP server is inside the target serverkill_sessiondestructive refuses to kill the session containing the MCP panekill_windowdestructive refuses to kill the window containing the MCP panekill_panedestructive refuses to kill the pane running the MCP server
These protections read both the TMUX and TMUX_PANE environment variables that tmux injects into pane child processes. The TMUX value is formatted socket_path,server_pid,session_id β libtmux-mcp parses the socket path and compares it to the target serverβs so the guard only fires when the caller is actually on the same tmux server. A kill across unrelated sockets is allowed; a kill of the callerβs own pane/window/session/server is refused. If the callerβs socket canβt be determined (rare β TMUX_PANE set without TMUX), the guard errs on the side of blocking.
macOS TMUX_TMPDIR caveatΒΆ
The self-kill guard resolves the target serverβs socket path in three steps (_effective_socket_path in src/libtmux_mcp/_utils.py):
Use
Server.socket_pathif libtmux already has it.Otherwise query the running server via
display-message -p '#{socket_path}'β authoritative because tmux itself reports the path it is actually using, regardless of the MCP process environment. This closes the launchd-vs-interactive-shell gap on macOS whereTMUX_TMPDIRcommonly differs between contexts.Fall back to reconstruction from
TMUX_TMPDIR(or/tmp) + euid + socket name. Only reached when the target server is unreachable (not running), in which case no self-kill is possible anyway and_caller_is_on_serverβs None-socket branch blocks conservatively.
The structural fix shipped in 0.1.x; setting TMUX_TMPDIR explicitly is no longer required for the guard to work, though it remains a useful diagnostic when investigating mismatched-path bug reports.
Footguns inside the mutating tierΒΆ
Most mutating tools are bounded: resize_pane only resizes, rename_window only renames. A few have broader reach because tmux itself exposes broader reach. Treat these as elevated risk even though they share the default tier:
pipe_paneΒΆ
pipe_pane mutating pipes a paneβs output to a shell command that the server runs. In practice this means the caller chooses an arbitrary path or pipeline on the server host. There is no allow-list. Assume it can create files anywhere the server process can write.
Mitigations:
Run the server as an unprivileged user with a scoped home directory.
Consider
LIBTMUX_SAFETY=readonlyfor untrusted MCP clients.Audit log records (see below) capture the
output_pathargument so reviewers can spot unexpected destinations.
set_environmentΒΆ
set_environment mutating writes into tmuxβs global, session, or window environment. Those values propagate into every shell tmux spawns afterwards. An agent that writes PATH, LD_PRELOAD, or AWS_* variables can influence every future command on that scope β including commands the user runs directly, not just commands the agent issues.
Mitigations:
The audit log redacts the
valueargument to a{len, sha256_prefix}digest so log files donβt leak the secrets agents set, but operators should still treat the tool as high-privilege.If only a single command needs an env override, prefer having the agent invoke
env VAR=value commandviasend_keysinstead β the blast radius is one command, not every future child.
respawn_paneΒΆ
respawn_pane mutating restarts a paneβs process while preserving the pane id and layout β exactly what an agent wants when a shell wedges. Default kill=True terminates the running process before relaunch. The pane_id and layout are preserved (the point of the tool), but any unsaved REPL state, ssh session, or in-flight job in that pane is lost. Repeated calls are not idempotent β each call kills a new process.
Unlike other mutating tools, the registration carries destructiveHint=True and idempotentHint=False (via the ANNOTATIONS_MUTATING_DESTRUCTIVE preset) so MCP clients see honest annotations even though the tier tag stays at mutating for default-profile recovery.
Mitigations:
pane_idis required (no fallback to βfirst pane in session/windowβ). Agents that pass onlysession_nameget aToolErrorinstead of an unintended kill β resolve vialist_panesreadonly first.Any
shellargument is briefly visible in the OS process table and tmuxβspane_current_commandmetadata before the spawned shell takes over; the audit log redactsshellpayloads (see below), but do not pass credentials directly even with redaction.The optional
environmentargument (dict[str, str]) maps to one tmux-e KEY=VALUEflag per item. The audit log redacts each value via a{len, sha256_prefix}digest while keeping the keys visible β env var names likeDATABASE_URLare usually operator-debug-useful, but their values are the secret. The same OS-process-table caveat asshellapplies:respawn-pane -e DB_PASSWORD=...may briefly appear inpsoutput before the spawned process inherits the env.The same self-pane guard that protects the destructive kill commands also refuses to respawn the pane running the MCP server.
send_keys / paste_textΒΆ
These can execute anything the paneβs shell accepts. There is no payload validation. The audit log stores a digest of the content, not the content itself, so a secret typed via send_keys does not land in logs.
Audit logΒΆ
Every tool call emits one INFO record on the libtmux_mcp.audit logger carrying:
toolβ the tool nameoutcomeβokorerror, witherror_typeon failureduration_msclient_id/request_idβ from the fastmcp context when availableargsβ a summary of arguments. Sensitive scalar keys (keys,text,value,content,shell) are replaced by{len, sha256_prefix}; the dict-shaped sensitive keyenvironmentkeeps its keys but digests each value individually. Non-sensitive strings over 200 characters are truncated.
Route this logger to a dedicated sink if you want a durable audit trail; it is deliberately namespaced separately from the main libtmux_mcp logger.
Tool annotationsΒΆ
Each tool carries MCP tool annotations that hint at its behavior:
Tool |
Tier |
readOnly |
destructive |
idempotent |
|---|---|---|---|---|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
readonly |
true |
false |
true |
|
mutating |
false |
false |
false |
|
mutating |
false |
false |
false |
|
mutating |
false |
false |
false |
|
mutating |
false |
false |
false |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
false |
true |
|
mutating |
false |
true |
false |
|
destructive |
false |
true |
false |
|
destructive |
false |
true |
false |
|
destructive |
false |
true |
false |
|
destructive |
false |
true |
false |