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, readonly batches

Monitoring, browsing

mutating (default)

mutating

+ create, send_keys, send_keys_batch, mutating batches, rename, resize

Normal agent workflow

destructive

destructive

+ destructive batches, 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 an ExpectedToolError 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 / send_keys_batch / 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 or send_keys_batch 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

capture_since

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

true

false

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