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

readonly

List, capture, search, info

Monitoring, browsing

mutating (default)

mutating

+ create, send_keys, rename, resize

Normal agent workflow

destructive

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ΒΆ

  1. 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).

  2. 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.

Tool tagsΒΆ

Every tool is tagged with exactly one safety tier:

  • readonly readonly β€” Read-only operations that don’t modify tmux state

  • mutating mutating β€” Operations that create, modify, or send input to tmux objects

  • destructive destructive β€” Operations that destroy tmux objects (kill commands)

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:

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):

  1. Use Server.socket_path if libtmux already has it.

  2. 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 where TMUX_TMPDIR commonly differs between contexts.

  3. 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=readonly for untrusted MCP clients.

  • Audit log records (see below) capture the output_path argument 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 value argument 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 command via send_keys instead β€” 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_id is required (no fallback to β€œfirst pane in session/window”). Agents that pass only session_name get a ToolError instead of an unintended kill β€” resolve via list_panes readonly first.

  • Any shell argument is briefly visible in the OS process table and tmux’s pane_current_command metadata before the spawned shell takes over; the audit log redacts shell payloads (see below), but do not pass credentials directly even with redaction.

  • The optional environment argument (dict[str, str]) maps to one tmux -e KEY=VALUE flag per item. The audit log redacts each value via a {len, sha256_prefix} digest while keeping the keys visible β€” env var names like DATABASE_URL are usually operator-debug-useful, but their values are the secret. The same OS-process-table caveat as shell applies: respawn-pane -e DB_PASSWORD=... may briefly appear in ps output 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 name

  • outcome β€” ok or error, with error_type on failure

  • duration_ms

  • client_id / request_id β€” from the fastmcp context when available

  • args β€” a summary of arguments. Sensitive scalar keys (keys, text, value, content, shell) are replaced by {len, sha256_prefix}; the dict-shaped sensitive key environment keeps 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

list_sessions

readonly

true

false

true

get_server_info

readonly

true

false

true

list_windows

readonly

true

false

true

list_panes

readonly

true

false

true

capture_pane

readonly

true

false

true

get_pane_info

readonly

true

false

true

search_panes

readonly

true

false

true

wait_for_text

readonly

true

false

true

show_option

readonly

true

false

true

show_environment

readonly

true

false

true

create_session

mutating

false

false

false

create_window

mutating

false

false

false

split_window

mutating

false

false

false

send_keys

mutating

false

false

false

rename_session

mutating

false

false

true

rename_window

mutating

false

false

true

resize_pane

mutating

false

false

true

resize_window

mutating

false

false

true

set_pane_title

mutating

false

false

true

clear_pane

mutating

false

false

true

select_layout

mutating

false

false

true

set_option

mutating

false

false

true

set_environment

mutating

false

false

true

respawn_pane

mutating

false

true

false

kill_server

destructive

false

true

false

kill_session

destructive

false

true

false

kill_window

destructive

false

true

false

kill_pane

destructive

false

true

false