A Model Context Protocol (MCP) server for tmux, written in Rust. It lets AI assistants create sessions, split panes, run commands, and capture output.
Requires tmux 3.x installed and available on PATH. The server checks the
version on startup and refuses to run against tmux 2.x, whose output formats and
split flags differ. CI runs integration tests against the tmux version provided
by ubuntu-latest; development is on 3.6.
[!WARNING] Using this MCP allows the agent to escape the sandbox and its security limitations. Here be dragons!
You can automate tmux with a plain-text skill, but the MCP tools are more reliable and cheaper to run:
execute-command + get-command-result yields attributable output and exit codes without screen scraping.cargo install tmux-mcp-rs
brew install bnomei/tmux-mcp/tmux-mcp-rs
Download a prebuilt archive from the GitHub Releases page, extract it, and place tmux-mcp-rs on your PATH.
git clone https://github.com/bnomei/tmux-mcp.git
cd tmux-mcp
cargo build --release
The raw input tools are gated behind Cargo features that are enabled by default:
interactive — the send-keys, send-hex, and paste-text tools.special-keys — the send-enter, send-tab, send-escape, arrow, page,
home/end, send-backspace, send-cancel, and send-eof tools.Because these tools can inject bytes straight into the PTY, the
command_filter is not a complete boundary for the feature set (for example, a
denied command can be typed character-by-character and then submitted with
Enter). Disabling a feature removes those tools from the binary entirely — they
are never registered, so an agent cannot call them and list-tools will not
advertise them.
# Filtered-only build: execute-command is the sole shell-input path.
cargo build --release --no-default-features --features rayon,rapidfuzz
# Keep raw keystrokes but drop the special-key helpers (or vice versa).
cargo build --release --no-default-features --features rayon,rapidfuzz,interactive
execute-command is always present and remains subject to the command_filter
allow/deny patterns, so a hardened build forces all shell input through the
validated path.
1) Add this MCP configuration. Examples for common MCP clients (pick one):
# Claude Code
claude mcp add --transport stdio tmux -- tmux-mcp-rs
# Codex CLI
codex mcp add tmux -- tmux-mcp-rs
# OpenCode (interactive)
opencode mcp add
# Amp (non-workspace)
amp mcp add tmux -- tmux-mcp-rs
{
"mcpServers": {
"tmux": {
"command": "tmux-mcp-rs"
}
}
}
2) Let the agent create its own tmux session (it will return the session id by default), or start one yourself if you want a pre-existing session (local, isolated socket, or remote over SSH). 3) Optional: attach to watch the agent work:
tmux attach -t <session>
Add the Quick Start snippet to your MCP client config. Example below includes all supported args (remove the ones you don't need):
{
"mcpServers": {
"tmux": {
"command": "tmux-mcp-rs",
"args": [
"--shell-type",
"zsh",
"--socket",
"/path/to/tmux.sock",
"--ssh",
"user@host",
"--config",
"/path/to/config.toml"
]
}
}
}
| Option | Description | Default |
|---|---|---|
--shell-type <SHELL> |
Shell to use (bash, zsh, fish) | bash |
--socket <PATH> |
Path to tmux server socket (recommend per-agent isolated socket id) | Default server (if unset) |
--ssh <CONNECTION> |
Run tmux over SSH (options + destination, destination last) | None |
--config <PATH> |
Path to TOML configuration file | None |
Environment variables: TMUX_MCP_SOCKET can also set the socket path (recommend per-agent isolated socket id). TMUX_MCP_SSH can set the SSH connection string and is parsed with the same startup validation as --ssh. TMUX_MCP_TOOLS can override the configured tool surface for one process.
[shell]
type = "zsh"
[ssh]
remote = "user@host"
[security]
enabled = true
allow_execute_command = true
allowed_sockets = ["/tmp/ai-agent.sock"]
allowed_sessions = ["workspace"]
allowed_panes = ["%1"]
[security.command_filter]
mode = "off" # off | allowlist | denylist
patterns = []
[security.tools]
mode = "deny" # deny | allow
items = [] # tool names or groups such as "@raw-input"
[tracking]
capture_initial_lines = 1000
capture_max_lines = 16000
capture_backoff_factor = 2
completed_retention_minutes = 240
completed_max_entries = 1000
tracking_deadline_seconds = 600
[search]
streaming_threshold_bytes = 262144
capture_initial_lines: initial number of lines to capture for command output.capture_max_lines: maximum lines to capture before giving up.capture_backoff_factor: multiplier for each capture retry window.completed_retention_minutes: age threshold for evicting completed command history.completed_max_entries: max number of completed commands retained.tracking_deadline_seconds: how long a command whose START marker has scrolled out of reach (very large output) stays Pending before being declared expired. The DONE marker still completes it at any time; this only bounds genuinely-lost tracking. Raise it for high-output commands that also run long.streaming_threshold_bytes: when a buffer exceeds this size, search streams a window via a temp file instead of loading the full buffer in memory.Use [security.tools] to remove tools from list-tools and deny direct calls.
Entries can be exact tool names or groups prefixed with @.
# Disable raw PTY input while keeping execute-command available.
[security.tools]
mode = "deny"
items = ["@raw-input"]
# Expose only read-only context tools plus tracked command execution.
[security.tools]
mode = "allow"
items = ["@read", "execute-command"]
TMUX_MCP_TOOLS overrides [security.tools] for a single process. Without a
prefix it is a denylist; use allow: for an explicit allowlist.
TMUX_MCP_TOOLS=send-keys,paste-text tmux-mcp-rs
TMUX_MCP_TOOLS=deny:@raw-input tmux-mcp-rs
TMUX_MCP_TOOLS=allow:@read,execute-command tmux-mcp-rs
Known groups:
| Group | Tools |
|---|---|
@all |
Every known tool compiled into the binary |
@read |
Read-only/introspection tools, including list/find, capture, buffer reads, and command result reads |
@list |
list-*, find-session, and get-current-session |
@execute |
execute-command, get-command-result |
@raw-input |
All raw PTY input tools from @interactive and @special-keys |
@interactive |
send-keys, send-hex, paste-text |
@special-keys |
send-enter, arrows, page/home/end, tab, escape, backspace, cancel, EOF |
@capture |
capture-pane |
@buffer-read |
list-buffers, show-buffer, search-buffer, subsearch-buffer |
@buffer-write |
save-buffer, load-buffer, delete-buffer, set-buffer, append-buffer, rename-buffer |
@create |
create-session, create-window |
@split |
split-pane |
@rename |
rename-session, rename-window, rename-pane |
@move |
focus, resize, zoom, layout, join, break, swap, move, synchronize-panes |
@kill |
kill-session, kill-window, kill-pane, detach-client |
@socket |
socket-for-path |
literal=true for exact text and enter=true to submit in one callpaste-buffer -p); embedded newlines stay literal (no per-line submit) when the receiving program supports bracketed paste1b 5b 31 33 3b 32 75 = Shift+Enter) for escape sequences key names cannot expressThe server exposes the following MCP resources:
| URI | Description |
|---|---|
tmux://server/info |
Default socket and SSH context for routing tool calls |
tmux://pane/{paneId} |
Content of a specific pane (last 200 lines) |
tmux://pane/{paneId}/info |
Metadata for a specific pane |
tmux://pane/{paneId}/tail/{lines} |
Tail N lines from a pane |
tmux://pane/{paneId}/tail/{lines}/ansi |
Tail N lines with ANSI colors |
tmux://window/{windowId}/info |
Metadata for a window |
tmux://session/{sessionId}/tree |
Session + windows + panes snapshot |
tmux://clients |
List tmux clients |
tmux://command/{commandId}/result |
Status and output of a tracked command |
Resources are dynamically enumerated - the server lists available panes, windows, sessions, clients, and active commands.
$ claude mcp add tmux-mcp \
-- python -m otcore.mcp_server <graph>