tmux MCP, built in Ruby
tmux terminal control for AI agents, built on libtmux for Ruby and MCP Ruby SDK. This server maps tmux's object hierarchy — sessions, windows, panes — into MCP tools. It offers only the tools selected when it starts.
tmux through MCP
tmux before the first call.
tmux after tmux_capabilities: unchanged, the call only reads.
tmux after tmux_create.
tmux after tmux_create.
tmux after tmux_snapshot: unchanged, the call only reads.
tmux after tmux_send.
tmux after tmux_wait.
tmux after tmux_send.
tmux after tmux_wait.
tmux after tmux_close.
Create a tmux session called checks, split its window into two panes, run pytest -q in one and show checks.log in the other, show me the results, then remove the session.
agentAsk the server which tools and limits are enabled.
Result
{ "ok": true, "data": { "endpoint": "docs", "server_identity": { "generation": "3f9a1c", "pid": 1119217, "start_time": 1794050582, "tmux_version": "3.8a" }, "enabled_tools": [ "tmux_capabilities", "tmux_snapshot", "tmux_send", "tmux_create", "… 4 more" ], "criteria_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:libtmux-ruby:where:1", "title": "Ruby libtmux criteria version 1", "description": "The Ruby decoder additionally enforces byte, depth and node limits, rejects duplicate keys and floating-point tokens for integer fields.", "oneOf": [ { "type": "object", "required": "…", "additionalProperties": false, "properties": "…" }, { "type": "object", "required": "…", "additionalProperties": false, "properties": "…" }, { "type": "object", "required": "…", "additionalProperties": false, "properties": "…" }, { "type": "object", "required": "…", "additionalProperties": false, "properties": "…" }, "… 1 more" ], "$defs": { "session.id": { "anyOf": "…" }, "session.name": { "anyOf": "…" }, "session.created": { "anyOf": "…" }, "session.attached": { "anyOf": "…" }, "session.window_count": { "anyOf": "…" }, "session": { "type": "object", "additionalProperties": false, "properties": "…" }, "window.id": { "anyOf": "…" }, "window.name": { "anyOf": "…" }, "window.width": { "anyOf": "…" }, "window.height": { "anyOf": "…" }, "window.pane_count": { "anyOf": "…" }, "window.layout": { "anyOf": "…" }, "…": "30 more" } }, "limits": { "max_captures": 16, "max_capture_bytes": 8388608, "capture_ttl_seconds": 30, "request_timeout_seconds": 30, "max_response_bytes": 1048576, "max_mutation_bytes": 65536, "max_observers": 16, "max_enrollments": 8, "default_enrollment_seconds": 60, "max_enrollment_seconds": 300, "max_script_bytes": 65536, "default_run_output_bytes": 65536, "…": "2 more" }, "owns_daemon": false, "resource_subscriptions": false, "observation": { "screen": "bounded_rows", "history_continuity": "unknown", "process_cursor": "conditional", "requirements": [ "tmux >= 3.3a", "64-bit Linux with peer pidfds (kernel >= 6.6)", "same PID namespace and matching procfs", "live pane process", "… 3 more" ], "wait_conditions": [ "screen_contains", "process_exit" ] }, "authored_run": { "availability": "conditional", "shell_profile": "zsh-5.9-zle", "enrollment": "explicit_source", "authorization": "exact_generation_at_queue_grant", "stdin": "closed", "persistent_shell_changes": false, "descendant_termination": "unobserved", "requirements": [ "tmux >= 3.3a", "64-bit Linux with peer pidfds (kernel >= 6.6)", "same PID namespace and matching procfs", "live pane process", "… 5 more" ] } }}agentCreate the session.
tmux_create {"kind":"session","name":"checks","argv":["sh"]} Result
{ "ok": true, "data": { "entity": { "generation": "3f9a1c", "kind": "session", "id": "$1" }, "created": [ { "generation": "3f9a1c", "kind": "session", "id": "$1" }, { "generation": "3f9a1c", "kind": "window", "id": "@1" }, { "generation": "3f9a1c", "kind": "pane", "id": "%1" } ], "delivery": "observed", "program_completion": "unobserved" }}agentSplit the pane the create result returned (%1) in two.
tmux_create {"kind":"pane","parent":{"kind":"pane","id":"%1","generation":"3f9a1c"},"direction":"horizontal","argv":["sh"]} Result
{ "ok": true, "data": { "entity": { "generation": "3f9a1c", "kind": "pane", "id": "%2" }, "created": [ { "generation": "3f9a1c", "kind": "pane", "id": "%2" } ], "delivery": "observed", "program_completion": "unobserved" }}agentRead the window's layout.
tmux_snapshot {"entity":"window"} Result
{ "ok": true, "data": { "capture_id": "a13829fbcf68c4d13192d49431427978", "server_identity": { "generation": "3f9a1c", "pid": 1119217, "start_time": 1794050582, "tmux_version": "3.8a" }, "entity": "window", "coverage": { "session": "complete", "window": "complete", "pane": "complete", "window_link": "complete", "client": "complete" }, "interval": { "clock": "monotonic_seconds", "started": 6950.386331011, "finished": 6950.398742721, "reads": 5 }, "items": [ { "kind": "window", "fields": { "id": "@0", "name": "cat", "width": 80, "height": 24, "paneCount": 1, "layout": "{\"V\":2,\"L\":{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"%0\"}}" }, "ref": { "generation": "3f9a1c", "kind": "window", "id": "@0" } }, { "kind": "window", "fields": { "id": "@1", "name": "sh", "width": 80, "height": 24, "paneCount": 2, "layout": "{\"V\":2,\"L\":{\"t\":\"h\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"c\":[{\"t\":\"p\",\"w\":40,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"%1\"},{\"t\":\"p\",\"w\":39,\"h\":24,\"x\":41,\"y\":0,\"i\":1,\"I\":\"%2\"}]}}" }, "ref": { "generation": "3f9a1c", "kind": "window", "id": "@1" } } ], "truncated": false }}agentType the command into the pane the create result returned (%1).
tmux_send {"target":{"kind":"pane","id":"%1","generation":"3f9a1c"},"input":{"type":"text","text":"pytest -q\n"}} Result
{ "ok": true, "data": { "target": { "generation": "3f9a1c", "kind": "pane", "id": "%1" }, "delivery": "observed", "client_exit_status": 0, "completion": "dispatch_only" }}agentWait for the test summary in the pane the create result returned (%1).
tmux_wait {"target":{"kind":"pane","id":"%1","generation":"3f9a1c"},"condition":{"type":"screen_contains","text":"2 passed"},"timeout":10} Result
{ "ok": true, "data": { "target": { "kind": "pane", "id": "%1", "generation": "3f9a1c" }, "condition": "screen_contains", "capture": { "mode": "snapshot", "target": { "kind": "pane", "id": "%1", "generation": "3f9a1c" }, "capture_id": "7614cc6ece6c7b3bbcb5119bb6291b2f", "process_generation": "23fb4e239db0000be47a27a2b54059d5", "rows": [ "$ pytest -q\n", ".. [100%]\n", "2 passed in 0.01s\n", "$ \n", "… 20 more" ], "encoding": "utf-8", "row_count": 24, "bytes": 93, "truncated": false, "history_continuity": "unknown", "scope": { "max_lines": 200, "max_bytes": 65536, "history_lines": 0 }, "interval": { "clock": "monotonic_seconds", "started": 6953.983863023, "finished": 6954.004227945 } } }}agentType the command into the new pane the split returned (%2).
tmux_send {"target":{"kind":"pane","id":"%2","generation":"3f9a1c"},"input":{"type":"text","text":"cat checks.log\n"}} Result
{ "ok": true, "data": { "target": { "generation": "3f9a1c", "kind": "pane", "id": "%2" }, "delivery": "observed", "client_exit_status": 0, "completion": "dispatch_only" }}agentWait for the last line of the log in the pane the split returned (%2).
tmux_wait {"target":{"kind":"pane","id":"%2","generation":"3f9a1c"},"condition":{"type":"screen_contains","text":"checks finished"},"timeout":10} Result
{ "ok": true, "data": { "target": { "kind": "pane", "id": "%2", "generation": "3f9a1c" }, "condition": "screen_contains", "capture": { "mode": "snapshot", "target": { "kind": "pane", "id": "%2", "generation": "3f9a1c" }, "capture_id": "14373ab34e565f19369f7e1bc9e0b210", "process_generation": "0dd4defa031cdb8636cd70dc67eede49", "rows": [ "$ cat checks.log\n", "2026-01-01 checks started\n", "2026-01-01 checks finished\n", "$ \n", "… 20 more" ], "encoding": "utf-8", "row_count": 24, "bytes": 93, "truncated": false, "history_continuity": "unknown", "scope": { "max_lines": 200, "max_bytes": 65536, "history_lines": 0 }, "interval": { "clock": "monotonic_seconds", "started": 6957.556795452, "finished": 6957.578571397 } } }}agentRemove the session by the id the create result returned ($1).
tmux_close {"target":{"kind":"session","id":"$1","generation":"3f9a1c"}} Result
{ "ok": true, "data": { "target": { "generation": "3f9a1c", "kind": "session", "id": "$1" }, "delivery": "observed", "client_exit_status": 0 }}Install
Choose your client and configuration scope, then run the command or paste the configuration.
With RubyGems installed:
$ claude mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint localThe server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.claude.json (this project)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.claude.json (all projects)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .mcp.json (in repo, version-controlled)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.claude.json (this project)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.claude.json (all projects)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .mcp.json (in repo, version-controlled)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: claude_desktop_config.json
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: claude_desktop_config.json
With RubyGems installed:
$ codex mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint localThe server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.codex/config.toml
With RubyGems installed:
Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .codex/config.toml (in repo)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.codex/config.toml
With Bundler installed:
Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .codex/config.toml (in repo)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.gemini/settings.json
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .gemini/settings.json (in repo)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.gemini/settings.json
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .gemini/settings.json (in repo)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .cursor/mcp.json (in repo)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.cursor/mcp.json
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: .cursor/mcp.json (in repo)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.cursor/mcp.json
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.grok/config.toml
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ./.grok/config.toml (in repo)
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.grok/config.toml
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ./.grok/config.toml (in repo)
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.gemini/config/mcp_config.json
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.gemini/config/mcp_config.json
With RubyGems installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.config/opencode/opencode.jsonc
With Bundler installed:
The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.
Config file: ~/.config/opencode/opencode.jsonc
libtmux-mcp exposes one existing tmux server over MCP standard input and
output. Requiring libtmux/mcp starts no server; the installed
libtmux-mcp executable owns the protocol transport and borrows the selected
tmux daemon.
The default catalog contains tmux_capabilities and tmux_snapshot.
Observation and mutation tools are opt-in, so a client cannot acquire them by
calling an undisclosed name.
Start here¶
- Install configures an MCP client to launch the Ruby server.
- Tools records the actual wire schemas and per-tool policy.
- Guides connect a client to a private tmux server.
- Topics explain captures, cursors, references, and tool policy.
- Examples run a client launcher or embed the MCP application.
- Language API documents the Ruby embedding surface.
The server requires Ruby 3.3 or newer. Strong process tracking for waits and authored runs requires tmux 3.3a or newer plus native process identity support. The MCP server guide covers the server’s own source documentation.
Tools
The Ruby server declares no toolsets, so its tools are listed together. Read each tool's page before enabling it.
-
tmux_capabilitiesDiscover the fixed endpoint, server binding, enabled tools, criteria schema and limits. -
tmux_captureRead exact-pane screen/history rows preserving LF. -
tmux_closeDestroy one exact session, window or pane. -
tmux_createCreate a session, window in an exact session, or split an exact pane using argv. -
tmux_runRun an authored POSIX script in an explicitly enrolled idle, empty zsh 5.9 editor. -
tmux_sendSend literal UTF-8 text or named keys to one exact pane. -
tmux_snapshotCapture ordered metadata and evaluate Ruby criteria locally. -
tmux_waitWait for observed literal screen text or the initial pane process exit.
Browse every tool, with its arguments, result and protocol data.