# MCP Clients
Source: https://libtmux-mcp.git-pull.com/clients/
(clients)=
# MCP Clients
Pick your client, install method, and config scope below — the snippet
updates accordingly. The scope row appears only for clients with more
than one scope (Claude Desktop is always user-level so it has no scope
row). The full table of file locations is at the bottom of the page.
```{mcp-install}
:variant: full
```
If your client isn't listed, any tool supporting MCP stdio transport
will work with the JSON config pattern shown for Claude Desktop or
Cursor. See {ref}`migration` for the recommended `tmux` registration
slug (existing `libtmux` registrations keep working).
## MCP Inspector
For testing and debugging:
```console
$ npx @modelcontextprotocol/inspector
```
## Config file locations
| Client | Config file | Format |
|--------|-------------|--------|
| Claude Code | `.mcp.json` (project) or `~/.claude.json` (local/user) | JSON |
| Claude Desktop | `claude_desktop_config.json` | JSON |
| Codex CLI | `~/.codex/config.toml` (user) or `.codex/config.toml` (project, manual) | TOML |
| Gemini CLI | `~/.gemini/settings.json` (user) or `.gemini/settings.json` (project) | JSON |
| Cursor | `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) | JSON |
## Local checkout (development)
For live development, point your client at a local checkout via `uv --directory`:
**Claude Code:**
```console
$ claude mcp add \
--scope user \
tmux -- \
uv --directory ~/work/python/libtmux-mcp \
run libtmux-mcp
```
Codex CLI / Gemini CLI / Cursor
**Codex CLI:**
```console
$ codex mcp add tmux -- \
uv --directory ~/work/python/libtmux-mcp \
run libtmux-mcp
```
**Gemini CLI:**
```console
$ gemini mcp add \
--scope user \
tmux uv -- \
--directory ~/work/python/libtmux-mcp \
run libtmux-mcp
```
**Cursor** — add to `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"tmux": {
"command": "uv",
"args": [
"--directory", "~/work/python/libtmux-mcp",
"run", "libtmux-mcp"
]
}
}
}
```
## Common pitfalls
- **Absolute paths**: Some clients require absolute paths in config. Use `$HOME/...` or the full path instead of `~/...`.
- **Virtual environments**: If using pip install, ensure the venv is activated or the `libtmux-mcp` binary is on your PATH.
- **Socket isolation**: Set `LIBTMUX_SOCKET` in the `env` block to isolate the MCP server from your default tmux. See {ref}`configuration`.
---
# Configuration
Source: https://libtmux-mcp.git-pull.com/configuration/
(configuration)=
# Configuration
Runtime configuration for the libtmux-mcp server. For MCP client setup, see {ref}`clients`.
## Environment variables
```{envvar} LIBTMUX_SOCKET
```
tmux socket name (`-L`). Isolates the MCP server to a specific tmux socket.
- **Type:** string
- **Default:** (none — uses the default tmux socket)
```{envvar} LIBTMUX_SOCKET_PATH
```
tmux socket path (`-S`). Alternative to socket name for custom socket locations.
- **Type:** string
- **Default:** (none)
```{envvar} LIBTMUX_TMUX_BIN
```
Path to tmux binary. Useful for testing with different tmux versions.
- **Type:** string
- **Default:** `tmux`
```{envvar} LIBTMUX_SAFETY
```
Safety tier controlling which tools are available. See {ref}`safety`.
- **Type:** string
- **Default:** `mutating`
- **Values:** `readonly`, `mutating`, `destructive`
## Setting environment variables
Set environment variables in your MCP client config:
```json
{
"mcpServers": {
"libtmux": {
"command": "uvx",
"args": ["libtmux-mcp"],
"env": {
"LIBTMUX_SOCKET": "ai_workspace",
"LIBTMUX_SAFETY": "readonly"
}
}
}
}
```
## Socket isolation
By default, the MCP server connects to the default tmux socket. Set {envvar}`LIBTMUX_SOCKET` to isolate AI agent activity from your personal tmux sessions:
```json
"env": { "LIBTMUX_SOCKET": "ai_workspace" }
```
The agent will only see sessions on the `ai_workspace` socket, not your personal sessions.
## All tools accept `socket_name`
Every tool accepts an optional `socket_name` parameter that overrides {envvar}`LIBTMUX_SOCKET` for that call. This allows agents to work across multiple tmux servers in a single session.
---
# Badge & Role Demo
Source: https://libtmux-mcp.git-pull.com/demo/
---
orphan: true
---
# Badge & Role Demo
A showcase of the custom Sphinx roles and visual elements available in libtmux-mcp documentation.
## Safety badges
Standalone badges via `{badge}`:
- {badge}`readonly` — green, read-only operations
- {badge}`mutating` — amber, state-changing operations
- {badge}`destructive` — red, irreversible operations
## Tool references
### `{tool}` — code-linked with badge
{tool}`capture-pane` · {tool}`capture-since` · {tool}`send-keys` · {tool}`search-panes` · {tool}`wait-for-text` · {tool}`kill-pane` · {tool}`create-session` · {tool}`split-window`
### `{toolref}` — code-linked, no badge
{toolref}`capture-pane` · {toolref}`capture-since` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window`
### `{tooliconl}` — icon left, outside code
{tooliconl}`capture-pane` · {tooliconl}`capture-since` · {tooliconl}`send-keys` · {tooliconl}`search-panes` · {tooliconl}`wait-for-text` · {tooliconl}`kill-pane` · {tooliconl}`create-session` · {tooliconl}`split-window`
### `{tooliconr}` — icon right, outside code
{tooliconr}`capture-pane` · {tooliconr}`capture-since` · {tooliconr}`send-keys` · {tooliconr}`search-panes` · {tooliconr}`wait-for-text` · {tooliconr}`kill-pane` · {tooliconr}`create-session` · {tooliconr}`split-window`
### `{tooliconil}` — icon inline-left, inside code
{tooliconil}`capture-pane` · {tooliconil}`capture-since` · {tooliconil}`send-keys` · {tooliconil}`search-panes` · {tooliconil}`wait-for-text` · {tooliconil}`kill-pane` · {tooliconil}`create-session` · {tooliconil}`split-window`
### `{tooliconir}` — icon inline-right, inside code
{tooliconir}`capture-pane` · {tooliconir}`capture-since` · {tooliconir}`send-keys` · {tooliconir}`search-panes` · {tooliconir}`wait-for-text` · {tooliconir}`kill-pane` · {tooliconir}`create-session` · {tooliconir}`split-window`
### `{ref}` — plain text link
{ref}`capture-pane` · {ref}`capture-since` · {ref}`send-keys` · {ref}`search-panes` · {ref}`wait-for-text` · {ref}`kill-pane` · {ref}`create-session` · {ref}`split-window`
## Badges in context
### In a heading
These are the actual tool headings as they render on tool pages:
> `capture_pane` {badge}`readonly`
> `split_window` {badge}`mutating`
> `kill_session` {badge}`destructive`
### In a table
| Tool | Tier | Description |
|------|------|-------------|
| {toolref}`list-sessions` | {badge}`readonly` | List all sessions |
| {toolref}`send-keys` | {badge}`mutating` | Send commands to a pane |
| {toolref}`kill-pane` | {badge}`destructive` | Destroy a pane |
### In prose
Use {tooliconl}`search-panes` to find text across all panes. If you know which pane, use {tooliconl}`capture-pane` for one read or {tooliconl}`capture-since` for repeated observation. After running a command with {tooliconl}`send-keys`, compose `tmux wait-for -S` and call {tooliconl}`wait-for-channel` before capturing.
### Dense inline (toolref, no badges)
The fundamental pattern: {toolref}`send-keys` → {toolref}`wait-for-channel` → {toolref}`capture-pane`. For discovery: {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`get-pane-info`.
## Environment variable references
{envvar}`LIBTMUX_SOCKET` · {envvar}`LIBTMUX_SAFETY` · {envvar}`LIBTMUX_SOCKET_PATH` · {envvar}`LIBTMUX_TMUX_BIN`
## Glossary terms
{term}`SIGINT` · {term}`SIGQUIT` · {term}`MCP` · {term}`Safety tier` · {term}`Pane` · {term}`Session`
## Admonitions
```{tip}
Use {tooliconl}`search-panes` before {tooliconl}`capture-pane` when you don't know which pane has the output you need.
```
```{warning}
Do not call {toolref}`capture-pane` immediately after {toolref}`send-keys` — there is a race condition. Compose `tmux wait-for -S` into the command and use {toolref}`wait-for-channel` between them.
```
```{note}
All tools accept an optional `socket_name` parameter for multi-server support.
```
## Badge anatomy
Each badge renders as:
```html
🔍 readonly
```
Features:
- **Emoji icon** — 🔍 readonly, ✏️ mutating, 💣 destructive (native system emoji, no filters)
- **Matte colors** — forest green, smoky amber, matte crimson with 1px border
- **Accessible** — `role="note"` + `aria-label` for screen readers
- **Non-selectable** — `user-select: none` so copying tool names skips badge text
- **Context-aware sizing** — slightly larger in headings, smaller inline
- **Sidebar compression** — badges collapse to colored dots in the right-side TOC
- **Heading flex** — `h2/h3/h4:has(.sd-badge)` centers badge against cap-height
---
# Glossary
Source: https://libtmux-mcp.git-pull.com/glossary/
(glossary)=
# Glossary
```{glossary}
MCP
Model Context Protocol. A standard for AI agents to interact with tools and resources.
FastMCP
A Python framework for building MCP servers. libtmux-mcp uses FastMCP to expose tmux operations as MCP tools.
libtmux
A typed Python library that provides an ORM wrapper for tmux. libtmux-mcp depends on libtmux for all tmux interactions.
tmux
A terminal multiplexer. It lets you switch easily between several programs in one terminal, detach them, and reattach them to a different terminal. See https://github.com/tmux/tmux.
Server
A tmux server instance. Manages sessions and communicates via a socket.
Session
A tmux session. Contains one or more windows. Has a name and ID (e.g. `$1`).
Window
A tmux window within a session. Contains one or more panes. Has a name, index, and ID (e.g. `@1`).
Pane
A tmux pane within a window. A pseudoterminal that runs a single process. Has an ID (e.g. `%1`) that is globally unique within a server.
Safety tier
A level controlling which MCP tools are available: `readonly`, `mutating`, or `destructive`. Set via the {envvar}`LIBTMUX_SAFETY` env var.
Socket
The Unix socket used to communicate with a tmux server. Can be specified by name (`-L`) or path (`-S`).
SIGINT
Interrupt signal (Ctrl-C). Sent via {toolref}`send-keys` with `keys: "C-c"` and `enter: false`. Most processes terminate gracefully on SIGINT.
SIGQUIT
Quit signal (Ctrl-\\). Sent via {toolref}`send-keys` with `keys: "C-\\"` and `enter: false`. Stronger than {term}`SIGINT` — may produce a core dump on Unix. Use as an escalation when SIGINT is ignored.
```
---
# Changelog
Source: https://libtmux-mcp.git-pull.com/history/
(changes)=
(changelog)=
(history)=
```{currentmodule} libtmux_mcp
```
```{include} ../CHANGES
:parser: markdown
```
---
# libtmux-mcp
Source: https://libtmux-mcp.git-pull.com/
(index)=
# libtmux-mcp
Terminal control for AI agents, built on [libtmux](https://libtmux.git-pull.com) and [FastMCP](https://gofastmcp.com).
This server maps tmux's object hierarchy — sessions, windows, panes — into MCP tools. Some tools read state. Some mutate it. Some destroy. The distinction is explicit and enforced.
```{warning}
**Pre-alpha.** APIs may change. [Feedback welcome](https://github.com/tmux-python/libtmux-mcp/issues).
```
```{mcp-install}
:variant: compact
```
---
::::{grid} 1 1 2 2
:gutter: 3
:::{grid-item-card} Quickstart
:link: quickstart
:link-type: doc
Install, connect, get a first result. Under 2 minutes.
:::
:::{grid-item-card} Tools
:link: tools/index
:link-type: doc
Every tool, grouped by intent and safety tier.
:::
:::{grid-item-card} Prompts
:link: prompts
:link-type: doc
Four workflow recipes the client renders for the model.
:::
:::{grid-item-card} Resources
:link: resources
:link-type: doc
Snapshot views of the tmux hierarchy via `tmux://` URIs.
:::
:::{grid-item-card} Safety tiers
:link: topics/safety
:link-type: doc
Readonly, mutating, destructive. Know what changes state.
:::
:::{grid-item-card} Client setup
:link: clients
:link-type: doc
Config blocks for Claude Desktop, Claude Code, Cursor, and others.
:::
::::
---
## What you can do
### Inspect (readonly)
Read tmux state without changing anything.
{toolref}`list-sessions` · {toolref}`capture-pane` · {toolref}`capture-since` · {toolref}`snapshot-pane` · {toolref}`get-pane-info` · {toolref}`find-pane-by-position` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`wait-for-content-change` · {toolref}`display-message`
### Act (mutating)
Create or modify tmux objects.
{toolref}`create-session` · {toolref}`send-keys` · {toolref}`paste-text` · {toolref}`create-window` · {toolref}`split-window` · {toolref}`select-pane` · {toolref}`select-window` · {toolref}`move-window` · {toolref}`resize-pane` · {toolref}`pipe-pane` · {toolref}`set-option`
### Destroy (destructive)
Tear down tmux objects. Not reversible.
{toolref}`kill-session` · {toolref}`kill-window` · {toolref}`kill-pane` · {toolref}`kill-server`
[Browse all tools →](tools/index)
---
## Mental model
- **Object hierarchy** — sessions contain windows, windows contain panes ({doc}`topics/concepts`)
- **Read vs. mutate** — some tools observe, some act, some destroy ({doc}`topics/safety`)
- **tmux is the source of truth** — the server reads from it and writes to it, never caches or abstracts
---
```{toctree}
:hidden:
:caption: Get started
quickstart
installation
clients
```
```{toctree}
:hidden:
:caption: Use it
tools/index
prompts
resources
recipes
configuration
```
```{toctree}
:hidden:
:caption: Understand it
topics/index
```
```{toctree}
:hidden:
:caption: Reference
reference/api/index
reference/compatibility
glossary
```
```{toctree}
:hidden:
:caption: Project
project/index
history
migration
GitHub
```
---
# Installation
Source: https://libtmux-mcp.git-pull.com/installation/
(installation)=
# Installation
## Requirements
- Python 3.10+
- tmux >= 3.2a
- [uv](https://github.com/astral-sh/uv) ([install](https://docs.astral.sh/uv/getting-started/installation/)) or [pipx](https://github.com/pypa/pipx) ([install](https://pipx.pypa.io/stable/installation/)) — for running without a persistent install
## Run without installing
No persistent install needed — run directly with a package executor:
`````{tab} uvx
```console
$ uvx libtmux-mcp
```
`````
`````{tab} pipx
```console
$ pipx run libtmux-mcp
```
`````
To wire it into your MCP client, see {ref}`clients`.
## Install the package
`````{tab} uv
```console
$ uv pip install libtmux-mcp
```
`````
`````{tab} pip
```console
$ pip install libtmux-mcp
```
`````
## Development install
Install [uv](https://github.com/astral-sh/uv) ([install](https://docs.astral.sh/uv/getting-started/installation/)), then clone and install in editable mode:
```console
$ git clone https://github.com/tmux-python/libtmux-mcp.git
```
```console
$ cd libtmux-mcp
```
```console
$ uv pip install -e "."
```
Code changes take effect immediately — no reinstall needed.
## Running the server
```console
$ libtmux-mcp
```
Or via Python module:
```console
$ python -m libtmux_mcp
```
## Upgrading
`````{tab} uv
```console
$ uv pip install --upgrade libtmux-mcp
```
`````
`````{tab} pip
```console
$ pip install --upgrade libtmux-mcp
```
`````
With `uvx` or `pipx run`, you always get the latest version automatically.
---
# Migration notes
Source: https://libtmux-mcp.git-pull.com/migration/
(migration)=
```{include} ../MIGRATION
```
---
# Code style
Source: https://libtmux-mcp.git-pull.com/project/code-style/
(code-style)=
# Code style
## Linting and formatting
libtmux-mcp uses [ruff](https://docs.astral.sh/ruff/) for linting and formatting:
```console
$ uv run ruff check .
```
```console
$ uv run ruff format .
```
## Type checking
[mypy](https://mypy-lang.org/) with strict mode:
```console
$ uv run mypy
```
## Docstrings
NumPy-style docstrings throughout.
## Imports
- `from __future__ import annotations` at the top of every file.
- `import typing as t` and access via namespace.
- Standard-library modules imported by namespace (`import pathlib`, not `from pathlib import Path`).
---
# Development
Source: https://libtmux-mcp.git-pull.com/project/contributing/
# Development
Install [git] and [uv] ([install](https://docs.astral.sh/uv/getting-started/installation/))
[git]: https://git-scm.com/
[uv]: https://github.com/astral-sh/uv
Clone:
```console
$ git clone https://github.com/tmux-python/libtmux-mcp.git
```
```console
$ cd libtmux-mcp
```
Install:
```console
$ uv pip install -e . -G dev
```
## Testing
```console
$ uv run pytest
```
Run a specific test file:
```console
$ uv run pytest tests/test_pane_tools.py
```
Run a specific test:
```console
$ uv run pytest tests/test_pane_tools.py::test_send_keys
```
Watch mode:
```console
$ uv run ptw .
```
## Linting
```console
$ uv run ruff check .
```
Format:
```console
$ uv run ruff format .
```
Auto-fix:
```console
$ uv run ruff check . --fix --show-fixes
```
## Type checking
```console
$ uv run mypy
```
## Documentation
Build:
```console
$ just build-docs
```
Serve with auto-reload:
```console
$ just start-docs
```
## Workflow
1. Format: `uv run ruff format .`
2. Test: `uv run pytest`
3. Lint: `uv run ruff check . --fix --show-fixes`
4. Types: `uv run mypy`
5. Verify: `uv run pytest`
## Releasing
Releases are published to PyPI via GitHub Actions when a tag is pushed:
```console
$ git tag v0.1.0
```
```console
$ git push --tags
```
The CI workflow builds the package, creates attestations, and publishes via OIDC trusted publishing.
---
# Project
Source: https://libtmux-mcp.git-pull.com/project/
(project)=
# Project
Information for contributors and maintainers.
::::{grid} 1 1 2 2
:gutter: 2 2 3 3
:::{grid-item-card} Contributing
:link: contributing
:link-type: doc
Development setup, running tests, submitting PRs.
:::
:::{grid-item-card} Code Style
:link: code-style
:link-type: doc
Ruff, mypy, NumPy docstrings, import conventions.
:::
:::{grid-item-card} Releasing
:link: releasing
:link-type: doc
Release checklist and version policy.
:::
::::
```{toctree}
:hidden:
contributing
code-style
releasing
```
---
# Releasing
Source: https://libtmux-mcp.git-pull.com/project/releasing/
(releasing)=
# Releasing
## Version scheme
libtmux-mcp follows [PEP 440](https://peps.python.org/pep-0440/) with alpha suffixes during pre-1.0 development (e.g. `0.1.0a0`).
## Release checklist
1. Update `CHANGES`.
2. Bump version in `src/libtmux_mcp/__about__.py`.
3. Commit and tag: `git tag v0.X.Y`.
4. Push with tags: `git push --follow-tags`.
5. CI publishes to PyPI.
---
# Prompts
Source: https://libtmux-mcp.git-pull.com/prompts/
(prompts-overview)=
# Prompts
MCP prompts are reusable, parameterised text templates the server
ships to its clients. A client renders a prompt by calling
``prompts/get``; the rendered text is what the model sees.
libtmux-mcp's prompts are short *workflow recipes* — the MCP-shaped
counterpart to the longer narrative recipes in {doc}`/recipes`.
## Available prompts
::::{grid} 1 2 2 2
:gutter: 2 2 3 3
:::{grid-item-card} `run_and_wait`
:link: fastmcp-prompt-run-and-wait
:link-type: ref
Execute a shell command and block until it finishes, preserving exit status.
:::
:::{grid-item-card} `diagnose_failing_pane`
:link: fastmcp-prompt-diagnose-failing-pane
:link-type: ref
Gather pane context and produce a root-cause hypothesis without taking action.
:::
:::{grid-item-card} `build_dev_workspace`
:link: fastmcp-prompt-build-dev-workspace
:link-type: ref
Set up a 3-pane editor / shell / logs layout shell-agnostically.
:::
:::{grid-item-card} `interrupt_gracefully`
:link: fastmcp-prompt-interrupt-gracefully
:link-type: ref
Send SIGINT and verify the shell prompt returns, refusing to auto-escalate.
:::
::::
```{tip}
Most MCP clients render prompts via a slash-command UI
(``/:``). For tools-only clients that don't expose
prompts, set ``LIBTMUX_MCP_PROMPTS_AS_TOOLS=1`` in the server
environment to surface them as ``list_prompts`` / ``get_prompt``
tools instead.
```
---
```{fastmcp-prompt} run_and_wait
```
**Use when** the agent needs to execute a single shell command and
must know whether it succeeded before deciding the next step.
**Why use this instead of `send_keys` + `capture_pane` polling?**
Each rendered call embeds a UUID-scoped ``tmux wait-for`` channel,
so concurrent agents (or parallel prompt calls from one agent) can
never cross-signal each other. The server side blocks until the
channel is signalled — strictly cheaper in agent turns than a
``capture_pane`` retry loop.
```{fastmcp-prompt-input} run_and_wait
```
**Sample render** (``command="pytest"``, ``pane_id="%1"``):
````markdown
Run this shell command in tmux pane %1 and block
until it finishes:
```python
send_keys(
pane_id='%1',
keys='pytest; tmux wait-for -S libtmux_mcp_wait_',
)
wait_for_channel(channel='libtmux_mcp_wait_', timeout=60.0)
capture_pane(pane_id='%1', max_lines=100)
```
After the channel signals, read the last ~100 lines to verify the
command's behaviour. Do NOT use a `capture_pane` retry loop —
`wait_for_channel` is strictly cheaper in agent turns.
The payload does not preserve the command's exit status: doing so
in an interactive shell would require exiting the shell (which kills
the pane) or routing through an out-of-band file or tmux variable.
If you need the status, inspect the captured output for
command-specific success markers.
````
Shell ``;`` semantics fire the ``wait-for -S`` whether ``pytest``
succeeded or failed, so the edge-triggered signal never deadlocks the
agent on a crashed command. Status preservation is intentionally
omitted: chaining ``exit $status`` after the signal would exit the
interactive shell itself, destroying single-pane sessions.
---
```{fastmcp-prompt} diagnose_failing_pane
```
**Use when** something visibly went wrong in a pane and the agent
needs to investigate before deciding what to fix. Produces a plan,
not an action.
**Why use this instead of just calling `capture_pane`?** The recipe
prefers {tool}`snapshot-pane`, which returns content + cursor
position + pane mode + scroll state in one call — saving a
follow-up ``get_pane_info`` round-trip. It also explicitly forbids
the agent from acting before it has a hypothesis, which prevents
"fix the symptom" anti-patterns. For repeated observation, it routes
follow-up reads through {tool}`capture-since` cursors instead of full
pane captures.
```{fastmcp-prompt-input} diagnose_failing_pane
```
**Sample render** (``pane_id="%1"``):
```markdown
Something went wrong in tmux pane %1. Diagnose it:
1. Call `snapshot_pane(pane_id="%1")` to get content,
cursor position, pane mode, and scroll state in one call.
2. If the content looks truncated, re-call with `max_lines=None`.
3. If you need to watch the pane across more than one turn, call
`capture_since(pane_id="%1")`, keep the returned cursor,
and pass it to later `capture_since(cursor=...)` calls.
4. Identify the last command that ran (look at the prompt line and
the line above it) and the last non-empty output line.
5. Propose a root cause hypothesis and a minimal command to verify
it (do NOT execute anything yet — produce the plan first).
```
---
```{fastmcp-prompt} build_dev_workspace
```
**Use when** the operator wants a fresh 3-pane workspace with editor
on top, terminal bottom-left, and a logs pane bottom-right — the
most common shape for active development.
**Why use this instead of describing the layout in free form?** The
recipe uses real parameter names that match the tools' actual
signatures (``session_name=``, ``pane_id=``, ``direction="below"``)
so an agent following it verbatim never hits a validation error. It
also explicitly avoids waiting for shell prompts after launching
``vim`` / ``watch`` / ``tail -f`` — the kind of guidance that would
deadlock an agent following naïve "wait for the prompt between each
step" advice.
```{fastmcp-prompt-input} build_dev_workspace
```
Pass e.g. ``"tail -f /var/log/syslog"`` on Linux or
``"log stream --level info"`` on macOS as the ``log_command`` to
override the OS-neutral default.
**Sample render** (``session_name="dev"``):
````markdown
Set up a 3-pane development workspace named
'dev' with editor on top, a shell on the bottom-left, and
a logs tail on the bottom-right:
1. `create_session(session_name="dev")` — creates the
session with a single pane (pane A, the editor). Capture the
returned `active_pane_id` as `%A`.
2. `split_window(pane_id="%A", direction="below")` — splits off
the bottom half (pane B, the terminal). Capture the returned
`pane_id` as `%B`.
3. `split_window(pane_id="%B", direction="right")` — splits pane B
horizontally (pane C, the logs pane). Capture the returned
`pane_id` as `%C`.
4. Launch the editor and the log command via `send_keys`:
`send_keys(pane_id="%A", keys="vim")` and
`send_keys(pane_id="%C", keys='watch -n 1 date')`. Leave pane B
at its fresh shell prompt — nothing needs to be sent there. No
pre-launch wait is required: tmux buffers keystrokes into the
pane's PTY whether or not the shell has finished drawing, so
`send_keys` immediately after `split_window` is safe and
shell-agnostic.
5. Optionally confirm each program drew its UI via
`wait_for_content_change(pane_id="%A", timeout=3.0)`
(and similarly for `%C`). This is a "did the screen change?"
check — it works whether the pane shows a prompt glyph, a vim
splash screen, or a log tail, so no shell-specific regex is
needed.
Use pane IDs (`%N`) for all subsequent targeting — they are stable
across layout changes; window renames are not.
````
---
```{fastmcp-prompt} interrupt_gracefully
```
**Use when** the agent needs to stop a running command and confirm
control returned to the shell — without escalating beyond SIGINT.
**Why use this instead of just sending `C-c`?** The recipe pairs the
interrupt with a {tool}`wait-for-text` against a common shell prompt
glyph and an explicit instruction to *stop and ask* if the wait
times out. That prevents the most dangerous failure mode — an agent
auto-escalating to ``C-\\`` (SIGQUIT, may core-dump) or ``kill``
without operator consent — by drawing a clear escalation boundary.
```{fastmcp-prompt-input} interrupt_gracefully
```
**Sample render** (``pane_id="%1"``):
````markdown
Interrupt whatever is running in pane %1 and
verify that control returns to the shell:
1. `send_keys(pane_id="%1", keys="C-c", literal=False,
enter=False)` — tmux interprets `C-c` as SIGINT.
2. `wait_for_text(pane_id="%1", pattern="\$ |\# |\% ",
regex=True, timeout=5.0)` — waits for a common shell prompt
glyph. Adjust the pattern to match the user's shell theme.
3. If the wait times out the process is ignoring SIGINT. Stop and
ask the caller how to proceed — do NOT escalate automatically
to `C-\` (SIGQUIT) or `kill`.
````
The shell-prompt regex covers default bash / zsh — adjust for fish
(``> ``), zsh + oh-my-zsh (``➜ ``), or starship (``❯ ``). When the
pattern doesn't match the user's prompt theme the recipe times out
and surfaces the situation to the caller, which is the right
default for "I tried, can't tell, what should I do?" workflows.
---
# Quickstart
Source: https://libtmux-mcp.git-pull.com/quickstart/
(quickstart)=
# Quickstart
One happy path from zero to a working tool invocation.
## 1. Install
Pick your MCP client and install method:
```{mcp-install}
```
See {ref}`clients` for dev-checkout setup, full config-file locations, and common pitfalls.
## 2. Verify
Ask your LLM:
```{admonition} Prompt
:class: prompt
List all my tmux sessions and show me what's running in each pane.
```
The agent will call `list_sessions`, then `list_panes` and `capture_pane` to inspect your workspace. You should see your tmux sessions, windows, and pane contents in the response.
## 3. Try it
Here are a few things to try:
```{admonition} Prompt
:class: prompt
Create a new tmux session called "workspace" with a window named "build".
```
```{admonition} Prompt
:class: prompt
Send `make test` to the pane in my build window, then wait for it to finish and capture the output.
```
```{admonition} Prompt
:class: prompt
Search all my panes for the word "error".
```
## How it works
When you say "run `make test` and show me the output", the agent executes a three-step pattern:
1. {tool}`send-keys` — send the command (composed with `tmux wait-for -S `) to a tmux pane
2. {tool}`wait-for-channel` — block deterministically until the command signals completion
3. {tool}`capture-pane` — read the terminal output
This **send → wait → capture** sequence is the fundamental workflow. For commands the agent authors, the channel pattern is deterministic; for output the agent does not author (third-party log lines, daemon prompts, interactive supervisors), substitute {tool}`wait-for-text` for step 2.
When you need to keep checking the same pane after that first read, switch to
{tool}`capture-since`: the first call returns a cursor, and follow-up calls
return only new pane output.
## Next steps
- {ref}`concepts` — Understand the tmux hierarchy and how tools target panes
- {ref}`configuration` — Environment variables and socket isolation
- {ref}`safety` — Control which tools are available
- {ref}`Tools ` — Browse all available tools
---
# Recipes
Source: https://libtmux-mcp.git-pull.com/recipes/
(recipes)=
# Recipes
Each recipe starts from a real workspace situation and traces the agent's
reasoning through discovery, decision, and action. The goal is not to show
tool-call sequences -- it is to show *how an agent decides* what to do with
existing tmux state so you can write better prompts and system instructions.
Every recipe uses the same structure:
- **Situation** -- the developer's world before the agent acts
- **Prompt** -- the natural-language sentence that triggers the recipe
- **Discover** -- what the agent inspects and why
- **Decide** -- the judgment call that changes the plan
- **Act** -- the minimum safe action sequence
- **The non-obvious part** -- the lesson you would miss from reading tool docs
alone
---
## Find a running dev server and test against it
**Situation.** A developer manages a React project with
[tmuxp](https://tmuxp.git-pull.com). One pane is already running
`pnpm start` with Vite somewhere in the `react` window. They want to run
Playwright e2e tests. The agent does not know which pane has the server,
or what port it chose.
```{admonition} Prompt
:class: prompt
Run the Playwright tests against my dev server in the myapp session.
```
### Discover
```{admonition} Agent reasoning
:class: agent-thought
{toolref}`list-panes` will not help here -- it shows metadata like current
command and working directory, not terminal content. The dev server printed
its URL to the terminal minutes ago, so I need to search terminal content.
```
The agent calls {tooliconl}`search-panes` with `pattern: "Local:"` and
`session_name: "myapp"`. The response comes back with pane `%5` in the `react`
window, matched line: `Local: http://localhost:5173/`.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
The server is alive and its URL is known. I do not need to start anything.
I just need an idle pane for running tests.
```
The agent calls {tooliconl}`list-panes` on the `myapp` session. Several panes show
`pane_current_command: zsh` -- idle shells. It picks `%4` in the same window.
### Act
The agent calls {tooliconl}`send-keys` in pane `%4`:
`PLAYWRIGHT_BASE_URL=http://localhost:5173 pnpm exec playwright test`
Then it calls {tooliconl}`wait-for-text` on pane `%4` with `pattern: "passed|failed|timed out"`, `regex: true`, and `timeout: 120`. Once the
wait resolves, it calls {tooliconl}`capture-pane` on `%4` with `start: -80` to
read the test results.
```{tip}
The agent's first instinct might be to *start* a Vite server. But
{tooliconl}`search-panes` reveals one is already running. This avoids a port
conflict, a wasted pane, and the most common agent mistake: treating tmux
like a blank shell.
```
### The non-obvious part
{toolref}`search-panes` searches terminal *content* -- what you would see on
screen. {toolref}`list-panes` searches *metadata* like current command and
working directory. If the agent had used {toolref}`list-panes` to find a pane
running `node`, it would know a process exists but not whether it is ready or
what URL it chose.
---
## Target the bottom-right pane for an ad-hoc command
**Situation.** The developer keeps a four-pane tiled layout for the
project: editor top-left, watcher top-right, logs bottom-left, scratch
bottom-right. They want the agent to spin up a one-off dev server in the
scratch pane without naming it explicitly.
```{admonition} Prompt
:class: prompt
Spin up the dev server in the bottom-right pane.
```
### Discover
```{admonition} Agent reasoning
:class: agent-thought
I do not need to list every pane and compute geometry. tmux already
tracks each pane's edge predicates -- there is a tool that resolves a
corner directly to a `PaneInfo`.
```
The agent calls {tooliconl}`find-pane-by-position` with
`corner: "bottom-right"`. The response is a
{class}`~libtmux_mcp.models.PaneInfo` carrying the pane's `pane_id` plus
the new geometry block: `pane_at_bottom: true`, `pane_at_right: true`,
`pane_left`, `pane_top`, etc.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
I have the `pane_id`. From here on I target by ID, never by corner --
once the user resizes the layout, "bottom-right" might mean a different
pane, but the `pane_id` of the pane I just identified stays stable.
```
### Act
The agent calls {tooliconl}`send-keys` in that pane: `pnpm start`. Then
{tooliconl}`wait-for-text` with `pattern: "Local:"` and a generous
`timeout` so Vite has room to start.
```{tip}
"The bottom-right pane" is a *role* -- a layout-relative target the
human reasons about. The `pane_id` returned by
{toolref}`find-pane-by-position` is the *handle* the agent should use
for every subsequent call. Do not call the corner-finder again on each
follow-up; reuse the ID.
```
### The non-obvious part
Before {toolref}`find-pane-by-position`, the only way to resolve a
corner was {toolref}`display-message` with `#{pane_at_bottom}` and
`#{pane_at_right}` per pane, then parsing the string output. The
structured `PaneInfo` response now carries `pane_left`, `pane_top`,
`pane_right`, `pane_bottom` and the four `pane_at_*` predicates as
typed fields, so agents reasoning about layout no longer need a
parsing detour through {toolref}`display-message`.
For single-pane windows, every corner resolves to the same pane (it
touches every edge). For genuinely ambiguous layouts, the visually
innermost pane wins via a `pane_left + pane_top` tie-break.
---
## Start a service and wait for it before running dependent work
**Situation.** The developer is starting fresh in their `backend` session --
no server running yet. They want to run integration tests, but the test
suite needs a live API server.
```{admonition} Prompt
:class: prompt
Start the API server in my backend session and run the integration tests once it's ready.
```
### Discover
```{admonition} Agent reasoning
:class: agent-thought
First I need to know what exists in the `backend` session. If a server is
already running, I should reuse it instead of starting a duplicate.
```
The agent calls {tooliconl}`list-panes` for the `backend` session. No pane is
running a server process. A {tooliconl}`search-panes` call for `"listening"`
returns no matches.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
Nothing to reuse. I need a dedicated pane for the server so its output
stays separate from the test output.
```
### Act
The agent calls {tooliconl}`split-window` with `session_name: "backend"` to
create a new pane, then calls {tooliconl}`send-keys` in that pane:
`npm run serve`.
The agent calls {tooliconl}`wait-for-text` on the server pane with
`pattern: "Listening on"` and `timeout: 30`. Once the wait resolves, the
agent calls {tooliconl}`send-keys` in the original pane:
`npm test -- --integration`, then {tooliconl}`wait-for-text` with
`pattern: "passed|failed|error"` and `regex: true`, then
{tooliconl}`capture-pane` to read the test results.
```{warning}
Calling {toolref}`capture-pane` immediately after {toolref}`send-keys` is a
race condition. {toolref}`send-keys` returns the moment tmux accepts the
keystrokes, not when the command finishes. For commands the agent authors,
compose `tmux wait-for -S ` into the command and call
{toolref}`wait-for-channel` — deterministic, race-free. For output the
agent does not author (server-startup banners, test-result lines like
the ones above), use {toolref}`wait-for-text` instead.
```
### The non-obvious part
{toolref}`wait-for-text` replaces `sleep`. The server might start in 2
seconds or 20 -- the agent adapts. The anti-pattern is polling with repeated
{toolref}`capture-pane` calls or hardcoding a sleep duration. When the job is
already running and the agent needs to keep observing it across turns, use
{toolref}`capture-since` so each read returns only new pane output.
---
## Find the failing pane without opening random terminals
**Situation.** The developer kicked off multiple jobs across panes in a `ci`
session -- linting, unit tests, integration tests, type checking. One of
them failed, but they stepped away and do not remember which pane.
```{admonition} Prompt
:class: prompt
Check my ci session -- which jobs failed?
```
### Discover
```{admonition} Agent reasoning
:class: agent-thought
I should not capture every pane and read them all -- that is expensive and
slow. Instead I will search for common failure indicators across all panes
at once.
```
The agent calls {tooliconl}`search-panes` with
`pattern: "FAIL|ERROR|error:|Traceback"`, `regex: true`, scoped to
`session_name: "ci"`.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
Two panes matched: `%3` has `FAIL: test_upload` and `%6` has
`error: Type 'string' is not assignable`. I will capture context from each.
```
### Act
The agent calls {tooliconl}`capture-pane` on `%3` with `start: -60`, then on
`%6` with `start: -60`.
```{tip}
If the error scrolled off the visible screen, use `content_start: -200` (or
deeper) when calling {tooliconl}`search-panes`. The `content_start` parameter
makes search reach into scrollback history, not just the visible screen.
```
### The non-obvious part
{toolref}`search-panes` checks all panes in a single call -- searching 20
panes costs roughly the same as searching 2. An agent that instead calls
{toolref}`list-panes` then {toolref}`capture-pane` on each one individually
makes 20+ round trips for the same information. The `regex: true` parameter
is required here because the `|` in the pattern is a regex alternation, not
literal text.
---
## Interrupt a stuck process and recover the pane
**Situation.** A long-running build is hanging. The developer wants to
interrupt it, verify the pane is responsive, and re-run the command.
```{admonition} Prompt
:class: prompt
The build in pane %2 is stuck. Kill it and restart.
```
Or with less specificity — the agent will discover the target:
```{admonition} Prompt
:class: prompt
The build in one of my panes is stuck. Kill it and restart.
```
Or if you've built muscle memory in your chats:
```{admonition} Prompt
:class: prompt
The build is stuck. Kill it and restart.
```
### Discover
```{admonition} Agent reasoning
:class: agent-thought
I need to send Ctrl-C. This is a tmux key name, not text -- so I must use
`enter: false` or tmux will send Ctrl-C followed by Enter, which could
confirm a prompt I did not intend to answer.
```
The agent calls {tooliconl}`send-keys` with `keys: "C-c"` and `enter: false` on
the target pane.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
Did the interrupt work? Some processes ignore {term}`SIGINT`. I will wait briefly
for a shell prompt to reappear. Developers use custom prompts, so I cannot
just look for `$`.
```
The agent calls {tooliconl}`wait-for-text` with `pattern: "[$#>%] *$"`,
`regex: true`, and `timeout: 5`.
```{admonition} Agent reasoning
:class: agent-thought
If the wait resolves, the shell is back. If it times out, the process
ignored Ctrl-C. I will escalate: try {term}`SIGQUIT` (`C-\` with `enter: false`),
then destroy and replace the pane only as a last resort.
```
### Act
If the wait times out, the agent sends `C-\` (also with `enter: false`). If
that also fails, it calls {tooliconl}`kill-pane` on the stuck pane, then
{tooliconl}`split-window` to create a replacement, then {tooliconl}`send-keys` to
re-run.
```{warning}
The `enter: false` parameter is critical. Without it, {toolref}`send-keys`
sends Ctrl-C *then* Enter, which could confirm a "really quit?" prompt,
submit a partially typed command, or enter a newline into a REPL.
```
### The non-obvious part
Recovery is a two-step decision. Try {term}`SIGINT` first (Ctrl-C),
verify it worked with {toolref}`wait-for-text`, escalate to {term}`SIGQUIT`
only if needed. The escalation ladder is: interrupt, verify, escalate signal,
destroy. Skipping
straight to {toolref}`kill-pane` loses the pane's scrollback history and any
partially written output that might explain *why* it hung.
---
## Re-run a command without mixing old and new output
**Situation.** The developer wants `pytest` re-run in tmux, but the
candidate pane already has old test output in scrollback. They want only
fresh results.
```{admonition} Prompt
:class: prompt
Run `pytest` in the test pane and show me only the fresh output.
```
### Discover
The agent calls {tooliconl}`list-panes` to find the pane by title, cwd, or
current command. If more than one pane is plausible, it uses
{tooliconl}`capture-pane` with a small range to confirm the target.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
The pane is a shell. I should clear it before running so the capture
afterwards contains only fresh output. If it were running a watcher or
long-lived process, I would not hijack it -- I would use a different pane.
```
### Act
The agent calls {tooliconl}`clear-pane`, then {tooliconl}`send-keys` with
`keys: "pytest; tmux wait-for -S pytest_done"`, then
{tooliconl}`wait-for-channel` with `channel: "pytest_done"`, then
{tooliconl}`capture-pane` to read the fresh output. Composing the
`tmux wait-for -S` signal directly into the shell command is the
deterministic path for authored commands.
### The non-obvious part
{toolref}`clear-pane` runs two tmux commands internally (`send-keys -R` then
`clear-history`) with a brief gap between them. Calling
{toolref}`capture-pane` immediately after {toolref}`clear-pane` may catch
partial state. The {toolref}`wait-for-text` call after {toolref}`send-keys`
naturally provides the needed delay, so the sequence clear-send-wait-capture
is safe.
---
## Build a workspace the agent can revisit later
**Situation.** The developer wants a durable project workspace -- not just a
quick split, but a layout that later prompts can refer to by role ("the
server pane", "the test pane").
```{admonition} Prompt
:class: prompt
Set up a tmux workspace for myproject with editor, server, and test panes.
```
### Discover
```{admonition} Agent reasoning
:class: agent-thought
Before creating anything, I need to check whether a session with this name
already exists. Creating a duplicate will fail.
```
The agent calls {tooliconl}`list-sessions`. No session named `myproject` exists.
### Decide
```{admonition} Agent reasoning
:class: agent-thought
Safe to create. I need three panes: editor, server, tests. I will create
the session, split twice, then apply a layout so tmux handles the geometry
instead of me calculating sizes.
```
### Act
The agent calls {tooliconl}`create-session` with `session_name: "myproject"` and
`start_directory: "/home/dev/myproject"`. Then {tooliconl}`split-window` twice
(with `direction: "right"` and `direction: "below"`), followed by
{tooliconl}`select-layout` with `layout: "main-vertical"`.
The agent calls {tooliconl}`set-pane-title` on each pane: `editor`, `server`,
`tests`.
The agent calls {tooliconl}`send-keys` in the server pane: `npm run dev`, then
{tooliconl}`wait-for-text` for `pattern: "ready|listening|Local:"` with
`regex: true` and `timeout: 30`.
```{tip}
If the session *does* already exist, the right move is to reuse and extend
it, not recreate it. The {toolref}`list-sessions` check at the top is what
makes that decision possible.
```
### The non-obvious part
Titles and naming are not cosmetic. They reduce future discovery cost. When
the agent comes back in a later conversation and the user says "restart the
server," the agent calls {toolref}`list-panes`, finds the pane titled
`server`, and acts -- no searching, no guessing, no capturing every pane to
figure out which one is which. But note: pane IDs are ephemeral across tmux
server restarts, so the agent should always re-discover by metadata (session
name, pane title, cwd) rather than trusting remembered `%N` values.
---
## What to read next
For the principles that recur across these recipes -- discover before acting,
wait instead of polling, content vs. metadata, prefer IDs, escalate
gracefully -- see the {ref}`prompting guide `. For specific
pitfalls like `enter: false` and the `send_keys`/`capture_pane` race
condition, see {ref}`gotchas `.
---
# API Reference
Source: https://libtmux-mcp.git-pull.com/reference/api/
(api)=
# API Reference
::::{grid} 1 1 2 2
:gutter: 2 2 3 3
:::{grid-item-card} Server
:link: server
:link-type: doc
MCP server entry point and lifecycle.
:::
:::{grid-item-card} Tools
:link: tools
:link-type: doc
Tool function implementations.
:::
:::{grid-item-card} Models
:link: models
:link-type: doc
Pydantic models for requests and responses.
:::
:::{grid-item-card} Middleware
:link: middleware
:link-type: doc
Safety-tier enforcement and request hooks.
:::
:::{grid-item-card} Utils
:link: utils
:link-type: doc
Shared helpers and utilities.
:::
::::
```{toctree}
:hidden:
server
tools
models
middleware
utils
```
---
# Middleware
Source: https://libtmux-mcp.git-pull.com/reference/api/middleware/
# Middleware
```{eval-rst}
.. automodule:: libtmux_mcp.middleware
:members:
:undoc-members:
:show-inheritance:
```
---
# Models
Source: https://libtmux-mcp.git-pull.com/reference/api/models/
# Models
```{eval-rst}
.. automodule:: libtmux_mcp.models
:members:
:undoc-members:
:show-inheritance:
```
---
# Server
Source: https://libtmux-mcp.git-pull.com/reference/api/server/
# Server
```{eval-rst}
.. automodule:: libtmux_mcp.server
:members:
:undoc-members:
:show-inheritance:
```
---
# Tools
Source: https://libtmux-mcp.git-pull.com/reference/api/tools/
# Tools
## Server tools
```{eval-rst}
.. automodule:: libtmux_mcp.tools.server_tools
:members:
:undoc-members:
:show-inheritance:
```
## Session tools
```{eval-rst}
.. automodule:: libtmux_mcp.tools.session_tools
:members:
:undoc-members:
:show-inheritance:
```
## Window tools
```{eval-rst}
.. automodule:: libtmux_mcp.tools.window_tools
:members:
:undoc-members:
:show-inheritance:
```
## Pane tools
```{eval-rst}
.. automodule:: libtmux_mcp.tools.pane_tools
:members:
:undoc-members:
:show-inheritance:
```
## Option tools
```{eval-rst}
.. automodule:: libtmux_mcp.tools.option_tools
:members:
:undoc-members:
:show-inheritance:
```
## Environment tools
```{eval-rst}
.. automodule:: libtmux_mcp.tools.env_tools
:members:
:undoc-members:
:show-inheritance:
```
---
# Utilities
Source: https://libtmux-mcp.git-pull.com/reference/api/utils/
# Utilities
```{eval-rst}
.. automodule:: libtmux_mcp._utils
:members:
:undoc-members:
:show-inheritance:
```
---
# Compatibility
Source: https://libtmux-mcp.git-pull.com/reference/compatibility/
(compatibility)=
# Compatibility
## Python versions
| Python | Status |
|--------|--------|
| 3.10 | Supported |
| 3.11 | Supported |
| 3.12 | Supported |
| 3.13 | Supported |
| 3.14 | Supported |
| PyPy | Supported |
## tmux versions
| tmux | Status |
|------|--------|
| >= 3.2a | Supported |
| < 3.2a | Not supported (libtmux requirement) |
## Dependencies
| Package | Required version |
|---------|-----------------|
| [libtmux](https://libtmux.git-pull.com/) | >= 0.55.0, < 1.0 |
| [FastMCP](https://github.com/jlowin/fastmcp) | >= 3.1.0, < 4.0.0 |
## MCP clients
| Client | Tested | Transport |
|--------|--------|-----------|
| Claude Code | Yes | stdio |
| Claude Desktop | Yes | stdio |
| Codex CLI | Yes | stdio |
| Gemini CLI | Yes | stdio |
| Cursor | Yes | stdio |
| MCP Inspector | Yes | stdio |
## OS support
| Platform | Status |
|----------|--------|
| Linux | Supported |
| macOS | Supported |
| WSL2 | Supported |
| Windows (native) | Not supported (tmux requires Unix) |
---
# Resources
Source: https://libtmux-mcp.git-pull.com/resources/
(resources-overview)=
# Resources
MCP resources are addressable documents the server exposes at
``tmux://`` URIs. Clients read them via ``resources/read``. All
libtmux-mcp resources are
[resource templates](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates)
— each URI includes a ``{?socket_name}`` query parameter for socket
isolation, plus structural path parameters (``{session_name}``,
``{pane_id}``, …) so a single template covers every session, window,
or pane.
Every resource delivers a snapshot of the tmux hierarchy at call
time. Agents use them for read-only inspection; any write workflow
goes through the corresponding {doc}`tools `.
## Available resources
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} `get_sessions`
:link: fastmcp-resource-template-get-sessions
:link-type: ref
List all tmux sessions.
:::
:::{grid-item-card} `get_session`
:link: fastmcp-resource-template-get-session
:link-type: ref
Get details of a specific tmux session.
:::
:::{grid-item-card} `get_session_windows`
:link: fastmcp-resource-template-get-session-windows
:link-type: ref
List all windows in a tmux session.
:::
:::{grid-item-card} `get_window`
:link: fastmcp-resource-template-get-window
:link-type: ref
Get details of a specific window in a session.
:::
:::{grid-item-card} `get_pane`
:link: fastmcp-resource-template-get-pane
:link-type: ref
Get details of a specific pane.
:::
:::{grid-item-card} `get_pane_content`
:link: fastmcp-resource-template-get-pane-content
:link-type: ref
Capture and return the content of a pane.
:::
::::
---
## Sessions
```{fastmcp-resource-template} get_sessions
```
```{fastmcp-resource-template} get_session
```
```{fastmcp-resource-template} get_session_windows
```
## Windows
```{fastmcp-resource-template} get_window
```
## Panes
```{fastmcp-resource-template} get_pane
```
```{fastmcp-resource-template} get_pane_content
```
---
# Delete buffer
Source: https://libtmux-mcp.git-pull.com/tools/buffer/delete-buffer/
# Delete buffer
```{fastmcp-tool} buffer_tools.delete_buffer
```
**Use when** you're done with a buffer and want to free server-side
state. Always call this when the buffer's purpose is complete —
tmux servers outlive the MCP process, so leaked buffers persist
across MCP restarts.
**Side effects:** Removes the named buffer from the tmux server.
Subsequent {tooliconl}`paste-buffer` calls against the deleted name
return an {exc}`~libtmux_mcp._utils.ExpectedToolError`.
```{fastmcp-tool-input} buffer_tools.delete_buffer
```
---
# Buffer tools
Source: https://libtmux-mcp.git-pull.com/tools/buffer/
# Buffer tools
tmux paste buffers are a server-global namespace shared by every client on the same socket. The buffer tools in libtmux-mcp expose a narrow, agent-namespaced subset: every allocation gets a UUID-scoped name like `libtmux_mcp_<32-hex>_`, so concurrent agents (or parallel tool calls from one agent) cannot collide on each other's payloads.
There is **no** `list_buffers` tool. The user's OS clipboard often syncs into tmux paste buffers, so a generic enumeration would leak passwords, tokens, and other private content the agent has no business reading. Callers track the buffers they own via the {tool}`load-buffer` returns.
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} {tooliconl}`load-buffer`
Stage content into a new tmux paste buffer.
:::
:::{grid-item-card} {tooliconl}`paste-buffer`
Push a staged buffer into a pane.
:::
:::{grid-item-card} {tooliconl}`show-buffer`
Read a staged buffer's contents back.
:::
:::{grid-item-card} {tooliconl}`delete-buffer`
Free the server-side state of a staged buffer.
:::
::::
```{toctree}
:hidden:
:maxdepth: 1
load-buffer
paste-buffer
show-buffer
delete-buffer
```
---
# Load buffer
Source: https://libtmux-mcp.git-pull.com/tools/buffer/load-buffer/
# Load buffer
```{fastmcp-tool} buffer_tools.load_buffer
```
**Use when** you need to stage multi-line text for paste — sending a
shell script, prepared input for an interactive prompt, or content
that's too long for a clean {tooliconl}`send-keys` invocation.
**Avoid when** the text is a single command line that {tooliconl}`send-keys`
can deliver directly. `load_buffer` allocates server-side state that
must be cleaned up via {tooliconl}`delete-buffer`.
**Side effects:** Allocates a tmux paste buffer. Use the returned
`buffer_name` for follow-up calls. The `content` argument is redacted
in audit logs.
**Example:**
```json
{
"tool": "load_buffer",
"arguments": {
"content": "for i in 1 2 3; do\n echo line $i\ndone\n",
"logical_name": "loop"
}
}
```
Response:
```json
{
"buffer_name": "libtmux_mcp_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6_loop",
"logical_name": "loop"
}
```
```{fastmcp-tool-input} buffer_tools.load_buffer
```
---
## Paste
---
# Paste buffer
Source: https://libtmux-mcp.git-pull.com/tools/buffer/paste-buffer/
# Paste buffer
```{fastmcp-tool} buffer_tools.paste_buffer
```
**Use when** you've staged content via {tooliconl}`load-buffer` and
need to push it into a target pane. Use bracketed paste mode
(default `bracket=true`) for terminals that handle it; the wrapping
escape sequences signal "this is pasted text, not typed input".
**Avoid when** the buffer name doesn't match the MCP namespace —
`paste_buffer` refuses non-`libtmux_mcp_*` names so it cannot be
turned into an arbitrary-buffer paster.
**Side effects:** Pastes content into the target pane (the pane's
shell receives the bytes as input). Open-world: the resulting shell
behavior is whatever the pasted bytes invoke.
```{fastmcp-tool-input} buffer_tools.paste_buffer
```
---
## Inspect
---
# Show buffer
Source: https://libtmux-mcp.git-pull.com/tools/buffer/show-buffer/
# Show buffer
```{fastmcp-tool} buffer_tools.show_buffer
```
**Use when** you need to confirm what was staged before pasting, or
to read back a buffer between modifications. Restricted to
MCP-namespaced buffers — non-agent buffers are rejected.
**Side effects:** None. Readonly.
```{fastmcp-tool-input} buffer_tools.show_buffer
```
---
## Clean up
---
# Hook tools
Source: https://libtmux-mcp.git-pull.com/tools/hook/
# Hook tools
tmux hooks let you attach commands to lifecycle events — `pane-exited`, `session-renamed`, `command-error`, and so on. libtmux-mcp exposes **read-only** hook introspection so agents can audit what hooks the human user has configured before running automation that might trigger them.
## Why no `set_hook`?
Write-hooks are deliberately not exposed. tmux servers outlive the MCP process, and FastMCP's `lifespan` teardown runs only on graceful SIGTERM/SIGINT — it's bypassed on `kill -9`, OOM-kill, and C-extension-fault crashes. Any cleanup registry in Python could be silently bypassed, leaking agent-installed shell hooks into the user's persistent tmux server where they would fire forever. Three plausible future paths exist (a tmux-side `client-detached` meta-hook for self-cleanup, requiring `LIBTMUX_SAFETY=destructive`, or exposing one-shot `run_hook` only); none is in scope.
Until one of those paths is implemented, the surface here is visibility only.
## Inspect
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} {tooliconl}`show-hooks`
Enumerate bindings at a scope.
:::
:::{grid-item-card} {tooliconl}`show-hook`
Inspect a single binding.
:::
::::
```{toctree}
:hidden:
:maxdepth: 1
show-hooks
show-hook
```
---
# Show hook
Source: https://libtmux-mcp.git-pull.com/tools/hook/show-hook/
# Show hook
```{fastmcp-tool} hook_tools.show_hook
```
**Use when** you know which hook you want to inspect by name. Returns
empty when the hook is unset; raises an
{exc}`~libtmux_mcp._utils.ExpectedToolError` for
unknown hook names (typos, wrong scope) so input mistakes don't
masquerade as "nothing configured".
**Side effects:** None. Readonly.
```{fastmcp-tool-input} hook_tools.show_hook
```
---
# Show hooks
Source: https://libtmux-mcp.git-pull.com/tools/hook/show-hooks/
# Show hooks
```{fastmcp-tool} hook_tools.show_hooks
```
**Use when** you need to enumerate every hook configured on a
target — the human user's tmux config, an inherited team setup, or
a session that another tool may have touched.
**Side effects:** None. Readonly.
```{fastmcp-tool-input} hook_tools.show_hooks
```
---
# Tools
Source: https://libtmux-mcp.git-pull.com/tools/
(tools-overview)=
# Tools
All tools accept an optional `socket_name` parameter for multi-server support. It defaults to the {envvar}`LIBTMUX_SOCKET` env var. See {ref}`configuration`.
## Which tool do I want?
**Reading terminal content?**
- Re-reading the same pane while it changes? → {tool}`capture-since`
- Need a one-shot read of a known pane? → {tool}`capture-pane`
- Need text + cursor + mode in one call? → {tool}`snapshot-pane`
- Don't know which pane? → {tool}`search-panes`
- Need to wait for specific output? → {tool}`wait-for-text`
- Need to wait for *any* change? → {tool}`wait-for-content-change`
- Only need metadata (PID, path, size)? → {tool}`get-pane-info`
- Need an arbitrary tmux variable? → {tool}`display-message`
**Targeting a pane by layout?**
- "The bottom-right pane", "top-left", any corner → {tool}`find-pane-by-position`
- Already know the `pane_id` → use it directly
**Running a command?**
- {tool}`send-keys` (with `tmux wait-for -S ` composed into the keys) → {tool}`wait-for-channel` → {tool}`capture-pane` — the deterministic path for commands the agent authors
- For output the agent does not author (third-party logs, daemon prompts), use {tool}`wait-for-text` or {tool}`wait-for-content-change` between `send-keys` and `capture-pane`
- Pasting multi-line text? → {tool}`paste-text`
**Creating workspace structure?**
- New session → {tool}`create-session`
- New window → {tool}`create-window`
- New pane → {tool}`split-window`
**Navigating?**
- Switch pane → {tool}`select-pane` (by ID or direction)
- Switch window → {tool}`select-window` (by ID, index, or direction)
**Rearranging layout?**
- Swap two panes → {tool}`swap-pane`
- Move window → {tool}`move-window`
- Change layout → {tool}`select-layout`
**Scrollback / copy mode?**
- Enter copy mode → {tool}`enter-copy-mode`
- Exit copy mode → {tool}`exit-copy-mode`
- Log output to file → {tool}`pipe-pane`
**Coordinating across panes?**
- Block until signalled → {tool}`wait-for-channel`
- Signal a waiter → {tool}`signal-channel`
**Staging multi-line input?**
- Stage content → {tool}`load-buffer`
- Push into pane → {tool}`paste-buffer`
- Read back → {tool}`show-buffer`
- Free server state → {tool}`delete-buffer`
**Auditing tmux hooks?**
- Enumerate → {tool}`show-hooks`
- Inspect one → {tool}`show-hook`
**Changing settings?**
- tmux options → {tool}`show-option` / {tool}`set-option`
- Environment vars → {tool}`show-environment` / {tool}`set-environment`
**Reaching for a workflow recipe?** The server also ships four
short MCP prompts the client renders for the model — see
{doc}`/prompts`. They cover the most common patterns
(run-and-wait, diagnose-failing-pane, build-dev-workspace,
interrupt-gracefully) with embedded UUID-scoped wait channels and
shell-agnostic guidance.
## Inspect
Read tmux state without changing anything.
::::{grid} 1 2 3 3
:gutter: 2 2 3 3
:::{grid-item-card} list_sessions
:link: list-sessions
:link-type: ref
List all active sessions.
:::
:::{grid-item-card} list_windows
:link: list-windows
:link-type: ref
List windows in a session.
:::
:::{grid-item-card} list_panes
:link: list-panes
:link-type: ref
List panes in a window.
:::
:::{grid-item-card} capture_pane
:link: capture-pane
:link-type: ref
Read visible content of a pane.
:::
:::{grid-item-card} capture_since
:link: capture-since
:link-type: ref
Start a cursor, then read only new pane output.
:::
:::{grid-item-card} get_pane_info
:link: get-pane-info
:link-type: ref
Get detailed pane metadata.
:::
:::{grid-item-card} find_pane_by_position
:link: find-pane-by-position
:link-type: ref
Resolve "the bottom-right pane" (or any corner) to a `PaneInfo`.
:::
:::{grid-item-card} search_panes
:link: search-panes
:link-type: ref
Search text across panes.
:::
:::{grid-item-card} wait_for_text
:link: wait-for-text
:link-type: ref
Wait for text to appear in a pane.
:::
:::{grid-item-card} get_server_info
:link: get-server-info
:link-type: ref
Get tmux server info.
:::
:::{grid-item-card} list_servers
:link: list-servers
:link-type: ref
Discover live tmux servers under `$TMUX_TMPDIR`.
:::
:::{grid-item-card} show_option
:link: show-option
:link-type: ref
Query a tmux option value.
:::
:::{grid-item-card} show_environment
:link: show-environment
:link-type: ref
Show tmux environment variables.
:::
:::{grid-item-card} snapshot_pane
:link: snapshot-pane
:link-type: ref
Rich capture: content + cursor + mode + scroll.
:::
:::{grid-item-card} wait_for_content_change
:link: wait-for-content-change
:link-type: ref
Wait for any screen change.
:::
:::{grid-item-card} display_message
:link: display-message
:link-type: ref
Query arbitrary tmux format strings.
:::
:::{grid-item-card} show_buffer
:link: show-buffer
:link-type: ref
Read back an MCP-staged paste buffer.
:::
:::{grid-item-card} show_hooks
:link: show-hooks
:link-type: ref
Enumerate configured tmux hooks at a scope.
:::
:::{grid-item-card} show_hook
:link: show-hook
:link-type: ref
Inspect a single tmux hook by name.
:::
::::
## Act
Create or modify tmux objects.
::::{grid} 1 2 3 3
:gutter: 2 2 3 3
:::{grid-item-card} create_session
:link: create-session
:link-type: ref
Start a new tmux session.
:::
:::{grid-item-card} create_window
:link: create-window
:link-type: ref
Add a window to a session.
:::
:::{grid-item-card} split_window
:link: split-window
:link-type: ref
Split a window into panes.
:::
:::{grid-item-card} send_keys
:link: send-keys
:link-type: ref
Send commands or keystrokes to a pane.
:::
:::{grid-item-card} rename_session
:link: rename-session
:link-type: ref
Rename a session.
:::
:::{grid-item-card} rename_window
:link: rename-window
:link-type: ref
Rename a window.
:::
:::{grid-item-card} resize_pane
:link: resize-pane
:link-type: ref
Adjust pane dimensions.
:::
:::{grid-item-card} resize_window
:link: resize-window
:link-type: ref
Adjust window dimensions.
:::
:::{grid-item-card} select_layout
:link: select-layout
:link-type: ref
Set window layout.
:::
:::{grid-item-card} set_pane_title
:link: set-pane-title
:link-type: ref
Set pane title.
:::
:::{grid-item-card} clear_pane
:link: clear-pane
:link-type: ref
Clear pane content.
:::
:::{grid-item-card} set_option
:link: set-option
:link-type: ref
Set a tmux option.
:::
:::{grid-item-card} set_environment
:link: set-environment
:link-type: ref
Set a tmux environment variable.
:::
:::{grid-item-card} select_pane
:link: select-pane
:link-type: ref
Focus a pane by ID or direction.
:::
:::{grid-item-card} select_window
:link: select-window
:link-type: ref
Focus a window by ID, index, or direction.
:::
:::{grid-item-card} swap_pane
:link: swap-pane
:link-type: ref
Exchange positions of two panes.
:::
:::{grid-item-card} move_window
:link: move-window
:link-type: ref
Move window to another index or session.
:::
:::{grid-item-card} pipe_pane
:link: pipe-pane
:link-type: ref
Stream pane output to a file.
:::
:::{grid-item-card} enter_copy_mode
:link: enter-copy-mode
:link-type: ref
Enter copy mode for scrollback.
:::
:::{grid-item-card} exit_copy_mode
:link: exit-copy-mode
:link-type: ref
Exit copy mode.
:::
:::{grid-item-card} paste_text
:link: paste-text
:link-type: ref
Paste multi-line text via tmux buffer.
:::
:::{grid-item-card} load_buffer
:link: load-buffer
:link-type: ref
Stage multi-line text into an MCP-namespaced tmux buffer.
:::
:::{grid-item-card} paste_buffer
:link: paste-buffer
:link-type: ref
Paste an MCP buffer into a target pane.
:::
:::{grid-item-card} wait_for_channel
:link: wait-for-channel
:link-type: ref
Block until a tmux ``wait-for`` channel is signalled.
:::
:::{grid-item-card} signal_channel
:link: signal-channel
:link-type: ref
Wake clients blocked on a ``wait-for`` channel.
:::
::::
## Destroy
Tear down tmux objects. Not reversible.
::::{grid} 1 2 3 3
:gutter: 2 2 3 3
:::{grid-item-card} kill_session
:link: kill-session
:link-type: ref
Destroy a session and all its windows.
:::
:::{grid-item-card} kill_window
:link: kill-window
:link-type: ref
Destroy a window and all its panes.
:::
:::{grid-item-card} kill_pane
:link: kill-pane
:link-type: ref
Destroy a pane.
:::
:::{grid-item-card} kill_server
:link: kill-server
:link-type: ref
Kill the entire tmux server.
:::
:::{grid-item-card} delete_buffer
:link: delete-buffer
:link-type: ref
Delete an MCP-staged tmux paste buffer.
:::
::::
```{toctree}
:hidden:
:caption: Tools by tmux scope
server/index
session/index
window/index
pane/index
buffer/index
hook/index
```
---
# Capture pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/capture-pane/
# Capture pane
```{fastmcp-tool} pane_tools.capture_pane
```
**Use when** you need to read what's currently displayed in a terminal —
after running a command, checking output, or verifying state.
**Avoid when** you need to search across multiple panes at once — use
{tooliconl}`search-panes`. If you are repeatedly watching the same pane, use
{tooliconl}`capture-since` with its cursor so unchanged scrollback is not sent
again. If you only need pane metadata (not content), use {tooliconl}`get-pane-info`.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "capture_pane",
"arguments": {
"pane_id": "%0",
"start": -50
}
}
```
Response (string):
```text
$ echo "Running tests..."
Running tests...
$ echo "PASS: test_auth (0.3s)"
PASS: test_auth (0.3s)
$ echo "FAIL: test_upload (AssertionError)"
FAIL: test_upload (AssertionError)
$ echo "3 tests: 2 passed, 1 failed"
3 tests: 2 passed, 1 failed
$
```
```{fastmcp-tool-input} pane_tools.capture_pane
```
---
# Capture since
Source: https://libtmux-mcp.git-pull.com/tools/pane/capture-since/
# Capture since
```{fastmcp-tool} pane_tools.capture_since
```
**Use when** you need to observe the same pane repeatedly — tailing logs,
watching a long-running command, checking a daemon, or revisiting a terminal
without paying to re-read the same scrollback every turn. The first call returns
the current visible screen plus a cursor; later calls pass that cursor back and
receive only rows written or rewritten after it.
**Avoid when** you control the command and only need completion — compose
`tmux wait-for -S ` into the command and call
{tooliconl}`wait-for-channel`. If you need a one-shot content + metadata view,
use {tooliconl}`snapshot-pane`; if you do not know which pane contains text,
use {tooliconl}`search-panes`.
**Side effects:** None. Readonly.
**Example:**
Start a cursor with the currently visible screen:
```json
{
"tool": "capture_since",
"arguments": {
"pane_id": "%2"
}
}
```
Response:
```json
{
"pane_id": "%2",
"cursor": "capture-since-v1:...",
"lines": [
"$ pytest -vv",
"tests/test_api.py::test_health PASSED"
],
"elapsed_seconds": 0.003,
"lines_missed": false,
"truncated": false,
"truncated_lines": 0,
"truncated_bytes": 0
}
```
Read only content since that cursor:
```json
{
"tool": "capture_since",
"arguments": {
"cursor": "capture-since-v1:..."
}
}
```
The cursor carries the original pane id, so the follow-up call does not need
`pane_id`. If you pass both, they must match; a cursor for another pane raises
an {exc}`~libtmux_mcp._utils.ExpectedToolError` instead of silently reading the
wrong process.
If nothing new was written after the cursor, `lines` is empty and the response
still includes a fresh cursor for the same pane. If the cursor row scrolled into
retained history, the tool can still return an exact delta; retained scrollback
is not a loss condition.
`lines_missed` becomes `true` when tmux has cleared or trimmed the history
needed to compute an exact delta. In that case, `lines` is a conservative
current visible capture and the response includes a fresh cursor.
Pane lifecycle is part of the cursor contract. If the pane dies or is respawned,
the call raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` instead of
reading from a different process that reused the same pane id.
`truncated`, `truncated_lines`, and `truncated_bytes` are structured metadata.
No truncation marker is injected into `lines`, so clients can display terminal
text without parsing an in-band header.
The cursor is intentionally opaque. It is based on tmux grid state
(`history_size + cursor_y`) and pane lifecycle fields (`pane_id`, `pane_pid`);
see tmux's grid and capture implementation in
[grid.c](https://github.com/tmux/tmux/blob/134ba6c/grid.c) and
[cmd-capture-pane.c](https://github.com/tmux/tmux/blob/134ba6c/cmd-capture-pane.c),
and libtmux's
[`Pane.capture_pane()`](https://github.com/tmux-python/libtmux/blob/v0.58.0/src/libtmux/pane.py).
```{fastmcp-tool-input} pane_tools.capture_since
```
---
# Clear pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/clear-pane/
# Clear pane
```{fastmcp-tool} pane_tools.clear_pane
```
**Use when** you want a clean terminal before capturing output.
**Side effects:** Clears the pane's visible content.
**Example:**
```json
{
"tool": "clear_pane",
"arguments": {
"pane_id": "%0"
}
}
```
Response (string):
```text
Pane cleared: %0
```
```{fastmcp-tool-input} pane_tools.clear_pane
```
---
# Evaluate tmux format string (display_message)
Source: https://libtmux-mcp.git-pull.com/tools/pane/display-message/
# Evaluate tmux format string (display_message)
```{fastmcp-tool} pane_tools.display_message
```
**Use when** you need to query arbitrary tmux variables — zoom state, pane
dead flag, client activity, or any `#{format}` string that isn't covered by
other tools. Despite the historical name (`display_message` is the tmux verb
it wraps), this tool does **not** display anything to the user; it expands
the format string with `display-message -p` and returns the value.
**Avoid when** a dedicated tool already provides the information — e.g. use
{tooliconl}`snapshot-pane` for cursor position and mode,
{tooliconl}`get-pane-info` for standard metadata (including the new
`pane_left` / `pane_top` / `pane_at_*` geometry block), or
{tooliconl}`find-pane-by-position` to resolve a window corner to a
`PaneInfo` without parsing `#{pane_at_bottom}` / `#{pane_at_right}`
yourself.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "display_message",
"arguments": {
"format_string": "zoomed=#{window_zoomed_flag} dead=#{pane_dead}",
"pane_id": "%0"
}
}
```
Response (string):
```text
zoomed=0 dead=0
```
```{fastmcp-tool-input} pane_tools.display_message
```
---
# Enter copy mode
Source: https://libtmux-mcp.git-pull.com/tools/pane/enter-copy-mode/
# Enter copy mode
```{fastmcp-tool} pane_tools.enter_copy_mode
```
**Use when** you need to scroll through scrollback history in a pane.
Optionally scroll up immediately after entering. Use
{tooliconl}`snapshot-pane` afterward to read the `scroll_position` and
visible content.
**Side effects:** Puts the pane into copy mode. The pane stops receiving
new output until you exit copy mode.
**Example:**
```json
{
"tool": "enter_copy_mode",
"arguments": {
"pane_id": "%0",
"scroll_up": 50
}
}
```
Response:
```json
{
"pane_id": "%0",
"pane_index": "0",
"pane_width": "80",
"pane_height": "24",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} pane_tools.enter_copy_mode
```
---
# Exit copy mode
Source: https://libtmux-mcp.git-pull.com/tools/pane/exit-copy-mode/
# Exit copy mode
```{fastmcp-tool} pane_tools.exit_copy_mode
```
**Use when** you're done scrolling through scrollback and want the pane to
resume receiving output.
**Side effects:** Exits copy mode, returning the pane to normal.
**Example:**
```json
{
"tool": "exit_copy_mode",
"arguments": {
"pane_id": "%0"
}
}
```
Response:
```json
{
"pane_id": "%0",
"pane_index": "0",
"pane_width": "80",
"pane_height": "24",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} pane_tools.exit_copy_mode
```
---
# Find pane by position
Source: https://libtmux-mcp.git-pull.com/tools/pane/find-pane-by-position/
# Find pane by position
```{fastmcp-tool} pane_tools.find_pane_by_position
```
**Use when** you need to act on a layout-relative pane — "the bottom-right
pane", "whichever pane is in the top-left" — without listing every pane and
computing geometry yourself.
**Avoid when** you already know the `pane_id`. Use {tooliconl}`get-pane-info`
or {tooliconl}`select-pane` directly.
**Side effects:** None. Read-only.
**Example:**
```json
{
"tool": "find_pane_by_position",
"arguments": {
"corner": "bottom-right",
"window_id": "@0"
}
}
```
Response is a {class}`~libtmux_mcp.models.PaneInfo` for the pane occupying
that corner. The new geometry fields make the result self-describing:
```json
{
"pane_id": "%3",
"pane_left": 40,
"pane_top": 12,
"pane_right": 79,
"pane_bottom": 23,
"pane_at_left": false,
"pane_at_right": true,
"pane_at_top": false,
"pane_at_bottom": true,
"pane_tty": "/dev/pts/5"
}
```
**Tie-break.** When multiple panes satisfy both edge predicates (a
single-pane window touches every edge; some custom layouts can produce
ambiguous corners) the visually innermost pane wins — the one with the
largest `pane_left + pane_top`.
```{fastmcp-tool-input} pane_tools.find_pane_by_position
```
---
# Get pane info
Source: https://libtmux-mcp.git-pull.com/tools/pane/get-pane-info/
# Get pane info
```{fastmcp-tool} pane_tools.get_pane_info
```
**Use when** you need pane dimensions, PID, current working directory, or
other metadata without reading the terminal content.
**Avoid when** you need the actual text — use {tooliconl}`capture-pane`.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "get_pane_info",
"arguments": {
"pane_id": "%0"
}
}
```
Response:
```json
{
"pane_id": "%0",
"pane_index": "0",
"pane_width": "80",
"pane_height": "24",
"pane_left": 0,
"pane_top": 0,
"pane_right": 79,
"pane_bottom": 23,
"pane_at_left": true,
"pane_at_right": true,
"pane_at_top": true,
"pane_at_bottom": true,
"pane_tty": "/dev/pts/5",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
Coordinates are window-relative cell offsets. `pane_left` / `pane_top` are
0-based and `pane_right` / `pane_bottom` are inclusive. The four
`pane_at_*` predicates account for `pane-border-status`. To target a
layout-relative pane (e.g. "the bottom-right pane") use
{tooliconl}`find-pane-by-position` instead of computing edges yourself.
```{fastmcp-tool-input} pane_tools.get_pane_info
```
---
# Pane tools
Source: https://libtmux-mcp.git-pull.com/tools/pane/
# Pane tools
Pane-scoped tools — read and drive individual terminals, wait for output, copy mode, and channel sync.
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} {tooliconl}`capture-pane`
Read visible or scrollback text from a pane.
:::
:::{grid-item-card} {tooliconl}`capture-since`
Start a cursor, then read only new pane output.
:::
:::{grid-item-card} {tooliconl}`search-panes`
Search text across many panes in one call.
:::
:::{grid-item-card} {tooliconl}`snapshot-pane`
Capture content plus cursor, mode, and scroll state in one call.
:::
:::{grid-item-card} {tooliconl}`get-pane-info`
Read pane metadata without content.
:::
:::{grid-item-card} {tooliconl}`find-pane-by-position`
Find the pane at a given corner of a window.
:::
:::{grid-item-card} {tooliconl}`display-message`
Evaluate a tmux format string against a target.
:::
:::{grid-item-card} {tooliconl}`send-keys`
Send keystrokes or commands to a pane.
:::
:::{grid-item-card} {tooliconl}`paste-text`
Paste multi-line text via tmux buffer.
:::
:::{grid-item-card} {tooliconl}`pipe-pane`
Fork pane output to a file or program.
:::
:::{grid-item-card} {tooliconl}`select-pane`
Switch focus to a pane.
:::
:::{grid-item-card} {tooliconl}`swap-pane`
Swap two panes' positions.
:::
:::{grid-item-card} {tooliconl}`set-pane-title`
Set a pane's human-readable title.
:::
:::{grid-item-card} {tooliconl}`clear-pane`
Clear a pane's scrollback.
:::
:::{grid-item-card} {tooliconl}`resize-pane`
Resize a pane.
:::
:::{grid-item-card} {tooliconl}`enter-copy-mode`
Enter tmux copy mode for scrollback navigation.
:::
:::{grid-item-card} {tooliconl}`exit-copy-mode`
Exit copy mode.
:::
:::{grid-item-card} {tooliconl}`wait-for-text`
Block until a pattern appears in a pane.
:::
:::{grid-item-card} {tooliconl}`wait-for-content-change`
Block until pane content changes.
:::
:::{grid-item-card} {tooliconl}`wait-for-channel`
Block until a tmux wait-for channel is signalled.
:::
:::{grid-item-card} {tooliconl}`signal-channel`
Signal a waiting channel.
:::
:::{grid-item-card} {tooliconl}`respawn-pane`
Restart a pane's process in place, preserving pane_id.
:::
:::{grid-item-card} {tooliconl}`kill-pane`
Terminate a pane. Destructive.
:::
::::
```{toctree}
:hidden:
:maxdepth: 1
capture-pane
capture-since
search-panes
snapshot-pane
get-pane-info
find-pane-by-position
display-message
send-keys
paste-text
pipe-pane
select-pane
swap-pane
set-pane-title
clear-pane
resize-pane
enter-copy-mode
exit-copy-mode
wait-for-text
wait-for-content-change
wait-for-channel
signal-channel
respawn-pane
kill-pane
```
---
# Kill pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/kill-pane/
# Kill pane
```{fastmcp-tool} pane_tools.kill_pane
```
**Use when** you're done with a specific terminal and want to remove it
without affecting sibling panes.
**Avoid when** you want to remove the entire window — use {tooliconl}`kill-window`.
**Side effects:** Destroys the pane. Not reversible.
**Example:**
```json
{
"tool": "kill_pane",
"arguments": {
"pane_id": "%1"
}
}
```
Response (string):
```text
Pane killed: %1
```
```{fastmcp-tool-input} pane_tools.kill_pane
```
---
# Paste text
Source: https://libtmux-mcp.git-pull.com/tools/pane/paste-text/
# Paste text
```{fastmcp-tool} pane_tools.paste_text
```
**Use when** you need to paste multi-line text into a pane — e.g. a code
block, a config snippet, or a heredoc. Uses tmux paste buffers for clean
multi-line input instead of sending text line-by-line via
{tooliconl}`send-keys`.
**Side effects:** Pastes text into the pane. With `bracket=true` (default),
uses bracketed paste mode so the terminal knows this is pasted text.
**Example:**
```json
{
"tool": "paste_text",
"arguments": {
"text": "def hello():\n print('world')\n",
"pane_id": "%0"
}
}
```
Response (string):
```text
Text pasted to pane %0
```
```{fastmcp-tool-input} pane_tools.paste_text
```
## Destroy
---
# Pipe pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/pipe-pane/
# Pipe pane
```{fastmcp-tool} pane_tools.pipe_pane
```
**Use when** you need to log pane output to a file — useful for monitoring
long-running processes or capturing output that scrolls past the visible
area.
**Avoid when** you only need a one-time capture — use {tooliconl}`capture-pane`
with `start`/`end` to read scrollback.
**Side effects:** Starts or stops piping output to a file. Call with
`output_path=null` to stop.
**Example:**
```json
{
"tool": "pipe_pane",
"arguments": {
"pane_id": "%0",
"output_path": "/tmp/build.log"
}
}
```
Response (start):
```text
Piping pane %0 to /tmp/build.log
```
**Stopping the pipe:**
```json
{
"tool": "pipe_pane",
"arguments": {
"pane_id": "%0",
"output_path": null
}
}
```
Response (stop):
```text
Piping stopped for pane %0
```
```{fastmcp-tool-input} pane_tools.pipe_pane
```
---
# Resize pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/resize-pane/
# Resize pane
```{fastmcp-tool} pane_tools.resize_pane
```
**Use when** you need to adjust pane dimensions.
**Side effects:** Changes pane size. May affect adjacent panes.
**Example:**
```json
{
"tool": "resize_pane",
"arguments": {
"pane_id": "%0",
"height": 15
}
}
```
Response:
```json
{
"pane_id": "%0",
"pane_index": "0",
"pane_width": "80",
"pane_height": "15",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} pane_tools.resize_pane
```
---
# Respawn pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/respawn-pane/
# Respawn pane
```{fastmcp-tool} pane_tools.respawn_pane
```
**Use when** a pane's shell or command has wedged (hung REPL, runaway
process, bad terminal mode) and you need a clean restart *without*
destroying the `pane_id` references other tools or callers may still
be holding. With `kill=True` (the default) tmux kills the current
process first; optional `shell` relaunches with a different command;
optional `start_directory` sets its cwd; optional `environment` adds
per-process env vars (one `-e KEY=VALUE` flag per entry).
**Avoid when** the pane genuinely needs to go away — use
{tooliconl}`kill-pane` instead. Also avoid when you want to change
the layout: `respawn-pane` preserves the pane in place.
**Side effects:** Kills the current process (with `kill=True`) and
starts a new one. **The `pane_id` is preserved** — that's the whole
point of the tool. `pane_pid` updates to the new process.
**Tip:** Call {tooliconl}`get-pane-info` first if you need to capture
`pane_current_command` before respawn — the new process loses its argv.
Omitting `shell` makes tmux replay the original argv (good default for
shells; may differ for processes spawned via custom shell at split
time).
**Example — recover a wedged pane, relaunching the default shell:**
```json
{
"tool": "respawn_pane",
"arguments": {
"pane_id": "%5"
}
}
```
**Example — relaunch with a different command and working directory:**
```json
{
"tool": "respawn_pane",
"arguments": {
"pane_id": "%5",
"shell": "pytest -x",
"start_directory": "/home/user/project"
}
}
```
**Example — relaunch with extra environment variables:**
```json
{
"tool": "respawn_pane",
"arguments": {
"pane_id": "%5",
"shell": "pytest -x",
"environment": {
"PYTHONPATH": "/home/user/project/src",
"DATABASE_URL": "postgres://localhost/test"
}
}
}
```
The audit log redacts each `environment` *value* via `{len, sha256_prefix}` digests but keeps the keys visible (env var names like `DATABASE_URL` are operator-debug-useful, while their values are the secret). Note that values may still appear briefly in the OS process table while tmux spawns the new process — see {ref}`safety` for details.
Response (PaneInfo):
```json5
{
"pane_id": "%5",
"pane_pid": "98765",
"pane_current_command": "pytest",
"pane_current_path": "/home/user/project",
// ...
}
```
```{fastmcp-tool-input} pane_tools.respawn_pane
```
---
# Search panes
Source: https://libtmux-mcp.git-pull.com/tools/pane/search-panes/
# Search panes
```{fastmcp-tool} pane_tools.search_panes
```
**Use when** you need to find specific text across multiple panes — locating
which pane has an error, finding a running process, or checking output
without knowing which pane to look in.
**Avoid when** you already know the target pane — use {tooliconl}`capture-pane`
for a one-shot read, or {tooliconl}`capture-since` for repeated observation.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "search_panes",
"arguments": {
"pattern": "FAIL",
"session_name": "dev"
}
}
```
Response is a `SearchPanesResult` wrapper: the matching panes live under
`matches`, and the wrapper fields (`truncated`, `truncated_panes`,
`total_panes_matched`, `offset`, `limit`) support pagination. For larger
result sets, iterate by re-calling with `offset += len(matches)`; stop when
`truncated == false` and `truncated_panes == []`.
Plain text searches with no content range use tmux's fast visual-row search.
That path is quick, but it cannot match text split by terminal wrapping. Pass
`regex=true` or a `content_start` / `content_end` range when long build,
test, or log lines may cross the pane's wrap column.
:::{note} Migrating from the flat-list shape
Earlier alpha releases returned a bare `list[PaneContentMatch]`.
Clients iterating the old shape directly (e.g. `for m in search_panes(...)`)
must switch to `for m in search_panes(...).matches`. See the
[CHANGES](../../../CHANGES) entry for context.
:::
```json
{
"matches": [
{
"pane_id": "%0",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"window_id": "@0",
"window_name": "editor",
"session_id": "$0",
"session_name": "dev",
"matched_lines": [
"FAIL: test_upload (AssertionError)",
"3 tests: 2 passed, 1 failed"
],
"is_caller": false
}
],
"truncated": false,
"truncated_panes": [],
"total_panes_matched": 1,
"offset": 0,
"limit": 500
}
```
```{fastmcp-tool-input} pane_tools.search_panes
```
---
# Select pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/select-pane/
# Select pane
```{fastmcp-tool} pane_tools.select_pane
```
**Use when** you need to focus a specific pane — by ID for a known target,
or by direction (`up`, `down`, `left`, `right`, `last`, `next`, `previous`)
to navigate a multi-pane layout.
**Side effects:** Changes the active pane in the window.
**Example:**
```json
{
"tool": "select_pane",
"arguments": {
"direction": "down",
"window_id": "@0"
}
}
```
Response:
```json
{
"pane_id": "%1",
"pane_index": "1",
"pane_width": "80",
"pane_height": "11",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12400",
"pane_title": "",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} pane_tools.select_pane
```
---
# Send keys
Source: https://libtmux-mcp.git-pull.com/tools/pane/send-keys/
# Send keys
```{fastmcp-tool} pane_tools.send_keys
```
**Use when** you need to type commands, press keys, or interact with a
terminal. This is the primary way to execute commands in tmux panes.
**Avoid when** you need to run something and immediately capture the result —
compose `tmux wait-for -S ` into the keys and call
{tooliconl}`wait-for-channel` for deterministic completion, or fall back to
{tooliconl}`wait-for-text` / {tooliconl}`wait-for-content-change` when you
must observe output the agent does not author.
**Side effects:** Sends keystrokes to the pane. If `enter` is true (default),
the command executes.
**Example:**
```json
{
"tool": "send_keys",
"arguments": {
"keys": "npm start",
"pane_id": "%2"
}
}
```
Response (string):
```text
Keys sent to pane %2
```
```{fastmcp-tool-input} pane_tools.send_keys
```
---
# Set pane title
Source: https://libtmux-mcp.git-pull.com/tools/pane/set-pane-title/
# Set pane title
```{fastmcp-tool} pane_tools.set_pane_title
```
**Use when** you want to label a pane for identification.
**Side effects:** Changes the pane title.
**Example:**
```json
{
"tool": "set_pane_title",
"arguments": {
"pane_id": "%0",
"title": "build"
}
}
```
Response:
```json
{
"pane_id": "%0",
"pane_index": "0",
"pane_width": "80",
"pane_height": "24",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "build",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} pane_tools.set_pane_title
```
---
# Signal channel
Source: https://libtmux-mcp.git-pull.com/tools/pane/signal-channel/
# Signal channel
```{fastmcp-tool} wait_for_tools.signal_channel
```
**Use when** you need to wake a blocked {tooliconl}`wait-for-channel`
caller from a different MCP context (e.g. when a long-running task in
one pane completes and another pane should proceed). Signalling an
unwaited channel is a successful no-op — safe to call defensively.
**Side effects:** Wakes any clients blocked on the named channel.
Doesn't allocate or persist state.
```{fastmcp-tool-input} wait_for_tools.signal_channel
```
---
# Snapshot pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/snapshot-pane/
# Snapshot pane
```{fastmcp-tool} pane_tools.snapshot_pane
```
**Use when** you need a complete picture of a pane in a single call — visible
text plus cursor position, whether the pane is in copy mode, scroll offset,
and scrollback history size. Replaces separate `capture_pane` +
`get_pane_info` calls when you need to reason about cursor location or
terminal mode.
**Avoid when** you only need raw text — {tooliconl}`capture-pane` is lighter.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "snapshot_pane",
"arguments": {
"pane_id": "%0"
}
}
```
Response:
```json
{
"pane_id": "%0",
"content": "$ npm test\n\nPASS src/auth.test.ts\nTests: 3 passed\n$",
"cursor_x": 2,
"cursor_y": 4,
"pane_width": 80,
"pane_height": 24,
"pane_left": 0,
"pane_top": 0,
"pane_right": 79,
"pane_bottom": 23,
"pane_at_left": true,
"pane_at_right": true,
"pane_at_top": true,
"pane_at_bottom": true,
"pane_tty": "/dev/pts/5",
"pane_in_mode": false,
"pane_mode": null,
"scroll_position": null,
"history_size": 142,
"title": null,
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"is_caller": false
}
```
The geometry block (`pane_left` / `pane_top` / `pane_right` /
`pane_bottom` and the four `pane_at_*` predicates) is fetched in the
same `display-message` round-trip as the cursor and mode fields, so
there is no extra tmux call. To target a layout-relative pane (e.g.
"the bottom-right pane") use {tooliconl}`find-pane-by-position`
instead of computing edges from this snapshot.
```{fastmcp-tool-input} pane_tools.snapshot_pane
```
---
# Swap pane
Source: https://libtmux-mcp.git-pull.com/tools/pane/swap-pane/
# Swap pane
```{fastmcp-tool} pane_tools.swap_pane
```
**Use when** you want to rearrange pane positions without changing content —
e.g. moving a log pane from bottom to top.
**Side effects:** Exchanges the visual positions of two panes.
**Example:**
```json
{
"tool": "swap_pane",
"arguments": {
"source_pane_id": "%0",
"target_pane_id": "%1"
}
}
```
Response:
```json
{
"pane_id": "%0",
"pane_index": "1",
"pane_width": "80",
"pane_height": "11",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} pane_tools.swap_pane
```
---
# Wait for channel
Source: https://libtmux-mcp.git-pull.com/tools/pane/wait-for-channel/
# Wait for channel
tmux's `wait-for` command exposes named, server-global channels that clients can signal and block on. These give agents an explicit synchronization primitive — strictly cheaper in agent turns than polling pane content via {tooliconl}`capture-pane` or {tooliconl}`wait-for-text`.
The composition pattern: {tooliconl}`send-keys` a command followed by `; tmux wait-for -S NAME`, then call `wait_for_channel`. Shell `;` semantics fire the second statement whether the first succeeds or fails, so the edge-triggered signal never deadlocks the agent on a crashed command.
```python
send_keys(
pane_id="%1",
keys="pytest; tmux wait-for -S tests_done",
)
wait_for_channel("tests_done", timeout=60)
```
The `; tmux wait-for -S NAME` suffix is the load-bearing safety contract — `wait-for` is edge-triggered, so a crash before the signal would deadlock until the wait's `timeout`. The shell separator `;` runs the next statement unconditionally, so the signal fires on both success and failure paths.
The payload deliberately does not append `exit $?` — in an interactive shell that exits the shell itself, taking single-pane sessions down with it. If exit-status preservation matters, capture the status out-of-band (e.g. write it to a file the agent reads later, or use a dedicated scratch pane).
```{fastmcp-tool} wait_for_tools.wait_for_channel
```
**Use when** the shell command can reliably emit the signal (single
test runs, build scripts, dev-server boot, anything composable with
`; tmux wait-for -S name`).
**Avoid when** the signal cannot be guaranteed — for example, when
the command might be killed externally. Use {tooliconl}`wait-for-text`
to poll for an output marker instead; state-polling is structurally
safer than edge-triggered signalling for fragile commands.
**Side effects:** Blocks the call up to `timeout` seconds (default 30).
Mandatory subprocess timeout — a crashed signaller raises an
{exc}`~libtmux_mcp._utils.ExpectedToolError` rather than blocking
indefinitely.
```{fastmcp-tool-input} wait_for_tools.wait_for_channel
```
---
# Wait for content change
Source: https://libtmux-mcp.git-pull.com/tools/pane/wait-for-content-change/
# Wait for content change
```{fastmcp-tool} pane_tools.wait_for_content_change
```
**Use when** you've sent a command and need to wait for *something* to happen,
but you don't know what the output will look like. Unlike
{tooliconl}`wait-for-text`, this waits for *any* screen change rather than a
specific pattern.
**Avoid when** you know the expected output — {tooliconl}`wait-for-text` is more
precise and avoids false positives from unrelated output.
**Side effects:** None. Readonly. Blocks until content changes or timeout.
Raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` if the pane dies or is respawned while waiting, because the
entry content baseline no longer describes the same pane process.
**Example:**
```json
{
"tool": "wait_for_content_change",
"arguments": {
"pane_id": "%0",
"timeout": 10
}
}
```
Response:
```json
{
"changed": true,
"pane_id": "%0",
"elapsed_seconds": 1.234
}
```
```{fastmcp-tool-input} pane_tools.wait_for_content_change
```
---
# Wait for text
Source: https://libtmux-mcp.git-pull.com/tools/pane/wait-for-text/
# Wait for text
```{fastmcp-tool} pane_tools.wait_for_text
```
**Use when** you need to block until specific output appears — waiting for a
server to start, a build to complete, or a prompt to return.
**Avoid when** the expected text may never appear — always set a reasonable
`timeout`. For repeated observation or tailing, use
{tooliconl}`capture-since`; for command completion you control, use
{tooliconl}`wait-for-channel`.
**Side effects:** None. Readonly. Blocks until text appears or timeout.
**Example:**
```json
{
"tool": "wait_for_text",
"arguments": {
"pattern": "Server listening",
"pane_id": "%2",
"timeout": 30
}
}
```
Response:
```json
{
"found": true,
"matched_lines": [
"Server listening on port 8000"
],
"pane_id": "%2",
"elapsed_seconds": 0.002,
"risk_band_warned": false
}
```
`risk_band_warned` is `true` when polling entered tmux's history-limit
trim-risk band. In that state, matching remains best-effort because older
scrollback can be discarded while the wait is active; use
{tooliconl}`wait-for-channel` for deterministic command completion.
```{fastmcp-tool-input} pane_tools.wait_for_text
```
---
# Create session
Source: https://libtmux-mcp.git-pull.com/tools/server/create-session/
# Create session
```{fastmcp-tool} server_tools.create_session
```
**Use when** you need a new isolated workspace. Sessions are the top-level
container — create one before creating windows or panes.
**Avoid when** a session with the target name already exists — check with
{tooliconl}`list-sessions` first, or the command will fail.
**Side effects:** Creates a new tmux session with one window and one pane.
**Example:**
```json
{
"tool": "create_session",
"arguments": {
"session_name": "dev"
}
}
```
Response:
```json
{
"session_id": "$1",
"session_name": "dev",
"window_count": 1,
"session_attached": "0",
"session_created": "1774521872",
"active_pane_id": "%0"
}
```
```{tip}
The returned ``active_pane_id`` is the pane ID (``%N``) of the
session's initial pane. It's guaranteed non-``None`` immediately
after ``create_session`` (libtmux always creates the session with
one initial pane), so you can target subsequent ``send_keys`` /
``split_window`` / ``capture_pane`` calls directly without a
follow-up {tooliconl}`list-panes` round-trip — saving an MCP call
in the most common "new session, then act on it" workflow.
```
```{fastmcp-tool-input} server_tools.create_session
```
---
# Get server info
Source: https://libtmux-mcp.git-pull.com/tools/server/get-server-info/
# Get server info
```{fastmcp-tool} server_tools.get_server_info
```
**Use when** you need to verify the tmux server is running, check its PID,
or inspect server-level state before creating sessions.
**Avoid when** you only need session names — use {tooliconl}`list-sessions`.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "get_server_info",
"arguments": {}
}
```
Response:
```json
{
"is_alive": true,
"socket_name": null,
"socket_path": null,
"session_count": 2,
"version": "3.6a"
}
```
```{fastmcp-tool-input} server_tools.get_server_info
```
---
# Server tools
Source: https://libtmux-mcp.git-pull.com/tools/server/
# Server tools
Server & process-level tools — discover sessions, control the tmux daemon, read/write server-scoped state.
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} {tooliconl}`list-sessions`
List all sessions on the tmux server.
:::
:::{grid-item-card} {tooliconl}`list-servers`
Discover tmux daemons on the system.
:::
:::{grid-item-card} {tooliconl}`get-server-info`
Query server process identity and version.
:::
:::{grid-item-card} {tooliconl}`create-session`
Create a new tmux session.
:::
:::{grid-item-card} {tooliconl}`kill-server`
Terminate the tmux daemon. Destructive.
:::
:::{grid-item-card} {tooliconl}`show-option`
Read a tmux option (server / session / window / pane scope).
:::
:::{grid-item-card} {tooliconl}`set-option`
Set a tmux option.
:::
:::{grid-item-card} {tooliconl}`show-environment`
Read the server's environment variables.
:::
:::{grid-item-card} {tooliconl}`set-environment`
Set an environment variable on the server.
:::
::::
```{toctree}
:hidden:
:maxdepth: 1
list-sessions
list-servers
get-server-info
create-session
kill-server
show-option
set-option
show-environment
set-environment
```
---
# Kill server
Source: https://libtmux-mcp.git-pull.com/tools/server/kill-server/
# Kill server
```{fastmcp-tool} server_tools.kill_server
```
**Use when** you need to tear down the entire tmux server. This kills every
session, window, and pane.
**Avoid when** you only need to remove one session — use {tooliconl}`kill-session`.
**Side effects:** Destroys everything. Not reversible.
**Example:**
```json
{
"tool": "kill_server",
"arguments": {}
}
```
Response (string):
```text
Server killed successfully
```
```{fastmcp-tool-input} server_tools.kill_server
```
---
# List servers
Source: https://libtmux-mcp.git-pull.com/tools/server/list-servers/
# List servers
```{fastmcp-tool} server_tools.list_servers
```
**Use when** you need to discover other live tmux servers on this
machine — for example, when an agent's tools were configured for the
default server but the user is also running a separate tmux for a
side project.
**Avoid when** you already know the socket name or path you want to
target — pass it directly to the tool that needs it via `socket_name`.
**Side effects:** None. Readonly. Stale socket files are filtered
via a kernel-fast UNIX `connect()` probe so the call stays under one
second even on machines with thousands of orphaned `tmux-/`
inodes.
**Scope:** Only servers under `${TMUX_TMPDIR:-/tmp}/tmux-/` are
discovered by the canonical scan. Custom `tmux -S /some/path/...`
daemons that live outside that directory must be supplied via
`extra_socket_paths`.
**Example:**
```json
{
"tool": "list_servers",
"arguments": {}
}
```
Response:
```json
[
{
"is_alive": true,
"socket_name": "default",
"socket_path": null,
"session_count": 3,
"version": "3.6a"
},
{
"is_alive": true,
"socket_name": "ci-runner",
"socket_path": null,
"session_count": 1,
"version": "3.6a"
}
]
```
To include a custom-path daemon:
```json
{
"tool": "list_servers",
"arguments": {
"extra_socket_paths": ["/home/user/.cache/tmux/socket"]
}
}
```
Paths that do not exist, are not sockets, or have no listener are
silently skipped.
```{fastmcp-tool-input} server_tools.list_servers
```
## Act
---
# List sessions
Source: https://libtmux-mcp.git-pull.com/tools/server/list-sessions/
# List sessions
```{fastmcp-tool} server_tools.list_sessions
```
**Use when** you need session names, IDs, or attached status before deciding
which session to target.
**Avoid when** you need window or pane details — use {tooliconl}`list-windows` or
{tooliconl}`list-panes` instead.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "list_sessions",
"arguments": {}
}
```
Response:
```json
[
{
"session_id": "$0",
"session_name": "myproject",
"window_count": 2,
"session_attached": "0",
"session_created": "1774521871"
}
]
```
```{fastmcp-tool-input} server_tools.list_sessions
```
---
# Set environment
Source: https://libtmux-mcp.git-pull.com/tools/server/set-environment/
# Set environment
```{fastmcp-tool} env_tools.set_environment
```
**Use when** you need to set a tmux environment variable.
**Side effects:** Sets the variable in the tmux server.
**Example:**
```json
{
"tool": "set_environment",
"arguments": {
"name": "MY_VAR",
"value": "hello"
}
}
```
Response:
```json
{
"name": "MY_VAR",
"value": "hello",
"status": "set"
}
```
```{fastmcp-tool-input} env_tools.set_environment
```
---
# Set option
Source: https://libtmux-mcp.git-pull.com/tools/server/set-option/
# Set option
```{fastmcp-tool} option_tools.set_option
```
**Use when** you need to change tmux behavior — adjusting history limits,
enabling mouse support, changing status bar format.
**Side effects:** Changes the tmux option value.
**Example:**
```json
{
"tool": "set_option",
"arguments": {
"option": "history-limit",
"value": "50000"
}
}
```
Response:
```json
{
"option": "history-limit",
"value": "50000",
"status": "set"
}
```
```{fastmcp-tool-input} option_tools.set_option
```
---
# Show environment
Source: https://libtmux-mcp.git-pull.com/tools/server/show-environment/
# Show environment
```{fastmcp-tool} env_tools.show_environment
```
**Use when** you need to inspect tmux environment variables.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "show_environment",
"arguments": {}
}
```
Response:
```json
{
"variables": {
"SHELL": "/bin/zsh",
"TERM": "xterm-256color",
"HOME": "/home/user",
"USER": "user",
"LANG": "C.UTF-8"
}
}
```
```{fastmcp-tool-input} env_tools.show_environment
```
## Act
---
# Show option
Source: https://libtmux-mcp.git-pull.com/tools/server/show-option/
# Show option
```{fastmcp-tool} option_tools.show_option
```
**Use when** you need to check a tmux configuration value — buffer limits,
history size, status bar settings, etc.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "show_option",
"arguments": {
"option": "history-limit"
}
}
```
Response:
```json
{
"option": "history-limit",
"value": "2000"
}
```
```{fastmcp-tool-input} option_tools.show_option
```
---
# Create window
Source: https://libtmux-mcp.git-pull.com/tools/session/create-window/
# Create window
```{fastmcp-tool} session_tools.create_window
```
**Use when** you need a new terminal workspace within an existing session.
**Side effects:** Creates a new window. Attaches to it if `attach` is true.
**Example:**
```json
{
"tool": "create_window",
"arguments": {
"session_name": "dev",
"window_name": "logs"
}
}
```
Response:
```json
{
"window_id": "@2",
"window_name": "logs",
"window_index": "3",
"session_id": "$0",
"session_name": "dev",
"pane_count": 1,
"window_layout": "b25f,80x24,0,0,5",
"window_active": "1",
"window_width": "80",
"window_height": "24"
}
```
```{fastmcp-tool-input} session_tools.create_window
```
---
# Get session info
Source: https://libtmux-mcp.git-pull.com/tools/session/get-session-info/
# Get session info
```{fastmcp-tool} session_tools.get_session_info
```
**Use when** you need metadata for a single session (ID, name, window
count, attachment status, activity timestamp) and you already know its
`session_id` or `session_name`. Avoids the `list_sessions` + filter dance.
**Avoid when** you need every session — call `list_sessions` or iterate
via the `tmux://sessions` resource.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "get_session_info",
"arguments": {
"session_id": "$0"
}
}
```
Response:
```json
{
"session_id": "$0",
"session_name": "dev",
"window_count": 3,
"session_attached": "1",
"session_created": "1713600000",
"active_pane_id": "%0"
}
```
Resolve by name when only the session_name is known:
```json
{
"tool": "get_session_info",
"arguments": {
"session_name": "dev"
}
}
```
```{fastmcp-tool-input} session_tools.get_session_info
```
---
# Session tools
Source: https://libtmux-mcp.git-pull.com/tools/session/
# Session tools
Session-scoped tools — enumerate windows, rename or kill a session, switch windows within it.
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} {tooliconl}`list-windows`
Enumerate windows inside a session.
:::
:::{grid-item-card} {tooliconl}`get-session-info`
Read metadata for one session.
:::
:::{grid-item-card} {tooliconl}`select-window`
Switch to a window by id, index, or direction.
:::
:::{grid-item-card} {tooliconl}`create-window`
Create a new window inside a session.
:::
:::{grid-item-card} {tooliconl}`rename-session`
Rename an existing session.
:::
:::{grid-item-card} {tooliconl}`kill-session`
Terminate a session. Destructive.
:::
::::
```{toctree}
:hidden:
:maxdepth: 1
list-windows
get-session-info
select-window
create-window
rename-session
kill-session
```
---
# Kill session
Source: https://libtmux-mcp.git-pull.com/tools/session/kill-session/
# Kill session
```{fastmcp-tool} session_tools.kill_session
```
**Use when** you're done with a workspace and want to clean up. Kills all
windows and panes in the session.
**Avoid when** you only want to close one window — use {tooliconl}`kill-window`.
**Side effects:** Destroys the session and all its contents. Not reversible.
**Example:**
```json
{
"tool": "kill_session",
"arguments": {
"session_name": "old-workspace"
}
}
```
Response (string):
```text
Session killed: old-workspace
```
```{fastmcp-tool-input} session_tools.kill_session
```
---
# List windows
Source: https://libtmux-mcp.git-pull.com/tools/session/list-windows/
# List windows
```{fastmcp-tool} session_tools.list_windows
```
**Use when** you need window names, indices, or layout metadata within a
session before selecting a window to work with.
**Avoid when** you need pane-level detail — use {tooliconl}`list-panes`.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "list_windows",
"arguments": {
"session_name": "dev"
}
}
```
Response:
```json
[
{
"window_id": "@0",
"window_name": "editor",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 2,
"window_layout": "c195,80x24,0,0[80x12,0,0,0,80x11,0,13,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
},
{
"window_id": "@1",
"window_name": "server",
"window_index": "2",
"session_id": "$0",
"session_name": "dev",
"pane_count": 1,
"window_layout": "b25f,80x24,0,0,2",
"window_active": "0",
"window_width": "80",
"window_height": "24"
}
]
```
```{fastmcp-tool-input} session_tools.list_windows
```
---
# Rename session
Source: https://libtmux-mcp.git-pull.com/tools/session/rename-session/
# Rename session
```{fastmcp-tool} session_tools.rename_session
```
**Use when** a session name no longer reflects its purpose.
**Side effects:** Renames the session. Existing references by old name will break.
**Example:**
```json
{
"tool": "rename_session",
"arguments": {
"session_name": "old-name",
"new_name": "new-name"
}
}
```
Response:
```json
{
"session_id": "$0",
"session_name": "new-name",
"window_count": 2,
"session_attached": "0",
"session_created": "1774521871"
}
```
```{fastmcp-tool-input} session_tools.rename_session
```
---
# Select window
Source: https://libtmux-mcp.git-pull.com/tools/session/select-window/
# Select window
```{fastmcp-tool} session_tools.select_window
```
**Use when** you need to switch focus to a different window — by ID, index,
or direction (`next`, `previous`, `last`).
**Side effects:** Changes the active window in the session.
**Example:**
```json
{
"tool": "select_window",
"arguments": {
"direction": "next",
"session_name": "dev"
}
}
```
Response:
```json
{
"window_id": "@1",
"window_name": "server",
"window_index": "2",
"session_id": "$0",
"session_name": "dev",
"pane_count": 1,
"window_layout": "b25f,80x24,0,0,2",
"window_active": "1",
"window_width": "80",
"window_height": "24"
}
```
```{fastmcp-tool-input} session_tools.select_window
```
## Destroy
---
# Get window info
Source: https://libtmux-mcp.git-pull.com/tools/window/get-window-info/
# Get window info
```{fastmcp-tool} window_tools.get_window_info
```
**Use when** you need metadata for a single window (name, index, layout,
dimensions, pane count) and you already know the `window_id` or
`window_index`. Avoids the `list_windows` + filter dance.
**Avoid when** you need every window in a session — call `list_windows` with
`session_id` or iterate via the `tmux://sessions/{name}/windows` resource.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "get_window_info",
"arguments": {
"window_id": "@1"
}
}
```
Response:
```json
{
"window_id": "@1",
"window_name": "editor",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 2,
"window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
}
```
Resolve by `window_index` when only the index is known — requires
`session_name` or `session_id` to disambiguate:
```json
{
"tool": "get_window_info",
"arguments": {
"window_index": "1",
"session_name": "dev"
}
}
```
```{fastmcp-tool-input} window_tools.get_window_info
```
---
# Window tools
Source: https://libtmux-mcp.git-pull.com/tools/window/
# Window tools
Window-scoped tools — enumerate panes, split / rename / relayout / resize / move / kill windows.
::::{grid} 1 2 2 3
:gutter: 2 2 3 3
:::{grid-item-card} {tooliconl}`list-panes`
Enumerate panes inside a window.
:::
:::{grid-item-card} {tooliconl}`get-window-info`
Read metadata for one window.
:::
:::{grid-item-card} {tooliconl}`split-window`
Split a window into a new pane.
:::
:::{grid-item-card} {tooliconl}`rename-window`
Rename an existing window.
:::
:::{grid-item-card} {tooliconl}`select-layout`
Apply one of tmux's named layouts.
:::
:::{grid-item-card} {tooliconl}`resize-window`
Resize a window (client view).
:::
:::{grid-item-card} {tooliconl}`move-window`
Reorder a window or move it across sessions.
:::
:::{grid-item-card} {tooliconl}`kill-window`
Terminate a window. Destructive.
:::
::::
```{toctree}
:hidden:
:maxdepth: 1
list-panes
get-window-info
split-window
rename-window
select-layout
resize-window
move-window
kill-window
```
---
# Kill window
Source: https://libtmux-mcp.git-pull.com/tools/window/kill-window/
# Kill window
```{fastmcp-tool} window_tools.kill_window
```
**Use when** you're done with a window and all its panes.
**Avoid when** you only want to remove one pane — use {tooliconl}`kill-pane`.
**Side effects:** Destroys the window and all its panes. Not reversible.
**Example:**
```json
{
"tool": "kill_window",
"arguments": {
"window_id": "@1"
}
}
```
Response (string):
```text
Window killed: @1
```
```{fastmcp-tool-input} window_tools.kill_window
```
---
# List panes
Source: https://libtmux-mcp.git-pull.com/tools/window/list-panes/
# List panes
```{fastmcp-tool} window_tools.list_panes
```
**Use when** you need to discover which panes exist in a window before
sending keys or capturing output.
**Side effects:** None. Readonly.
**Example:**
```json
{
"tool": "list_panes",
"arguments": {
"session_name": "dev"
}
}
```
Response:
```json
[
{
"pane_id": "%0",
"pane_index": "0",
"pane_width": "80",
"pane_height": "15",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12345",
"pane_title": "build",
"pane_active": "1",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
},
{
"pane_id": "%1",
"pane_index": "1",
"pane_width": "80",
"pane_height": "8",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "12400",
"pane_title": "",
"pane_active": "0",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
]
```
```{fastmcp-tool-input} window_tools.list_panes
```
## Act
---
# Move window
Source: https://libtmux-mcp.git-pull.com/tools/window/move-window/
# Move window
```{fastmcp-tool} window_tools.move_window
```
**Use when** you need to reorder windows within a session or move a window
to a different session entirely.
**Side effects:** Changes the window's index or parent session.
**Example:**
```json
{
"tool": "move_window",
"arguments": {
"window_id": "@1",
"destination_index": "1"
}
}
```
Response:
```json
{
"window_id": "@1",
"window_name": "server",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 1,
"window_layout": "b25f,80x24,0,0,2",
"window_active": "0",
"window_width": "80",
"window_height": "24"
}
```
```{fastmcp-tool-input} window_tools.move_window
```
## Destroy
---
# Rename window
Source: https://libtmux-mcp.git-pull.com/tools/window/rename-window/
# Rename window
```{fastmcp-tool} window_tools.rename_window
```
**Use when** a window name no longer reflects its purpose.
**Side effects:** Renames the window.
**Example:**
```json
{
"tool": "rename_window",
"arguments": {
"session_name": "dev",
"new_name": "build"
}
}
```
Response:
```json
{
"window_id": "@0",
"window_name": "build",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 2,
"window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
}
```
```{fastmcp-tool-input} window_tools.rename_window
```
---
# Resize window
Source: https://libtmux-mcp.git-pull.com/tools/window/resize-window/
# Resize window
```{fastmcp-tool} window_tools.resize_window
```
**Use when** you need to adjust the window dimensions.
**Side effects:** Changes window size.
**Example:**
```json
{
"tool": "resize_window",
"arguments": {
"session_name": "dev",
"width": 120,
"height": 40
}
}
```
Response:
```json
{
"window_id": "@0",
"window_name": "editor",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 2,
"window_layout": "baaa,120x40,0,0[120x20,0,0,0,120x19,0,21,1]",
"window_active": "1",
"window_width": "120",
"window_height": "40"
}
```
```{fastmcp-tool-input} window_tools.resize_window
```
---
# Select layout
Source: https://libtmux-mcp.git-pull.com/tools/window/select-layout/
# Select layout
```{fastmcp-tool} window_tools.select_layout
```
**Use when** you want to rearrange panes — `even-horizontal`,
`even-vertical`, `main-horizontal`, `main-vertical`, or `tiled`.
**Side effects:** Rearranges all panes in the window.
**Example:**
```json
{
"tool": "select_layout",
"arguments": {
"session_name": "dev",
"layout": "even-vertical"
}
}
```
Response:
```json
{
"window_id": "@0",
"window_name": "editor",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 2,
"window_layout": "even-vertical,80x24,0,0[80x12,0,0,0,80x11,0,13,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
}
```
```{fastmcp-tool-input} window_tools.select_layout
```
---
# Split window
Source: https://libtmux-mcp.git-pull.com/tools/window/split-window/
# Split window
```{fastmcp-tool} window_tools.split_window
```
**Use when** you need side-by-side or stacked terminals within the same
window.
**Side effects:** Creates a new pane by splitting an existing one.
**Example:**
```json
{
"tool": "split_window",
"arguments": {
"session_name": "dev",
"direction": "right"
}
}
```
Response:
```json
{
"pane_id": "%4",
"pane_index": "1",
"pane_width": "39",
"pane_height": "24",
"pane_current_command": "zsh",
"pane_current_path": "/home/user/myproject",
"pane_pid": "3732",
"pane_title": "",
"pane_active": "0",
"window_id": "@0",
"session_id": "$0",
"is_caller": false
}
```
```{fastmcp-tool-input} window_tools.split_window
```
---
# Architecture
Source: https://libtmux-mcp.git-pull.com/topics/architecture/
(architecture)=
# Architecture
For contributors who need to understand the codebase internals.
## Source layout
```
src/libtmux_mcp/
__init__.py # Entry point: main()
__main__.py # python -m libtmux_mcp support
server.py # FastMCP instance and configuration
_utils.py # Server caching, resolvers, serializers, error handling
models.py # Pydantic output models
middleware.py # Safety, audit, retry, and error-result middleware
tools/
server_tools.py # list_sessions, create_session, kill_server, get_server_info
session_tools.py # list_windows, create_window, rename_session, kill_session
window_tools.py # list_panes, split_window, rename_window, kill_window, select_layout, resize_window
pane_tools.py # send_keys, capture_pane, capture_since, resize_pane, kill_pane, set_pane_title, get_pane_info, clear_pane, search_panes, wait_for_text
option_tools.py # show_option, set_option
env_tools.py # show_environment, set_environment
resources/
hierarchy.py # tmux:// URI resources
```
## Request flow
Middleware wraps tool calls outermost-first (full ordering rationale in
the `server.py` stack comment):
```
MCP Client (Claude, Cursor, etc.)
→ stdio transport
→ FastMCP server (server.py)
→ TimingMiddleware (wall-time observer)
→ TailPreservingResponseLimitingMiddleware (response size backstop)
→ ToolErrorResultMiddleware (exceptions → is_error results)
→ AuditMiddleware (one log record per call)
→ ReadonlyRetryMiddleware (retries readonly tools only)
→ SafetyMiddleware (tier gate, fail-closed)
→ Tool function (tools/*.py)
→ libtmux Server/Session/Window/Pane
→ tmux binary (via subprocess)
```
## Key design decisions
### Tool registration
Each tool module defines a `register(mcp)` function that registers tools with metadata:
- `title` — human-readable name
- `annotations` — MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint)
- `tags` — safety tier tags for middleware filtering
### Server caching
`_utils.py` maintains a thread-safe cache keyed by `(socket_name, socket_path, tmux_bin)`. Dead servers are evicted on access via `is_alive()` checks.
### Object resolution
Tools use resolver functions (`_resolve_session`, `_resolve_window`, `_resolve_pane`) that accept multiple targeting parameters and resolve to the correct libtmux object. Resolution follows a priority chain: direct ID → name lookup → error.
### Safety middleware
`SafetyMiddleware` implements FastMCP's middleware interface. It operates as a secondary gate behind FastMCP's native tag visibility system, providing clear error messages when a tool above the configured tier is invoked.
### Error handling
Three boundaries split the work:
1. **Tool classification** — the {func}`~libtmux_mcp._utils.handle_tool_errors` decorator wraps tool functions, mapping libtmux exceptions to {exc}`~libtmux_mcp._utils.ExpectedToolError` (agent-correctable: unknown ids, invalid arguments, transient tmux errors; logged at WARNING) or stock `ToolError` (operator faults and unexpected bugs; logged at ERROR). The raise chains the original exception via `from e`, which is what lets {class}`~libtmux_mcp.middleware.ReadonlyRetryMiddleware` match transient `LibTmuxException` causes.
2. **Schema classification** — FastMCP validates tool arguments before tool code runs, so Pydantic validation failures never reach the decorator. {class}`~libtmux_mcp.middleware.ToolErrorResultMiddleware` classifies those schema-validation errors as expected, agent-correctable WARNINGs before converting them.
3. **Conversion** — {class}`~libtmux_mcp.middleware.ToolErrorResultMiddleware` catches the exception once it has cleared the audit/retry/safety trio and returns `ToolResult(is_error=True)` carrying the message exactly as raised, plus a `_meta` payload (`error_type`, `expected`, and an optional agent-facing `suggestion` for recovery hints such as discovery tools or rejected-argument fixes).
Errors must stay exceptions through the audit/retry/safety trio — audit detects failures by catching, retry matches via `__cause__` — so conversion happens only in the outermost error layer. The response limiter sits outside conversion and may truncate large success or error results on the return path; its truncation path preserves `is_error` and `_meta` so oversized expected failures stay tool errors. Level policy lives in {doc}`/topics/logging`.
## References
- [libtmux](https://libtmux.git-pull.com/) — Core tmux Python library
- [FastMCP](https://github.com/jlowin/fastmcp) — MCP server framework
- [MCP Specification](https://modelcontextprotocol.io/) — Model Context Protocol
- [tmux man page](http://man.openbsd.org/OpenBSD-current/man1/tmux.1)
---
# Completion
Source: https://libtmux-mcp.git-pull.com/topics/completion/
(completion-overview)=
# Completion
libtmux-mcp inherits FastMCP's built-in
[MCP completion](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion)
behaviour. We don't hand-author completion providers — the argument
shapes on our prompts and resource templates are what the client
sees.
## What the spec does
A client calls ``completion/complete`` with a partial argument for a
prompt or resource URI template; the server replies with up to 100
suggestions. Agents use this to offer auto-complete UX — e.g. a
session picker popup when filling ``session_name=`` on
``get_session``.
## What libtmux-mcp currently exposes
- **Prompt arguments** — the four recipes ({doc}`/prompts`)
advertise their argument names and types. FastMCP derives a default
completion shape from the Python signatures:
``str`` arguments accept free text, ``float`` arguments accept
numeric strings, no enum / list suggestions.
- **Resource template parameters** —
{doc}`/resources` URIs carry ``{session_name}``,
``{window_index}``, ``{pane_id}``, and ``{?socket_name}``
placeholders. Completion suggestions are again derived from the
function signatures' types, not from live tmux state.
```{warning}
libtmux-mcp does **not** currently wire completion back to live
tmux enumeration — i.e. the completion for ``session_name`` will not
return the names of sessions that exist on the server right now.
Adding that requires a dedicated FastMCP completion handler;
tracked as a potential enhancement.
```
## Workarounds for clients that need live enumeration
Agents that need to pick a real session / window / pane can call
{tool}`list-sessions`, {tool}`list-windows`, or {tool}`list-panes`
directly before rendering a prompt, then feed the chosen ID back
into the prompt's arguments.
## Further reading
- [MCP completion spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion)
- {doc}`/prompts` — the prompt argument surface
- {doc}`/resources` — the resource-template parameter surface
---
# Concepts
Source: https://libtmux-mcp.git-pull.com/topics/concepts/
(concepts)=
# Concepts
The mental model you need to use libtmux-mcp effectively.
## tmux hierarchy
tmux organizes terminals in a strict hierarchy:
```
Server (tmux server instance)
└─ Session (named group of windows, e.g. "dev")
└─ Window (tab within a session, e.g. "editor")
└─ Pane (terminal split within a window)
```
libtmux-mcp mirrors this hierarchy. Every tool operates on one of these objects.
## Identifiers
Each tmux object has identifiers you can use to target it:
| Object | ID format | Name | Index |
|--------|----------|------|-------|
| Session | `$1`, `$2` | `"dev"`, `"build"` | — |
| Window | `@1`, `@2` | `"editor"`, `"tests"` | `0`, `1`, `2` |
| Pane | `%1`, `%2` | — | — |
**Pane IDs are globally unique** within a tmux server and are the preferred targeting method. When you know the pane ID, use it directly — no session or window context needed.
Session names and window names are human-readable but may not be unique. Window indexes are unique within a session.
## Targeting
Most tools accept multiple targeting parameters. The resolution order is:
1. **Direct ID** — `pane_id`, `window_id`, or `session_id` (fastest, unambiguous)
2. **Name lookup** — `session_name` + optional `window_index` (convenient but may be ambiguous)
3. **Default** — If no targeting parameter is given, tools that need a single object will fail; list tools return everything
For pane tools, you can combine parameters to narrow the search: `session_name` + `window_id` → find the pane in that specific window.
## Discovery vs. mutation
Tools fall into three categories:
- **Discovery** — Read-only operations: `list_sessions`, `list_windows`, `list_panes`, `capture_pane`, `capture_since`, `get_pane_info`, `find_pane_by_position`, `search_panes`, `wait_for_text`, `show_option`, `show_environment`
- **Mutation** — Create, modify, or send input: `create_session`, `create_window`, `split_window`, `send_keys`, `rename_*`, `resize_*`, `set_pane_title`, `clear_pane`, `select_layout`, `set_option`, `set_environment`
- **Destruction** — Remove tmux objects: `kill_server`, `kill_session`, `kill_window`, `kill_pane`
These map to {ref}`safety tiers `.
## Agent self-awareness
When the MCP server runs inside a tmux pane (detected via the `TMUX_PANE` environment variable), it:
- Includes the caller's pane context in server instructions
- Annotates the caller's own pane with `is_caller=true` in tool results
- Prevents destructive tools from killing the caller's own pane, window, session, or server
This means agents can safely explore and manage tmux without accidentally terminating themselves.
## Server caching
Server instances are cached by `(socket_name, socket_path, tmux_bin)` tuple with thread-safe access. Dead servers are automatically evicted via `is_alive()` checks. This means multiple tool calls reuse the same server connection efficiently.
## Filtering
List tools (`list_sessions`, `list_windows`, `list_panes`) support Django-style filters:
```json
{"session_name__contains": "dev"}
```
Supported operators: `exact`, `contains`, `startswith`, `endswith`, `regex`, `icontains`, `iexact`, `istartswith`, `iendswith`, `iregex`.
## Resources
In addition to tools, the MCP server exposes `tmux://` URI resources for browsing the hierarchy:
- `tmux://sessions` — All sessions
- `tmux://sessions/{session_name}` — Session detail with windows
- `tmux://sessions/{session_name}/windows` — Session's windows
- `tmux://sessions/{session_name}/windows/{window_index}` — Window detail with panes
- `tmux://panes/{pane_id}` — Pane details
- `tmux://panes/{pane_id}/content` — Pane captured content
---
# Gotchas
Source: https://libtmux-mcp.git-pull.com/topics/gotchas/
(gotchas)=
# Gotchas
Things that will bite you if you don't know about them in advance. For symptom-based debugging, see {ref}`troubleshooting `.
## Metadata vs. content
{tooliconl}`list-panes` and {tooliconl}`list-windows` search **metadata** — names, IDs, current command. They do not search what is displayed in the terminal.
To find text that is visible in terminals, use {tooliconl}`search-panes`. To read what a specific pane shows once, use {tooliconl}`capture-pane`; to keep watching that pane, use {tooliconl}`capture-since`.
This is the most common source of agent confusion. The server instructions already warn about this, but it bears repeating: if a user asks "which pane mentions error", the answer is `search_panes`, not `list_panes`.
## `send_keys` sends Enter by default
When you call `send_keys` with `keys: "C-c"`, it sends Ctrl-C **and then presses Enter**. For control sequences, set `enter: false`:
```json
{"tool": "send_keys", "arguments": {"keys": "C-c", "pane_id": "%0", "enter": false}}
```
The `enter` parameter defaults to `true`, which is correct for commands (`make test` + Enter) but wrong for control keys, partial input, or key sequences.
## `capture_pane` after `send_keys` is a race condition
`send_keys` returns immediately after sending keystrokes to tmux. It does **not** wait for the command to execute or produce output.
```json
{"tool": "send_keys", "arguments": {"keys": "pytest", "pane_id": "%0"}}
{"tool": "capture_pane", "arguments": {"pane_id": "%0"}}
```
The capture above may return the terminal state **before** pytest runs. Compose `tmux wait-for -S ` into the command and block on {tooliconl}`wait-for-channel` — deterministic, race-free:
```json
{"tool": "send_keys", "arguments": {"keys": "pytest; tmux wait-for -S pytest_done", "pane_id": "%0"}}
{"tool": "wait_for_channel", "arguments": {"channel": "pytest_done", "timeout": 60}}
{"tool": "capture_pane", "arguments": {"pane_id": "%0"}}
```
For output the agent does not author (third-party logs, daemon prompts, interactive supervisors), substitute {tooliconl}`wait-for-text` for `wait_for_channel`. See {ref}`recipes` for the complete pattern.
## Repeated `capture_pane` calls resend old output
If you are tailing a pane or checking a long-running process over several
turns, repeated {tooliconl}`capture-pane` calls keep returning the same visible
screen and scrollback. Use {tooliconl}`capture-since` instead: the first call
returns a cursor, and follow-up calls return only output written or rewritten
after that cursor. If tmux has already trimmed or cleared the needed history,
the result marks `lines_missed=true` and gives you a fresh cursor.
## Window names are not unique across sessions
Two sessions can each have a window named "editor". Targeting by `window_name` alone is ambiguous — always include `session_name` or use the globally unique `window_id` (e.g., `@0`, `@1`).
Pane IDs (`%0`, `%1`, etc.) are globally unique and are the preferred targeting method.
## Pane IDs are globally unique but ephemeral
Pane IDs like `%0`, `%5`, `%12` are unique across all sessions and windows within a tmux server. They do not reset when windows are created or destroyed.
However, they reset when the tmux **server** restarts. Do not cache pane IDs across server restarts. After killing and recreating a session, re-discover pane IDs with {ref}`list-panes`.
## `suppress_history` requires shell support
The `suppress_history` parameter on `send_keys` prepends a space before the command, which prevents it from being saved in shell history. This only works if the shell's `HISTCONTROL` variable includes `ignorespace` (the default for bash, but not universal across all shells).
## `clear_pane` is not fully atomic
`clear_pane` runs two tmux commands in sequence: `send-keys -R` (reset terminal) then `clear-history` (clear scrollback). There is a brief gap between them where partial content may be visible.
For most use cases this is not a problem. If you need guaranteed clean state, add a small delay before the next `capture_pane`.
## Gemini CLI injects `wait_for_previous` into tool arguments
When Gemini CLI batches several tool calls in one turn, its scheduler merges the internal sequencing flag of the later calls into the MCP tool's arguments:
```json
{"tool": "get_pane_info", "arguments": {"wait_for_previous": true, "pane_id": "%0"}}
```
This has been observed with stock Gemini CLI behavior (no extensions involved). In local non-interactive `gemini -p` harness runs, batching can front-load the topic tool and the first MCP calls into one parallel turn, which is where the leaked flag usually appears; interactive sessions tend to schedule more sequentially and trigger it less often.
Tool schemas are strict (`additionalProperties: false`), so the call is rejected with a validation error — classified as expected (WARNING log, `expected: true` in the result's `_meta`) and carrying a suggestion that names the rejected argument and identifies `wait_for_previous` as a client scheduling flag to retry without. Gemini's model reads it, drops the flag, and retries successfully on its own.
The visible symptom is harmless noise: `Error executing tool mcp_tmux_: ... reported an error` lines in Gemini's output for calls that then succeed on retry, and matching WARNING records in the server log. Similar reports in other MCP servers have handled this injected key by stripping it or whitelisting arguments against the schema ([MemPalace/mempalace#816](https://github.com/MemPalace/mempalace/issues/816)). This server deliberately keeps the rejection: silently dropping unknown arguments would also swallow genuine argument typos from every client — on a server with mutating and destructive tools, a mis-named flag (`enter` on `send_keys`, say) must fail loudly, not run with defaults.
---
# Topics
Source: https://libtmux-mcp.git-pull.com/topics/
# Topics
Explore libtmux-mcp's core ideas and design at a high level.
::::{grid} 1 1 2 2
:gutter: 2 2 3 3
:::{grid-item-card} Architecture
:link: architecture
:link-type: doc
Source layout, request flow, and extension points.
:::
:::{grid-item-card} Concepts
:link: concepts
:link-type: doc
tmux hierarchy, MCP protocol, and the mental model.
:::
:::{grid-item-card} Safety Tiers
:link: safety
:link-type: doc
Three-tier safety system for controlling tool access.
:::
:::{grid-item-card} Troubleshooting
:link: troubleshooting
:link-type: doc
Symptom-based guide for common issues.
:::
:::{grid-item-card} Gotchas
:link: gotchas
:link-type: doc
Things that will bite you if you don't know about them.
:::
:::{grid-item-card} Agent Prompting
:link: prompting
:link-type: doc
Write effective instructions for AI agents using tmux tools.
:::
::::
## MCP protocol utilities
How libtmux-mcp maps to three optional utility capabilities from
the Model Context Protocol specification.
::::{grid} 1 1 3 3
:gutter: 2 2 3 3
:::{grid-item-card} Completion
:link: completion
:link-type: doc
Argument auto-complete — what FastMCP derives automatically and
what libtmux-mcp does not yet wire up.
:::
:::{grid-item-card} Logging
:link: logging
:link-type: doc
Server-to-client log forwarding and the ``libtmux_mcp.*`` logger
hierarchy.
:::
:::{grid-item-card} Pagination
:link: pagination
:link-type: doc
Protocol cursors, ``search_panes`` paging, and ``capture_since``
observation cursors.
:::
::::
```{toctree}
:hidden:
architecture
concepts
safety
gotchas
prompting
completion
logging
pagination
troubleshooting
```
---
# Logging
Source: https://libtmux-mcp.git-pull.com/topics/logging/
(logging-overview)=
# Logging
libtmux-mcp uses Python's standard ``logging`` module under the
``libtmux_mcp.*`` namespace. FastMCP forwards server-side log
records to connected MCP clients via the
[MCP logging capability](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging).
No manual wiring needed.
## Logger hierarchy
All loggers are children of ``libtmux_mcp``. The primary streams
are:
- ``libtmux_mcp.audit`` — one structured line per tool call, emitted
by {class}`~libtmux_mcp.middleware.AuditMiddleware`. Includes
tool name, digest-redacted arguments, latency, outcome. See
{doc}`/topics/safety` for the argument-redaction rules.
- ``libtmux_mcp.retry`` — warnings from
{class}`~libtmux_mcp.middleware.ReadonlyRetryMiddleware` when a
readonly tool retried after a transient
{exc}`libtmux.exc.LibTmuxException`.
- ``libtmux_mcp.server`` / ``libtmux_mcp.tools.*`` / etc. — ad-hoc
warnings and debug messages from the codebase.
- ``fastmcp.errors`` — one record per failed tool call, emitted by
{class}`~libtmux_mcp.middleware.ToolErrorResultMiddleware`.
## Error levels
Tool failures are logged at a level matching who needs to act:
- **WARNING** — expected, agent-correctable failures: unknown
pane/window/session ids, invalid arguments, safety-tier denials,
transient tmux errors. The calling agent receives the message and
can correct course; operators don't need to.
- **ERROR** — operator faults and potential bugs: a missing ``tmux``
binary, or any unexpected exception escaping a tool.
A WARNING-heavy, ERROR-quiet log stream is therefore healthy — it
means agents are probing and recovering. ERROR records are the ones
worth alerting on.
## Level control
Set the logger level via standard Python mechanisms — for local
development, the simplest is an environment variable:
```console
$ FASTMCP_LOG_LEVEL=DEBUG libtmux-mcp
```
FastMCP reads ``FASTMCP_LOG_LEVEL`` at startup and applies it to
every ``fastmcp.*`` and ``libtmux_mcp.*`` logger.
## What clients see
MCP clients render incoming ``notifications/message`` records in
their log pane (e.g. Claude Desktop's "MCP server logs" panel, or
``claude-cli``'s ``--verbose`` output). The records include the
server name (``libtmux-mcp``), level, and the log message — but not
the Python logger name, which the protocol doesn't model.
```{tip}
If a tool call fails silently (no user-visible error, no side
effect), the ``libtmux_mcp.audit`` log will show the invocation and
its return value. That's usually the fastest way to tell whether a
tool ran at all.
```
## Further reading
- [MCP logging spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging)
- {doc}`/topics/safety` — audit log redaction rules
- {class}`~libtmux_mcp.middleware.AuditMiddleware` — the primary
audit emitter
---
# Pagination
Source: https://libtmux-mcp.git-pull.com/topics/pagination/
(pagination-overview)=
# Pagination
libtmux-mcp follows the
[MCP pagination spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination):
``tools/list``, ``prompts/list``, ``resources/list``, and
``resources/templates/list`` all return an opaque ``nextCursor`` when
a page is truncated, and accept ``cursor`` on the next call to
resume.
## Where cursors and pages show up
### Protocol-level list calls
FastMCP handles ``tools/list`` / ``prompts/list`` / ``resources/list``
/ ``resources/templates/list`` pagination automatically. Neither
libtmux-mcp nor the agent needs to do anything: the server chooses
a sensible page size, encodes the cursor in an opaque base64 blob,
and replays state from it. Callers only need to thread through
``nextCursor`` if they consume the raw MCP protocol.
### Tool-level result paging on ``search_panes``
One libtmux-mcp tool owns its own paging surface because a
single tmux server can carry tens of thousands of pane lines:
- {tool}`search-panes` returns a
{class}`~libtmux_mcp.models.SearchPanesResult` wrapper with
``matches``, ``truncated``, ``truncated_panes``,
``total_panes_matched``, ``offset``, and ``limit``.
- Agents detect ``truncated=True`` and re-call with a higher
``offset`` to page through the match set.
This is application-level paging (not MCP-cursor pagination) —
the agent decides how many matches it needs and when to stop.
### Tool-level observation cursors on ``capture_since``
{tool}`capture-since` also has a ``cursor`` parameter, but it is
not a pagination cursor. The first call captures the current visible
pane and returns an opaque observation checkpoint. Follow-up calls
pass that cursor back to receive only rows written or rewritten after
the checkpoint while tmux still retains the needed history.
Because the cursor points into live tmux grid state, it has different
failure modes from protocol pagination:
- If the pane output scrolls into retained history, the cursor can
still produce an exact delta.
- If tmux clears or trims the needed history, the response sets
``lines_missed=True`` and returns a conservative current visible
capture with a fresh cursor.
- If the pane dies or is respawned, the cursor is invalid because it
would otherwise point at a different process's terminal state.
This is application-level observation, not a stable collection scan.
Use it to reduce repeated pane reads, not to page through search
matches.
## Why separate paths
Protocol-level cursors are for **collections the server owns
end-to-end**: the tool / prompt / resource registries. The server
knows what it has, so an opaque cursor is cheap.
Tool-level paging and observation cursors are for **state derived
from live tmux panes**. Capturing every pane's contents and running a
regex is expensive, and the result set can change mid-scan (new panes
open, old ones close). Repeatedly reading one pane has the opposite
cost shape: the target is known, but unchanged scrollback wastes
model context. libtmux-mcp exposes each contract separately instead
of pretending live terminal state is one stable list.
## Further reading
- [MCP pagination spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination)
- {class}`~libtmux_mcp.models.SearchPanesResult` — the structured
wrapper for ``search_panes``
- {tool}`search-panes` — the tool itself
- {class}`~libtmux_mcp.models.CaptureSinceResult` — the structured
response for ``capture_since``
- {tool}`capture-since` — incremental observation for a known pane
---
# Agent prompting guide
Source: https://libtmux-mcp.git-pull.com/topics/prompting/
(prompting)=
# Agent prompting guide
How to write effective instructions for AI agents using libtmux-mcp.
## What the server tells your agent automatically
Every MCP client receives these instructions when connecting to the libtmux-mcp server. You do not need to repeat this information — the agent already knows it.
```{code-block} text
:class: server-prompt
libtmux MCP server for programmatic tmux control. tmux hierarchy:
Server > Session > Window > Pane. Use pane_id (e.g. '%1') as the
preferred targeting method - it is globally unique within a tmux server.
Use send_keys to execute commands, capture_pane for one-shot reads, and
capture_since for repeated observation. All tools accept an optional
socket_name parameter for multi-server support (defaults to
LIBTMUX_SOCKET env var).
IMPORTANT — metadata vs content: list_windows, list_panes, and
list_sessions only search metadata (names, IDs, current command). To
find text that is actually visible in terminals — when users ask what
panes 'contain', 'mention', 'show', or 'have' — use search_panes to
search across all pane contents, capture_since for repeated reads of a
known pane, or capture_pane for a one-shot manual inspection.
```
The server also dynamically adds:
- **Safety tier context**: Which tier is active and what tools are available
- **Caller pane awareness**: If the server runs inside tmux, it tells the agent which pane is its own (via `TMUX_PANE`). See {ref}`concepts` "Agent self-awareness" for details.
## Activation and discovery
This server is designed to fire on bare terminal-surface phrasing — *"split this pane"*, *"what's in my current window"*, *"start a session for the build"* — without you having to say "tmux". The server treats `pane`, `session`, `window`, and `split` as positive triggers; tmux ID prefixes (`%1`, `@1`, `$1`) are unambiguous.
The server stays out of the way when you mean a browser window, an editor split, an i3/Hyprland workspace, or a Jupyter notebook cell — if your phrasing is ambiguous, the agent will ask before acting.
### Always-on tool listing (Claude Code only)
[Claude Code](https://code.claude.com/docs/en/mcp) defers loading MCP tool schemas when they exceed ~10% of your context window. By default, the three discovery anchors ({toolref}`list-panes`, {toolref}`list-windows`, {toolref}`snapshot-pane`) carry an `anthropic/alwaysLoad: true` hint so bare *"pane"* / *"window"* prompts surface this server without a `ToolSearch` hop.
To force libtmux-mcp's *full* schema list to load upfront — useful if you also want {toolref}`send-keys`, {toolref}`capture-pane`, {toolref}`select-window` etc. preloaded — add `"alwaysLoad": true` at the server entry level (requires Claude Code v2.1.121+):
```json
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": ["libtmux-mcp"],
"alwaysLoad": true
}
}
}
```
Cost: ~3-5K tokens of permanent context budget. Use only if libtmux-mcp is one of your top-3 most-used MCPs.
## Effective prompt patterns
These natural-language prompts reliably trigger the right tool sequences:
| Prompt | Agent interprets as |
|--------|-------------------|
| [Run `pytest` in my build pane and show results]{.prompt} | {toolref}`send-keys` (with `tmux wait-for -S` composed in) → {toolref}`wait-for-channel` → {toolref}`capture-pane` |
| [Start the dev server and wait until it's ready]{.prompt} | {toolref}`send-keys` → {toolref}`wait-for-text` (for "listening on" — third-party output the agent doesn't author) |
| [Spin up the dev server in the bottom-right pane]{.prompt} | {toolref}`find-pane-by-position` (corner=bottom-right) → {toolref}`send-keys` → {toolref}`wait-for-text` (for the server's readiness banner) |
| [Check if any pane has errors]{.prompt} | {toolref}`search-panes` with pattern "error" |
| [Keep watching the server pane]{.prompt} | {toolref}`capture-since` with the previous cursor |
| [Set up a workspace with editor, server, and tests]{.prompt} | {toolref}`create-session` → {toolref}`split-window` (x2) → {toolref}`set-pane-title` (x3) |
| [What's running in my tmux sessions?]{.prompt} | {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`capture-pane` |
| [Kill the old workspace session]{.prompt} | {toolref}`kill-session` (after confirming target) |
## Anti-patterns to avoid
| Prompt | Problem | Better version |
|--------|---------|---------------|
| [Run this command]{.prompt} | Ambiguous — agent may use its own shell instead of tmux | [Run `make test` in a tmux pane]{.prompt} |
| [Check my terminal]{.prompt} | Which pane? Agent must discover first | [Check the pane running `npm dev`]{.prompt} or [Search all panes for errors]{.prompt} |
| [Clean up everything]{.prompt} | Too broad for destructive operations | [Kill the `ci-test` session]{.prompt} |
| [Show me the output]{.prompt} | Capture immediately? Or wait? | [Wait for the command to finish, then show me the output]{.prompt} |
## System prompt fragments
Copy these into your agent's system instructions (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or MCP client config) to improve behavior:
### For general tmux workflows
```{code-block} text
:class: system-prompt
When executing long-running commands (servers, builds, test suites),
use tmux via the libtmux MCP server rather than running them directly.
This keeps output accessible for later inspection.
For command completion, compose `tmux wait-for -S ` into the
shell command and call wait_for_channel — deterministic, no polling.
Use wait_for_text or wait_for_content_change for observation flows
(third-party logs, daemon prompts), and use capture_since when you
need to read the same pane repeatedly. Never capture_pane immediately
after send_keys — the command may still be running.
```
### For safe agent behavior
```{code-block} text
:class: system-prompt
Before creating tmux sessions, check list_sessions to avoid duplicates.
Always use pane_id for targeting — it is globally unique. Never run
destructive operations (kill_session, kill_server) without confirming
the target with the user first.
```
### For development workflows
```{code-block} text
:class: system-prompt
When the user asks you to run tests or start servers, use dedicated
tmux panes. Split windows to run related processes side-by-side.
Use wait_for_text to know when a server is ready before running tests
that depend on it.
```
### To bias activation on bare "pane" / "window" / "session"
```{code-block} text
:class: system-prompt
This project uses tmux for its development workflow. When the user
says "pane", "window", or "session" without further qualification,
prefer the tmux MCP tools (libtmux-mcp) before assuming GUI semantics.
```
This is the lever closest to the failure mode for *"current window"* / *"this session"* anaphora — a project-level instructions file (`AGENTS.md`, `CLAUDE.md`, or whichever your host honors) lives one prompt-layer above the MCP server's `instructions` and your host composes it into the system prompt with higher priority.
## Tool selection heuristics
When an agent is unsure which tool to use, these rules help:
1. **Discovery first**: Call {toolref}`list-sessions` or {toolref}`list-panes` before acting on specific targets
2. **Prefer IDs**: Once you have a `pane_id`, use it for all subsequent calls — it never changes during the pane's lifetime
3. **Wait, don't poll**: For commands the agent authors, prefer {toolref}`wait-for-channel` with `tmux wait-for -S ` composed into the command — deterministic and race-free. Use {toolref}`capture-since` for repeated observation, and fall back to {toolref}`wait-for-text` or {toolref}`wait-for-content-change` for output the agent doesn't author. Never call {toolref}`capture-pane` in a retry loop.
4. **Content vs. metadata**: If looking for text *in* a terminal, use {toolref}`search-panes`. If looking for pane *properties* (name, PID, path), use {toolref}`list-panes` or {toolref}`get-pane-info`
5. **Destructive tools are opt-in**: Never kill sessions, windows, or panes unless the user explicitly asks
---
# Safety tiers
Source: https://libtmux-mcp.git-pull.com/topics/safety/
(safety)=
# 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` | {badge}`readonly` | List, capture, search, info | Monitoring, browsing |
| `mutating` (default) | {badge}`mutating` | + create, send_keys, rename, resize | Normal agent workflow |
| `destructive` | {badge}`destructive` | + kill_server, kill_session, kill_window, kill_pane | Full control |
## Configuration
Set the safety tier via the {envvar}`LIBTMUX_SAFETY` environment variable:
```json
{
"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:
- {badge}`readonly` `readonly` — Read-only operations that don't modify tmux state
- {badge}`mutating` `mutating` — Operations that create, modify, or send input to tmux objects
- {badge}`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:
- {tool}`kill-server` refuses to run if the MCP server is inside the target server
- {tool}`kill-session` refuses to kill the session containing the MCP pane
- {tool}`kill-window` refuses to kill the window containing the MCP pane
- {tool}`kill-pane` 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`):
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 {envvar}`TMUX_TMPDIR` commonly differs between contexts.
3. Fall back to reconstruction from {envvar}`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 {envvar}`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`
{tool}`pipe-pane` 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`
{tool}`set-environment` 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`
{tool}`respawn-pane` 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 {exc}`~libtmux_mcp._utils.ExpectedToolError` instead of an unintended kill — resolve via {tool}`list-panes` 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 |
|------|------|----------|-------------|------------|
| {ref}`list-sessions` | {badge}`readonly` | true | false | true |
| {ref}`get-server-info` | {badge}`readonly` | true | false | true |
| {ref}`list-windows` | {badge}`readonly` | true | false | true |
| {ref}`list-panes` | {badge}`readonly` | true | false | true |
| {ref}`capture-pane` | {badge}`readonly` | true | false | true |
| {ref}`capture-since` | {badge}`readonly` | true | false | true |
| {ref}`get-pane-info` | {badge}`readonly` | true | false | true |
| {ref}`search-panes` | {badge}`readonly` | true | false | true |
| {ref}`wait-for-text` | {badge}`readonly` | true | false | true |
| {ref}`show-option` | {badge}`readonly` | true | false | true |
| {ref}`show-environment` | {badge}`readonly` | true | false | true |
| {ref}`create-session` | {badge}`mutating` | false | false | false |
| {ref}`create-window` | {badge}`mutating` | false | false | false |
| {ref}`split-window` | {badge}`mutating` | false | false | false |
| {ref}`send-keys` | {badge}`mutating` | false | false | false |
| {ref}`rename-session` | {badge}`mutating` | false | false | true |
| {ref}`rename-window` | {badge}`mutating` | false | false | true |
| {ref}`resize-pane` | {badge}`mutating` | false | false | true |
| {ref}`resize-window` | {badge}`mutating` | false | false | true |
| {ref}`set-pane-title` | {badge}`mutating` | false | false | true |
| {ref}`clear-pane` | {badge}`mutating` | false | false | true |
| {ref}`select-layout` | {badge}`mutating` | false | false | true |
| {ref}`set-option` | {badge}`mutating` | false | false | true |
| {ref}`set-environment` | {badge}`mutating` | false | false | true |
| {ref}`respawn-pane` | {badge}`mutating` | false | true | false |
| {ref}`kill-server` | {badge}`destructive` | false | true | false |
| {ref}`kill-session` | {badge}`destructive` | false | true | false |
| {ref}`kill-window` | {badge}`destructive` | false | true | false |
| {ref}`kill-pane` | {badge}`destructive` | false | true | false |
---
# Troubleshooting
Source: https://libtmux-mcp.git-pull.com/topics/troubleshooting/
(troubleshooting)=
# Troubleshooting
Symptom-based guide. Find your problem, follow the steps.
## Server doesn't appear in client
**Symptoms**: Client shows no libtmux tools, or "server not found" errors.
**Check**:
1. Verify the server starts manually:
```console
$ uvx libtmux-mcp
```
You should see no output (it's waiting for stdio input). Press Ctrl+C to stop.
2. Check your client config points to the right command. Common issues:
- `uvx` not in PATH — [install uv](https://docs.astral.sh/uv/getting-started/installation/)
- Typo in `"command"` or `"args"` in JSON config
- TOML config syntax errors (Codex CLI)
3. Restart your MCP client after config changes.
## Tools fail with "no sessions found"
**Symptoms**: `list_sessions` returns empty, other tools can't find targets.
**Check**:
1. Is tmux running?
```console
$ tmux list-sessions
```
2. Are you on the right socket? If `LIBTMUX_SOCKET` is set, the server only sees sessions on that socket:
```console
$ tmux -L ai_workspace list-sessions
```
3. Create a session on the expected socket:
```console
$ tmux -L ai_workspace new-session -d -s test
```
## Wrong tmux socket
**Symptoms**: Server sees different sessions than expected, or sees nothing.
**Cause**: `LIBTMUX_SOCKET` in the MCP config isolates the server to a specific socket. Your personal sessions are on the default socket.
**Fix**: Either remove `LIBTMUX_SOCKET` from the config to use the default socket, or ensure sessions exist on the configured socket.
## Pane targeting mismatch
**Symptoms**: Tool targets the wrong pane, or "pane not found" errors.
**Cause**: Using ambiguous targeting (session name + window name) instead of direct IDs.
**Fix**: Use `pane_id` (e.g. `%1`) for unambiguous targeting. Pane IDs are globally unique within a tmux server. Run `list_panes` first to discover IDs.
## Command works in shell but not via MCP
**Symptoms**: `send_keys` sends the command but output isn't what you expect.
**Check**:
1. **Enter key**: `send_keys` sends Enter by default (`enter=true`). If you're sending a partial command, set `enter=false`.
2. **Special characters**: tmux interprets some key names (e.g. `C-c`, `Enter`). If sending literal text, use `literal=true`.
3. **Timing**: After {toolref}`send-keys`, prefer composing `tmux wait-for -S ` into the shell command and calling {toolref}`wait-for-channel` for deterministic completion. Use {toolref}`capture-since` for repeated observation, and use {toolref}`wait-for-text` or {toolref}`wait-for-content-change` only when waiting on output you do not author. Don't call {toolref}`capture-pane` immediately — the command may still be running.
## Silent startup failure
**Symptoms**: MCP client says connected but no tools are available.
**Check**:
1. Missing dependency — ensure `fastmcp` is installed:
```console
$ uvx libtmux-mcp
```
If using pip install, check:
```console
$ python -c "import fastmcp; print(fastmcp.__version__)"
```
2. Python version — requires 3.10+:
```console
$ python --version
```
## Safety tier blocking tools
**Symptoms**: Some tools are missing from the tool list, or return "blocked by safety tier" errors.
**Cause**: `LIBTMUX_SAFETY` is set to a restrictive tier.
**Fix**: Check the configured tier. Default is `mutating`, which includes most tools. Only `destructive` enables kill commands. See {ref}`safety`.
## How to see logs
The MCP server uses Python's `logging` module. To see debug output, set the log level before starting:
```console
$ PYTHONUNBUFFERED=1 uvx libtmux-mcp 2>server.log
```
For Claude Desktop on macOS, MCP server logs are at:
`~/Library/Logs/Claude/mcp-server-libtmux.log`
---