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, readonly batches |
Monitoring, browsing |
|
mutating |
+ create, |
Normal agent workflow |
|
destructive |
+ destructive batches, |
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
libtmux.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:
Piping pane outputΒΆ
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.
Setting tmux 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 server audit record replaces the
valueargument with a{len, sha256_prefix}digest, so the value does not appear verbatim inlibtmux_mcp.audit. That redaction does not cover separate library, process, application, or client logs, so operators should still treat the tool as high-privilege.If only a single command needs a non-sensitive env override, prefer having the agent invoke
env VAR=value commandviasend_keysinstead β the blast radius is one command, not every future child. For credentials, pass a reference that the child resolves instead of a literal value through tmux.
Respawning panesΒΆ
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 anExpectedToolErrorinstead 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 accepts either a mapping of string keys and values or a JSON object string, then maps each item to one tmux-e KEY=VALUEflag. For a mapping, the audit log keeps each key visible and replaces each value with a{len, sha256_prefix}digest. A JSON string is redacted as one scalar digest, so its keys are not retained in the audit record. 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.
Raw pane inputΒΆ
These can execute anything the paneβs shell accepts. There is no payload validation. The server audit log stores a digest of the content, not the content itself, so a secret typed via send_keys or send_keys_batch does not land in that audit record.
History suppression is not secret transportΒΆ
suppress_history on run_command asks the current shell not to persist one space-prefixed command event. suppress_persistent_history=true on the four spawn tools adds best-effort no-disk controls to a new environment. Shell behavior and startup files can defeat either request. History suppression does not isolate the process, does not clear in-memory history or scrollback, and does not hide the command from other observation surfaces:
pane echo and scrollback: the terminal can display input, tmux can retain it in pane history, and an attached terminal can keep its own scrollback.
capture tools and piping:
capture_pane,capture_since,snapshot_pane,search_panes, andpipe_panecan return or route displayed and retained text.hooks: configured tmux hooks, including state visible through
show_hooks, and shell instrumentation can observe process or pane activity independently of shell history.process visibility: command arguments and launch strings can appear in the tmux client argv. Environment values passed to
create_session,create_window,split_window, andrespawn_panecan also remain in a child process environment;create_sessionretains them in tmux session state for future panes, whereshow_environmentcan reveal them. MCP audit redaction does not hide any of these surfaces from host process or tmux environment inspection.MCP client transcripts: clients can retain the original request and response outside the serverβs control.
logs:
libtmux_mcp.auditrecords redacted arguments and whether the call succeeded or raised; it does not contain tool return values. Redaction applies only to these audit records and does not rewrite separate records emitted by libtmux, FastMCP, shells, or MCP clients. libtmux DEBUG or error records may contain shell-joined tmux arguments, while MCP client request logs and application logs remain outside the serverβs guarantee.
Prefer credential references that a process resolves from a secret manager, scoped file descriptor, or preconfigured host lookup. Avoid literal credentials in command, raw keys or text, shell, and environment arguments; history suppression cannot retract a value after another surface records it.
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,command,value,content,shell, and string-formenvironment) are replaced by{len, sha256_prefix}. Mapping-formenvironmentkeeps 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 |
|
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 |
true |
false |
|
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 |