# libtmux for Ruby
> The Ruby library (libtmux). These pages include its guides, tested examples and API documentation.
- [Ruby API reference](https://libtmux.org/en/ruby/v0.1.1/reference/): every public symbol, generated from the source. Hosted on libtmux.org.
---
# libtmux-workspace CLI manual
Source: https://libtmux.org/en/ruby/v0.1.1/workspace/cli/
> Commands, options and exit statuses for the Ruby workspace CLI.
**Workspace Manager concept example** A working example of the core library. It can change without notice.
`libtmux-workspace` validates a YAML or JSON configuration, shows its creation
plan, and applies it to an explicitly selected tmux server. Install the
workspace gem using the [workspace installation instructions](https://libtmux.org/en/ruby/v0.1.1/).
## Commands
Create a new session on an existing server selected by socket path.
## Input and common options
Pass one configuration file after the options. If you omit the file, discovery
requires exactly one of [`.tmuxp.yaml`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/validate/), [`.tmuxp.yml`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/validate/), or
[`.tmuxp.json`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/validate/) in the current directory. Missing or ambiguous
input exits with status `2`.
| Option | Behavior |
| --- | --- |
| `--json` | Write one JSON result or error to stdout. |
| `--expand-environment` | Expand `${NAME}` in configured paths and environment values using explicit [`--env`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#input-and-common-options-env-name-value) values. Shell command text stays unchanged. |
| `--env NAME=VALUE` | Supply an expansion value. Repeat for different names; duplicate names are invalid. Ambient environment variables are not the expansion map. |
| `-h`, `--help` | Show commands and options without reading a file or contacting tmux. |
| `--version` | Show the installed gem version without reading a file or contacting tmux. |
Show the installed command's options:
```console
$ libtmux-workspace --help
```
Check the installed version:
```console
$ libtmux-workspace --version
```
The command pages explain `--socket`, `--live`, `--timeout`, `--compensate`,
`--attach`, and `--switch`, including which commands accept them.
## Output and exit statuses
Successful human-readable results go to stdout. Human-readable errors go to
stderr; an application failure can also include its partial creation ledger
there. With [`--json`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#input-and-common-options-json), both results and errors go to stdout as JSON. Check the
exit status as well as the output.
| Status | Meaning |
| --- | --- |
| `0` | The requested operation succeeded. |
| `1` | Execution failed before recorded application effects. |
| `2` | Arguments or configuration are invalid. |
| `3` | Application effects are partial or uncertain, or a post-load attach or switch failed. |
| `130` | The operation was interrupted or cancelled. |
A successful load confirms that the creation operations completed and shell
commands were dispatched. It does not establish that the programs in those
panes have completed. A failed load can leave a session behind; inspect its
result before retrying.
[CLI parser and implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-workspace/lib/libtmux/workspace/cli.rb).
---
# validate
Source: https://libtmux.org/en/ruby/v0.1.1/workspace/cli/validate/
> Check a Ruby workspace configuration without contacting tmux.
**Workspace Manager concept example** A working example of the core library. It can change without notice.
`validate` reads and checks a YAML or JSON workspace without starting tmux,
opening a socket, creating a session, or running configured shell commands.
Use it before inspecting or applying a plan.
## Check a workspace
Save the [complete example configuration](https://libtmux.org/en/ruby/v0.1.1/workspace/examples/) as [`workspace.yaml`](),
then validate it:
```console
$ libtmux-workspace validate workspace.yaml
```
Success prints `Workspace configuration is valid.` and exits with status `0`.
To request a JSON result:
```console
$ libtmux-workspace validate \
--json \
workspace.yaml
```
The result contains `valid`, `profile`, and `version`. Invalid configuration
exits with status `2`. In JSON mode, the error object is written to stdout;
otherwise diagnostics go to stderr.
## Options and limits
The [common options](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#input-and-common-options) control output and explicit
environment expansion. Omit the filename only when the current directory
contains exactly one discoverable configuration. `--socket`, `--live`,
`--attach`, `--switch`, and `--compensate` are invalid for validation.
Validation checks the [supported configuration format](https://libtmux.org/en/ruby/v0.1.1/workspace/topics/). It does
not prove that a session name is free on a server, a shell command will
succeed, or a program is installed. Use [live planning](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/plan/#inspect-a-live-server)
to inspect the chosen server, and check [load results](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#results-and-failures)
when applying the workspace.
[CLI parser and implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-workspace/lib/libtmux/workspace/cli.rb).
---
# plan
Source: https://libtmux.org/en/ruby/v0.1.1/workspace/cli/plan/
> Inspect the ordered operations needed to create a Ruby workspace.
**Workspace Manager concept example** A working example of the core library. It can change without notice.
`plan` lists the ordered creation operations without applying them. By
default it reads only the configuration. It does not contact tmux or execute
the workspace's shell commands.
## Inspect an offline plan
Save the [complete example configuration](https://libtmux.org/en/ruby/v0.1.1/workspace/examples/) as [`workspace.yaml`](),
then inspect its plan:
```console
$ libtmux-workspace plan \
--json \
workspace.yaml
```
The JSON result includes configured command text and paths. Treat it as
application data, not a redacted diagnostic. Without [`--json`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#input-and-common-options-json), the output
lists the mode, step count, and each step's operation, target, and effect.
## Inspect a live server
`--live` acquires a snapshot from an existing server before planning. It
requires `--socket PATH`; passing a socket without `--live` is invalid.
Use the private server prepared in the [load walkthrough](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#create-a-workspace):
```console
$ libtmux-workspace plan \
--live \
--socket "$WORKSPACE_TMP/tmux.sock" \
--json \
workspace.yaml
```
Run this before loading the workspace. The manager creates new sessions; it
does not reconcile an existing session with the file. A live snapshot describes
the server when captured, so inspect the later load result rather than assuming
the server stayed unchanged.
## Options and failures
`--timeout SECONDS` bounds the live snapshot operation. It defaults to `5` and
must be finite and positive. The [common options](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#input-and-common-options)
control file discovery, JSON output, and explicit environment expansion.
`--attach`, `--switch`, and `--compensate` are invalid for planning.
Invalid arguments or configuration return status `2`. An unavailable live
server returns an execution error. See [exit statuses](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#output-and-exit-statuses)
before using a plan in automation.
[CLI parser and implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-workspace/lib/libtmux/workspace/cli.rb).
---
# load
Source: https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/
> Create a Ruby workspace on an explicitly selected existing tmux server.
**Workspace Manager concept example** A working example of the core library. It can change without notice.
`load` applies a creation plan to an existing server selected by [`--socket`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#options-socket-path).
It creates a new session and leaves it detached by default. It does not
reconcile, replace, or delete a preexisting workspace.
## Create a workspace
Install tmux and the workspace gem using the [installation instructions](https://libtmux.org/en/ruby/v0.1.1/),
then save the [complete example configuration](https://libtmux.org/en/ruby/v0.1.1/workspace/examples/) as
[`workspace.yaml`](). Run these commands in the same POSIX shell.
Create a temporary directory for this example's private socket:
```console
$ WORKSPACE_TMP=$(mktemp -d)
```
Start a private server with a keepalive session and no user configuration:
```console
$ tmux \
-S "$WORKSPACE_TMP/tmux.sock" \
-f /dev/null \
new-session -d -s manual-keepalive
```
Load the workspace on that socket:
```console
$ libtmux-workspace load \
--socket "$WORKSPACE_TMP/tmux.sock" \
--json \
workspace.yaml
```
The result records completed steps, effects, and created references. Inspect
the example's `work` session:
```console
$ tmux \
-S "$WORKSPACE_TMP/tmux.sock" \
list-windows -t work
```
This lists the `editor` window. When finished, stop only the private server:
```console
$ tmux -S "$WORKSPACE_TMP/tmux.sock" kill-server
```
Remove the private socket and temporary directory after stopping the server:
```console
$ rm -rf "$WORKSPACE_TMP"
```
## Options
| Option | Behavior |
| --- | --- |
| `--socket PATH` | Required path to an existing server's socket. Relative paths resolve from the current directory. |
| `--timeout SECONDS` | Finite positive time budget shared by preflight and all creation steps; defaults to `5`. A post-load client switch receives a separate budget of the same length. This option does not bound terminal attachment. |
| `--compensate` | Attempt guarded cleanup of positively identified created resources after application failure. It cannot undo shell effects. |
| `--attach` | Attach this CLI's terminal after loading. Requires `/dev/tty` and a valid `TERM`. |
| `--switch CLIENT` | Switch the explicitly named current client to the created session after loading. There is no fallback client selection. |
[`--attach`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#options-attach) and [`--switch`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#options-switch-client) are mutually exclusive. `--live` belongs to [`plan`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/plan/)
and is invalid for `load`. The [common options](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#input-and-common-options)
cover JSON output, file discovery, and explicit environment expansion.
## Results and failures
Success means that the creation operations completed and configured shell
commands were dispatched. It does not mean those programs finished or became
ready. Inspect their output or use an application-specific readiness signal.
A failed operation can leave completed steps in place. The result retains
created references and effects for inspection; do not retry blindly. The
default preserves partial state. [`--compensate`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#options-compensate) attempts cleanup only when
ownership can be established.
If post-load attachment or switching fails, the created session and its ledger
remain available and the command returns status `3`. Read the [exit statuses](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/#output-and-exit-statuses)
when using JSON output in a script.
[CLI parser and implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-workspace/lib/libtmux/workspace/cli.rb).
[Apply deadline](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-workspace/lib/libtmux/workspace/apply.rb).
---
# Async
Source: https://libtmux.org/en/ruby/v0.1.1/guides/async/
> Run concurrent commands within an owned Async scope.
Run libtmux operations inside an application-owned Async task: a
[subprocess facade][async-server], [ordered mapping][async-scope-map],
[control replies][guardedreply] and
[event subscriptions][async-controlsubscription].
[API reference](https://libtmux.org/en/ruby/latest/reference/libtmux-async/) ·
[Guide](https://libtmux.org/en/ruby/latest/guides/async/) ·
[RubyGems](https://rubygems.org/gems/libtmux-async)
[](https://libtmux.org/en/ruby/)
[](https://rubygems.org/gems/libtmux-async)
Part of [libtmux for Ruby](https://github.com/libtmux/libtmux-ruby#readme).
Built on [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux).
Imports start no scheduler, tmux server or background task. A scope borrows a
core [`Server`][server] and leaves its daemon running when the scope exits.
**Contents** — [Quick start](https://libtmux.org/en/ruby/v0.1.1/guides/async/#quick-start) ·
[Cancel](https://libtmux.org/en/ruby/v0.1.1/guides/async/#cancel-a-request-without-stalling-the-others) ·
[Map](https://libtmux.org/en/ruby/v0.1.1/guides/async/#keep-results-ordered-and-bounded) ·
[Control](https://libtmux.org/en/ruby/v0.1.1/guides/async/#read-control-events-in-a-scope) ·
[Leaves out](https://libtmux.org/en/ruby/v0.1.1/guides/async/#what-it-leaves-out) · [Support](https://libtmux.org/en/ruby/v0.1.1/guides/async/#support-and-status) ·
[Documentation](https://libtmux.org/en/ruby/v0.1.1/guides/async/#documentation)
## Quick start
Install the gem. It needs Ruby 3.3 or newer and tmux 3.3a or newer on
`PATH`:
```console
$ gem install libtmux-async
```
Open a scope over a core server with [`LibTmux::Async.open`][async-open], then
capture every pane concurrently with [`Scope#map`][async-scope-map]. The outer
[`Server.start`][server-start] block only supplies a server for the scope to
borrow:
```ruby
require "libtmux"
require "libtmux/async"
LibTmux::Server.start do |server|
server.new_session(name: "work", command: ["/bin/cat"])
server.new_session(name: "logs", command: ["/bin/cat"])
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
refs = scope.server.list_panes.map(&:ref)
screens = scope.map(refs, concurrency: 2) { |ref| scope.server.pane(ref).capture }
puts "captured panes: #{screens.length}"
puts "all succeeded: #{screens.all?(&:success?)}"
end
end.wait
end
```
It prints:
```text
captured panes: 2
all succeeded: true
```
- Leave out `parent:` to use the current Async task.
- A scope rejects use from another thread, process or scheduler, so create one
scope for each scheduler thread.
- Scope exit retires its clients and joins its owned tasks.
## Cancel a request without stalling the others
### [`Async::Server#run`][async-server-run]: cancel a request that is waiting
[`scope.server.run`]() waits for a tmux command in a task. Cancel that task and
the scope retires its client, while sibling requests keep running.
[`wait_for`][async-server-wait_for] returns once the request has signalled
that it is running, so the cancel arrives while it waits:
```ruby
require "libtmux"
require "libtmux/async"
LibTmux::Server.start do |server|
server.new_session(name: "work", command: ["/bin/cat"])
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
waiting = parent.async do
scope.server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 5)
rescue LibTmux::Cancelled => error
error
end
scope.server.wait_for("ready", timeout: 5)
waiting.cancel
failure = waiting.wait
puts "request: #{failure.class}"
puts "delivery: #{failure.delivery}"
end
end.wait
end
```
It prints:
```text
request: LibTmux::Cancelled
delivery: possibly_sent
```
- `run` returns the same binary [`CommandResult`][commandresult] as the core.
- [`Cancelled`][cancelled] carries `delivery` as `:not_sent` or
`:possibly_sent`. A deadline cannot undo tmux effects.
- Interactive terminal attachment stays on the blocking core and raises
[`UnsupportedFeatureError`][unsupportedfeatureerror] here.
The [complete program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/async_cancel.rb) also keeps capturing
while the request waits, and checks that the cancelled client is reaped. The
contract is in [Async scope][behavior-async-scope].
## Keep results ordered and bounded
### [`Scope#map`][async-scope-map]: input order, capped payloads
[`map`]() runs a block for each item, up to `concurrency:` at a time, and returns a
frozen Array in input order. The quick start is a [`map`](). These limits apply to
every scope:
| Scope limit | Default |
| --- | --- |
| Active subprocess clients | 4 |
| Admitted subprocess requests, including unconsumed results | 32 |
| Queued request payload | 4 MiB |
| Retained process and map output payload | 8 MiB |
| Per-command stdout / stderr | 1 MiB / 256 KiB |
| Control connections | 4 |
| Ordinary command deadline | 5 seconds |
- Independent requests may complete out of order, and the result is still in
input order.
- [`map`]() caps retained items and counts payload bytes of strings, primitive
values, [`CommandResult`][commandresult], Arrays and Hashes. Cycles and deep
nesting are refused.
- An application-defined result needs a `result_bytes:` callable that returns
a nonnegative Integer.
- Keep measured values unchanged until the map returns. The counts cover
payload bytes and do not measure the Ruby heap.
The contract is in [Async map][behavior-async-map].
## Read control events in a scope
### [`Async::Server#open_control`][async-server-open_control]: replies and events
A control connection has its own request, reply and subscriber limits.
[`exchange`][async-controlconnection-exchange] runs one command, and
[`events`][async-controlconnection-events] yields what tmux reports outside a
reply:
```ruby
require "libtmux"
require "libtmux/async"
LibTmux::Server.start do |server|
session = server.new_session(name: "work", command: ["/bin/cat"])
window = session.list_windows.fetch(0)
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
scope.server.open_control(session: session.ref) do |control|
reply = control.exchange("display-message -p ready", timeout: 5)
puts "reply: #{reply.blocks.last.body.strip}"
events = control.events
window.rename("renamed")
event = events.next(timeout: 5) until event&.raw&.start_with?("%window-renamed")
puts "event: #{event.raw.strip}"
end
end
end.wait
end
```
It prints:
```text
reply: ready
event: %window-renamed @0 renamed
```
- `exchange` answers a [`GuardedReply`][guardedreply] with `:boundary_window`
attribution. It makes no claim that the command finished.
- Events arrive through [`events.next`]() or an explicit
[`subscribe`][async-controlconnection-subscribe]. Consumer callbacks run
outside the parser.
- A reliable subscription raises on overflow, and a tail subscription reports
the dropped range.
- Cancelling a dispatched exchange that is not drained closes the connection.
- [`pause_output`][async-controlconnection-pause_output] and
[`resume_output`][async-controlconnection-resume_output] report possible
loss through gap events. A gap does not show that the action took effect.
- To reconnect, close the connection and pass it as `reconnect:`. The
replacement stays owned by the scope, has a new generation, and starts with
a gap of unknown loss. Nothing is replayed.
The contract is in [Control connections][behavior-control].
## What it leaves out
- **Terminal attachment.** `attach` stays on the blocking core and raises
[`UnsupportedFeatureError`][unsupportedfeatureerror] on this facade.
- **Replay.** Reconnect and resume never replay requests or missed output. See
[Control connections][behavior-control].
- **A scheduler.** Imports start no scheduler or background task, and your
application owns the Async root task. See [Execution modes][doc-modes].
- **Cross-thread scopes.** A scope belongs to the thread, process and
scheduler that opened it. See [Async scope][behavior-async-scope].
## Support and status
Releases are numbered `0.1.N`, and each release increments `N`. The API is not
settled: any release may change or remove exported identifiers, and the
changelog marks each break. Pin an exact version to avoid one.
- **Ruby 3.3 or newer** and **tmux 3.3a or newer.** The facade dispatches
through a version-checked core server, so it carries the same daemon release
check.
- **Control connections need tmux 3.7 or newer.**
[`open_control`][async-server-open_control] refuses an older daemon before a
control client attaches.
- **Async 2.46.** The gem depends on `async` ~> 2.46.0 and the same version of
`libtmux`.
- **Linux, including WSL, and macOS.** The [compatibility workflow][compat]
runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04 and
retains per-revision results. A push and a pull request run the oldest and
newest of those releases, 3.3a and 3.8, and a release runs all of them. macOS
is supported on a best-effort basis, and CI coverage for it is highly
limited.
## Documentation
| | |
| --- | --- |
| [Reference](https://libtmux.org/en/ruby/latest/reference/libtmux-async/) | The Async classes, one page each |
| [Async guide](https://libtmux.org/en/ruby/latest/guides/async/) | Running concurrent commands within an owned scope |
| [Execution modes][doc-modes] | Blocking calls, Async tasks and control subscriptions |
| [Recipes][doc-recipes] | Complete programs with checked excerpts |
| [Public behavior][behavior-async-scope] | Scheduling, ownership and limits of a scope |
[async-controlconnection-events]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-controlconnection-events/
[async-controlconnection-exchange]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-controlconnection-exchange/
[async-controlconnection-pause_output]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-controlconnection-pause_output/
[async-controlconnection-resume_output]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-controlconnection-resume_output/
[async-controlconnection-subscribe]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-controlconnection-subscribe/
[async-controlsubscription]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-controlsubscription/
[async-open]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-open/
[async-scope-map]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-scope-map/
[async-server]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-server/
[async-server-open_control]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-server-open_control/
[async-server-run]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-server-run/
[async-server-wait_for]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-server-wait_for/
[behavior-async-map]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#async-map
[behavior-async-scope]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#async-scope
[behavior-control]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#control-connections
[cancelled]: https://libtmux.org/en/ruby/latest/reference/libtmux-cancelled/
[commandresult]: https://libtmux.org/en/ruby/latest/reference/libtmux-commandresult/
[compat]: https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml
[doc-modes]: https://libtmux.org/en/ruby/v0.1.1/concepts/transports/
[doc-recipes]: https://libtmux.org/en/ruby/v0.1.1/examples/recipes/
[guardedreply]: https://libtmux.org/en/ruby/latest/reference/libtmux-guardedreply/
[server]: https://libtmux.org/en/ruby/latest/reference/libtmux-server/
[server-start]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-start/
[unsupportedfeatureerror]: https://libtmux.org/en/ruby/latest/reference/libtmux-unsupportedfeatureerror/
---
# Concepts
Source: https://libtmux.org/en/ruby/v0.1.1/concepts/
> Concepts for libtmux.
Understand captured queries, object identities, and operations.
- [Execution modes](https://libtmux.org/en/ruby/v0.1.1/concepts/transports/): Choose blocking commands, captured queries, control mode, or Async tasks.
---
# Server, session, window, pane
Source: https://libtmux.org/en/ruby/v0.1.1/concepts/server-session-window-pane/
> How tmux servers, sessions, windows, panes and attached clients relate to each other.
libtmux models tmux's server, session, window, and pane objects:
```
Server
├── Session
│ └── Window
│ └── Pane
└── Client (attached view)
```
A `Server` contains sessions. Each [`Session`]() contains links to windows, and each
[`Window`]() contains panes. Commands run inside a [`Pane`](), where you send input and
capture output. A window can be linked to more than one session.
## Stable identity, not name or index
tmux assigns a unique ID to each session, window, and pane at creation. The ID
remains stable for that object's lifetime even if its name or index changes:
| Object | ID prefix | Example |
|--------|-----------|---------|
| Session | `$` | `$13` |
| Window | `@` | `@3243` |
| Pane | `%` | `%5433` |
| Server | - | identified by socket name or path instead |
Use stable IDs to identify the same tmux object across reads. Names and indexes
can change while a program is running.
## Client: a view, not a child
A tmux client represents a terminal attached to a session. Several clients can view
the same server, and each can switch sessions or windows independently. Client
fields describe the view at the time of the read.
A [control-mode connection](https://libtmux.org/en/ruby/v0.1.1/concepts/transports/) is also a client. It appears in
[`list-clients`](https://libtmux.org/en/tmux/latest/manual/list-clients/), counts toward [`session_attached`](https://libtmux.org/en/tmux/latest/catalog/formats/session_attached/), and affects
attachment-dependent behavior such as [`destroy-unattached`](https://libtmux.org/en/tmux/latest/catalog/options/destroy-unattached/). Closing the last
attached client can therefore destroy a session configured with that option.
---
# Execution modes
Source: https://libtmux.org/en/ruby/v0.1.1/concepts/transports/
> Choose blocking commands, captured queries, control mode, or Async tasks.
| Mode | Wait and ownership | Result evidence |
| --- | --- | --- |
| Core command | Calling thread blocks; server binding owns each client and its pipes | [`CommandResult`]() has binary stdout/stderr and final client status |
| Captured query | Snapshot acquisition performs explicit I/O; [`Selection`]() filtering is local | Stable captured membership with acquisition interval and coverage |
| Command group | One client submits an ordered, nontransactional group | Aggregate final status; individual steps remain `unknown` |
| Core control | One owned reader thread drains the connection; each subscriber has limits | Guarded blocks between boundaries; hooks can share the interval |
| Async | Caller supplies an Async parent task; pipe I/O yields on its scheduler | Same command evidence, with bounded request admission and cancellation |
| MCP | Caller supplies transport streams, Async scope and tool policy | Versioned protocol envelopes and structured tool responses |
| Workspace | Parsing and planning are inert; apply performs ordered commands | Created-reference ledger, observed effects and explicit uncertainty |
Use `Server.open(socket_path: ...)` to borrow a daemon through a pinned
binding. With no socket named, `Server.open` borrows the daemon tmux would pick
(see [Endpoints](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#endpoints)). `Server.start` creates an
owned daemon on a unique socket and closes it with the block. These recipes
name a socket or own one.
`CommandResult#success?` means the client exited successfully. Sending input
does not establish shell completion. A [`GuardedReply`]() has no `success?`:
`%end` terminates a guard, while WAIT commands, aliases and hooks can change
what finishes when. Its `boundary_window` attribution is deliberately weaker
than per-command ownership.
Concurrent control calls pipeline complete wire requests on one connection.
The writer can submit a later request while an earlier reply waits; the reader
assigns boundaries in the same order. Admission limits include all pending
and completed but unconsumed requests. Cancellation after any request bytes
are written closes the connection: other written requests have
`possibly_sent` delivery, and requests with no written bytes have `not_sent`.
No uncertain request is replayed. Sequential calls still wait for each reply.
Reliable control subscriptions raise [`SubscriptionOverflow`]() when they cannot
retain the stream. Tail subscriptions emit a gap containing lost sequence
and byte evidence. Neither mode blocks the command reader behind a slow
consumer. Limits apply to connection-owned buffers; callers own the replies
they retain after return.
Captured queries evaluate the whole criteria tree before selecting rows,
including inactive OR branches. Explain methods perform no I/O. Current
source plans use explicit capture and local evaluation; unsupported required
pushdown fails rather than silently changing semantics.
See [the recipes](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/) for event-based WAIT release, cancellation,
overflow and group-failure examples. No throughput comparison is claimed.
---
# Examples
Source: https://libtmux.org/en/ruby/v0.1.1/examples/
> Examples for libtmux.
Run complete programs with their documented setup and cleanup.
- [Capture pane output](https://libtmux.org/en/ruby/v0.1.1/examples/capture-pane-output/): Send a command, wait for its output, and clean up the private server.
- [Recipes](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/): Run programs for queries, linked windows, capture, and cancellation.
---
# Capture pane output
Source: https://libtmux.org/en/ruby/v0.1.1/examples/capture-pane-output/
> Run a complete Ruby program that captures output on an isolated tmux server.
This complete Ruby program starts a private tmux server, sends a command,
and captures the line it prints. It includes imports, setup, and cleanup.
You need tmux and a Unix environment; no existing session is required.
## Read what's on screen
The leading newline puts the output on a fresh row. Matching the whole line
avoids mistaking the echoed command for its output.
```ruby title="capture.rb"
require "libtmux"
LibTmux::Server.start do |server|
created = server.new_session(
name: "capture", command: ["/bin/sh"],
environment: {"ENV" => "/dev/null"}, receipt: true
)
pane = created.pane
pane.send_text("printf '\\nlibtmux capture ready\\n'")
pane.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
loop do
if pane.capture.stdout.lines(chomp: true).include?("libtmux capture ready")
puts "libtmux capture ready"
break
end
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
raise "Output did not arrive within five seconds"
end
sleep 0.025
end
end
```
## Wait for output or completion
[`LibTmux::Server.start`]() owns the private server. Leaving its block stops the server, including when capture or the deadline raises an error.
Capture reads screen state and scrollback, so output that has scrolled away
may be absent. The program prints `libtmux capture ready` when its check passes
and exits unsuccessfully if an operation fails.
## Setup and run
Use an empty directory and save the files using the displayed names. You need
Ruby 4.0.7 and Bundler. Bundler installs the library and its runtime dependencies.
Save this dependency file beside the program.
```ruby title="Gemfile"
source "https://rubygems.org"
gem "libtmux", path: "libtmux-source/gems/libtmux"
```
The commands pin the library revision used to run this program.
```console
$ git clone https://github.com/libtmux/libtmux-ruby libtmux-source &&
git -C libtmux-source checkout 2599d45369515aaf2fd5793bfe71cdc20de641b6 &&
bundle config set --local path vendor/bundle &&
bundle install &&
bundle exec ruby capture.rb
```
## Where this comes from
The displayed files were compiled or loaded with their native tools and run
on Linux with tmux 3.2a and 3.7c. The rendering checks preserve those file bytes.
---
# Recipes
Source: https://libtmux.org/en/ruby/v0.1.1/examples/recipes/
> Run programs for queries, linked windows, capture, and cancellation.
Every linked program loads installed gems, starts an isolated server, asserts
its result and verifies owned-daemon cleanup. The shared
[support module](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/support.rb) supplies assertions and the cleanup
wrapper. No program adds checkout paths to Ruby's load path.
Run a program from the repository after installing its required local gems:
```console
$ ruby examples/window_links.rb
```
The artifact suite copies programs outside the checkout and runs them against
each gem's isolated dependency closure. [The manifest](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/manifest.json)
owns file discovery and the source regions used below. `scripts/examples
--check` rejects an unlisted program or a changed excerpt.
## Captured queries
[Complete list/filter/exact-one program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/list_filter.rb). A fresh
window does not alter an existing capture; local filtering still works after
the binding closes.
```ruby
session = server.new_session(name: "capture", command: ["/bin/cat"])
session.new_window(name: "second", command: ["/bin/cat"])
snapshot = server.snapshot
panes = snapshot.panes
first = panes.one(id: panes.first.id)
session.new_window(name: "later", command: ["/bin/cat"])
Example.check(panes.size == 2, "captured membership changed")
Example.check(panes.where(id: first.id).one.ref == first.ref, "wrong exact match")
Example.raises(LibTmux::MultipleMatchesError) { panes.one }
Example.check(panes.one_or_nil(id: "%4294967294").nil?, "missing pane was invented")
```
## Layout and bytes
[Complete layout/capture/send program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/layout_io.rb). On tmux 3.7
and newer a control output event establishes that the literal input has
reached the pane before capture; older daemons refuse the control client.
Binary buffers round-trip without text decoding.
```ruby
receipt = server.new_session(name: "layout", command: ["/bin/cat"], receipt: true)
pane = receipt.pane
second = pane.split(direction: :horizontal, size: "40%", command: ["/bin/cat"])
receipt.window.select_layout("tiled")
Example.check(receipt.window.list_panes.map(&:id).sort == [pane.id, second.id].sort, "assigned pane IDs differ")
if Example.control?(server)
server.open_control(session: receipt.entity.ref) do |control|
control.exchange("display-message -p ready", timeout: 0.5)
output = control.subscribe(pane_id: pane.id, max_bytes: 8192, max_events: 32)
literal = "literal; #{'#{pane_id}'} $HOME"
pane.send_text(literal)
bytes = "".b
bytes << output.next(timeout: 0.5).data until bytes.include?(literal)
Example.check(pane.capture.stdout.include?(literal), "capture lost literal input")
end
else
Example.refuses_control(server, receipt.entity)
end
payload = "NUL\0\xff\n".b
server.write_buffer(name: "bytes", data: payload)
Example.check(server.read_buffer("bytes").stdout == payload, "buffer bytes changed")
```
## One window at three indexes
[Complete window-link program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/window_links.rb). Window identity
and selection context remain distinct.
```ruby
session = server.new_session(name: "links", command: ["/bin/cat"])
window = session.list_windows.fetch(0)
session.link_window(window.ref, index: 4)
session.link_window(window.ref, index: 9)
links = session.list_window_links
Example.check(links.map(&:index) == [0, 4, 9], "link indexes differ")
Example.check(links.map { |link| link.window.ref }.uniq == [window.ref], "window identity split")
Example.check(links.map(&:ref).uniq.length == 3, "link contexts collapsed")
links.find { |link| link.index == 9 }.select
Example.check(session.display('#{window_index}').text == "9\n", "wrong current link")
```
## Blocking request cancellation
[Complete plain-Ruby cancellation program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/cancel.rb). A tmux event
proves dispatch before another thread cancels the blocked client. The program
joins the caller, checks client reaping and closes the token's owned pipe.
```ruby
cancellation = LibTmux::Cancellation.new
waiting = nil
begin
waiting = Thread.new do
server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"],
timeout: 0.5, cancel: cancellation)
rescue LibTmux::Cancelled => error
error
end
server.wait_for("ready", timeout: 0.5)
cancellation.cancel
failure = waiting.value
Example.check(failure.is_a?(LibTmux::Cancelled), "cancellation lost")
Example.check(failure.delivery == :possibly_sent, "dispatched wait claimed no effects")
Example.raises(Errno::ECHILD) { Process.waitpid(failure.pid, Process::WNOHANG) }
Example.check(server.diagnostics.fetch(:admitted_requests).zero?, "client remains admitted")
ensure
begin
cancellation.cancel
waiting&.join
ensure
cancellation.close
end
end
```
## Async capture and cancellation
[Complete Async program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/async_cancel.rb). A second task captures
while another tmux client waits; cancellation retires that client's process.
```ruby
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
waiting = parent.async do
scope.server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 0.5)
rescue LibTmux::Cancelled => error
error
end
scope.server.wait_for("ready", timeout: 0.5)
Example.check(scope.diagnostics.fetch(:active_process_slots) == 1, "waiting client lost its slot")
captures = scope.map(scope.server.list_panes.map(&:ref), concurrency: 2) do |ref|
scope.server.pane(ref).capture
end
Example.check(captures.all?(&:success?), "sibling captures stalled")
waiting.cancel
failure = waiting.wait
Example.check(failure.is_a?(LibTmux::Cancelled), "cancellation lost")
Example.check(failure.delivery == :possibly_sent, "cancelled dispatch claimed no effects")
Example.raises(Errno::ECHILD) { Process.waitpid(failure.pid, Process::WNOHANG) }
Example.check(scope.server.diagnostics.fetch(:admitted_requests).zero?, "cancelled client remains admitted")
end
end.wait
```
## Control overflow
[Complete control program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/control_overflow.rb). It needs tmux 3.7
or newer and demonstrates the refusal on older daemons. A slow reliable
subscriber fails explicitly; a tail subscriber reports a gap and replies
continue to drain.
```ruby
server.open_control(session: session.ref) do |control|
control.exchange("display-message -p ready", timeout: 0.5)
reliable = control.subscribe(max_events: 1, max_bytes: 1024)
tail = control.subscribe(mode: :tail, max_events: 1, max_bytes: 1024)
3.times { |index| window.rename("event#{index}") }
reply = control.exchange("display-message -p alive", timeout: 0.5)
Example.check(reply.blocks.last.body == "alive\n", "slow reader blocked commands")
Example.check(reply.attribution == :boundary_window, "reply overclaims attribution")
Example.check(reliable.diagnostics.fetch(:overflowed), "overflow is missing from diagnostics")
Example.check(control.diagnostics.fetch(:retained_reply_bytes).zero?, "consumed reply remains retained")
reliable.next(timeout: 0.5)
Example.raises(LibTmux::SubscriptionOverflow) { reliable.next(timeout: 0.5) }
gap = tail.next(timeout: 0.5)
Example.check(gap.kind == :gap && gap.dropped_bytes.positive?, "tail hid lost bytes")
end
```
## Failure inside a group
[Complete group program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/failed_group.rb). The first mutation
survives a later failure. A separate read establishes the missing later effect;
the group result does not invent statuses for its members.
```ruby
group = server.run_group([
["set-option", "-g", "@before", "retained"],
["select-pane", "-t", "%4294967294"],
["set-option", "-g", "@after", "not-executed"]
])
Example.check(!group.success?, "failing group succeeded")
Example.check(group.steps.all? { |step| step.fetch(:outcome) == :unknown }, "invented per-step status")
Example.check(server.options(scope: :session).get("@before").raw == "retained", "earlier effect rolled back")
Example.check(server.options(scope: :session).list.none? { |option| option.name == "@after" }, "later step executed")
```
## MCP protocol
[Complete MCP program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/mcp_protocol.rb). Real pipe frames exercise
discovery, snapshot reads, default mutation denial and cancellation of an
application-owned WAIT method. The custom WAIT method is a transport probe,
not part of the advertised tmux tool catalog.
```ruby
application = LibTmux::MCP::Application.new(server: scope.server, endpoint_name: "example")
sdk = application.sdk_server
input, client_input = IO.pipe
client_output, output = IO.pipe
transport = LibTmux::MCP::StdioTransport.new(server: sdk, parent: parent, input: input, output: output)
runner = parent.async { transport.run }
```
The installed executable also exercises explicitly enabled create, send,
close and authored-run tools. Its complete program creates a zsh pane whose
startup file explicitly sources the CLI's mode-0600 enrollment file, waits
for the authenticated acknowledgement, and checks binary stderr and native
exit status. It closes stdin and verifies enrollment cleanup and borrowed
daemon survival. An unsupported advertised process backend exercises the
structured refusal instead; that is not positive enrollment evidence.
```ruby
executable = Gem.bin_path("libtmux-mcp", "libtmux-mcp")
Open3.popen3(Gem.ruby, "-W:no-experimental", executable, "--socket", server.endpoint.socket_path,
"--tmux", Example.executable, "--endpoint", "installed", "--enable-tool", "tmux_create", "--enable-tool", "tmux_send",
"--enable-tool", "tmux_close", "--enable-tool", "tmux_run", *enrollment_arguments) do |input, output, errors, process|
request = lambda do |id, method, params = {}|
input.write(JSON.generate({jsonrpc: "2.0", id: id, method: method, params: params}) + "\n")
Example.check(IO.select([output], nil, nil, id == 1 ? 1.0 : 0.5), "installed MCP did not return a frame")
response = JSON.parse(output.gets)
Example.check(response.fetch("id") == id, "MCP response identity changed")
response.fetch("result")
end
request.call(1, "initialize", {protocolVersion: "2025-11-25", capabilities: {},
clientInfo: {name: "recipe", version: "1"}})
input.write(JSON.generate({jsonrpc: "2.0", method: "notifications/initialized"}) + "\n")
names = request.call(2, "tools/list").fetch("tools").map { |tool| tool.fetch("name") }
Example.check(names.sort == %w[tmux_capabilities tmux_close tmux_create tmux_run tmux_send tmux_snapshot], "tool policy differs")
created = request.call(3, "tools/call", {name: "tmux_create", arguments: {
kind: "session", name: "via-protocol", argv: ["/bin/cat"]}}).fetch("structuredContent")
Example.check(created.fetch("ok"), "protocol creation failed")
data = created.fetch("data")
pane = data.fetch("created").find { |ref| ref.fetch("kind") == "pane" }
sent = request.call(4, "tools/call", {name: "tmux_send", arguments: {
target: pane, input: {type: "text", text: "literal;"}}}).fetch("structuredContent")
Example.check(sent.fetch("data").fetch("completion") == "dispatch_only", "send claimed shell completion")
target = pane
if channel
Example.check(File.stat(setup).mode & 0o777 == 0o600, "enrollment setup permissions differ")
channel.puts(setup)
Example.check(IO.select([channel], nil, nil, 0.5) && channel.gets == "ready\n", "shell enrollment was not acknowledged")
target = pane.merge("id" => shell_pane.id)
end
script = 'printf "%s:%s" "$EXAMPLE_CONTEXT" "$TMUX_PANE"; printf "\\000\\377" >&2; exit 9'
run = request.call(5, "tools/call", {name: "tmux_run", arguments: {
target: target, script: script, stdout_limit: 128, stderr_limit: 2}}).fetch("structuredContent")
if channel
Example.check(run.fetch("ok"), "installed authored run failed")
result = run.fetch("data")
Example.check(result.fetch("stdout").fetch("data") == "installed:#{shell_pane.id}", "authored shell context differs")
Example.check(result.fetch("stderr") == {"encoding" => "base64", "data" => "AP8=", "bytes" => 2, "truncated" => false}, "authored bytes differ")
Example.check(result.fetch("completion") == {"state" => "exited", "exit_status" => 9, "signal" => nil}, "native completion differs")
Example.check(result.fetch("authorization").fetch("state") == "authorized", "authorization receipt missing")
else
Example.check(run.dig("error", "code") == "unsupported" && run.dig("error", "delivery") == "not_sent", "unsupported enrollment did not refuse")
end
closed = request.call(6, "tools/call", {name: "tmux_close", arguments: {target: data.fetch("entity")}})
Example.check(closed.fetch("structuredContent").fetch("ok"), "protocol close failed")
input.close
Example.check(process.join(0.5), "MCP EOF did not retire its process")
Example.check(process.value.success? && errors.read.empty?, "MCP executable failed")
Example.check(!File.exist?(setup), "enrollment setup survived EOF")
end
```
## Workspace plan, load and failure
[Complete workspace program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/workspace_apply.rb) consumes the
[checked configuration](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/workspace.yaml), applies it, then verifies
partial failure and guarded compensation when another layout exceeds available
pane space. It also runs installed CLI validation, planning and loading.
```ruby
workspace = LibTmux::Workspace.load(File.join(__dir__, "workspace.yaml"))
plan = workspace.plan(snapshot: server.snapshot)
Example.check(server.list_sessions.size == 1, "planning changed tmux")
result = plan.apply(server: server)
Example.check(result.success?, "workspace apply failed")
Example.check(result.effects.any? { |effect| effect.outcome == :dispatch_only }, "shell dispatch overclaims completion")
crowded = LibTmux::Workspace.parse(JSON.generate({
session_name: "crowded", windows: [{window_name: "small", panes: Array.new(40) { {} }}]
}), format: :json, base_directory: __dir__)
error = Example.raises(LibTmux::Workspace::ApplyError) do
crowded.plan.apply(server: server, compensate: true)
end
Example.check(!error.result.created_refs.empty?, "failure lost partial creation ledger")
Example.check(error.result.compensation == :completed, "owned compensation failed")
Example.check(server.list_sessions.map(&:ref).include?(borrowed.ref), "borrowed session was removed")
```
---
# Ruby MCP examples
Source: https://libtmux.org/en/ruby/v0.1.1/mcp/examples/
> Run complete Ruby programs for an MCP client or an embedded application.
**MCP concept example** A working example of the core library. It can change without notice.
Both examples include their complete program, pinned dependencies, run
commands, expected results, and cleanup. Each creates its own tmux daemon.
## Default read-only catalog
The [client guide](https://libtmux.org/en/ruby/v0.1.1/mcp/guides/connect-client/#configure-a-client) generates an
MCP configuration using the launcher's absolute path and the current Ruby
executable. Its default catalog contains [`tmux_capabilities`]() and
[`tmux_snapshot`](). Listing the tools and querying the example session verifies
the connection.
The [embedded example](https://libtmux.org/en/ruby/v0.1.1/mcp/examples/page-session-metadata/) calls those tools from Ruby
inside an Async scope. It creates two sessions and prints their names by
following a snapshot cursor.
## Add bounded observation
For capture or waits in the client launcher, add
`"--enable-tool", "`[`tmux_capture`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_capture/)`"` or `"--enable-tool", "`[`tmux_wait`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_wait/)`"` to the
array passed to [`LibTmux::MCP::CLI.run`]() in `run-mcp.rb`. Adding them to the
generated client configuration's arguments has no effect: the launcher does
not forward those arguments. Enable creation, input, close, or authored
execution only when the client needs those effects.
The [guides overview](https://libtmux.org/en/ruby/v0.1.1/mcp/guides/#enable-observation) explains the tool choices;
the [source-owned guide](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/) gives their lifecycle contracts.
- [Page session metadata](https://libtmux.org/en/ruby/v0.1.1/mcp/examples/page-session-metadata/): Embed the MCP application, page two session names from one capture, and close owned resources.
- [Launch a client server](https://libtmux.org/en/ruby/v0.1.1/mcp/guides/connect-client/): Run a stdio MCP server with a private tmux daemon and a generated client configuration.
---
# Page session metadata from Ruby
Source: https://libtmux.org/en/ruby/v0.1.1/mcp/examples/page-session-metadata/
> Embed the Ruby MCP application, read a retained session listing, and close its owned resources.
**MCP concept example** A working example of the core library. It can change without notice.
Use the MCP application directly when a Ruby host needs the same tool policy
and result envelopes as its MCP clients. This complete example creates two
sessions, selects one row per page, and prints both names from one retained
metadata capture.
It calls the application in process. For a client that launches a stdio
server, use the [client guide](https://libtmux.org/en/ruby/v0.1.1/mcp/guides/connect-client/).
## Prepare the project
Use Ruby 4.0.7 with its development headers, Bundler, Git, a C compiler and
Make, and tmux 3.3a or newer on a Unix host. The locked bundle includes native
extensions. Start with an empty directory:
```console
$ mkdir ruby-mcp-pages && cd ruby-mcp-pages
```
Fetch the source:
```console
$ git clone https://github.com/libtmux/libtmux-ruby libtmux-source
```
Select the documented revision:
```console
$ git -C libtmux-source checkout 9b1545562a112353c2c893a1d3e8c0d9b4b51f8d
```
Install the locked dependencies:
```console
$ BUNDLE_FROZEN=true BUNDLE_GEMFILE=./libtmux-source/Gemfile bundle install
```
## Run the complete example
Save this file beside the source checkout:
```ruby title="snapshot-sessions.rb"
# frozen_string_literal: true
ENV["BUNDLE_GEMFILE"] = File.expand_path("libtmux-source/Gemfile", __dir__)
require "bundler/setup"
require "libtmux/mcp"
def describe_failure(error)
details = ["#{error.class}: #{error.message}"]
if error.is_a?(LibTmux::Error)
details.concat(error.cleanup_errors.map { |message| "Cleanup: #{message}" })
end
if error.respond_to?(:async_cleanup_errors)
details.concat(error.async_cleanup_errors.map { |message| "Cleanup: #{message}" })
end
details.join("\n")
end
def tool_data(application, name, arguments = {})
result = application.call(name, arguments).structured_content
return result.fetch("data") if result.fetch("ok")
error = result.fetch("error")
raise "#{name}: #{error.fetch('code')} (#{error.fetch('delivery')}): " \
"#{error.fetch('message')}"
end
server = nil
failures = []
begin
server = LibTmux::Server.start(timeout: 5.0)
%w[build editor].each do |name|
server.new_session(name: name, command: ["/bin/cat"], timeout: 5.0)
end
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
application = nil
begin
application = LibTmux::MCP::Application.new(
server: scope.server, endpoint_name: "example", request_timeout: 5.0
)
capabilities = tool_data(application, "tmux_capabilities")
puts "Endpoint: #{capabilities.fetch('endpoint')}"
page = tool_data(
application, "tmux_snapshot", {entity: "session", limit: 1}
)
loop do
page.fetch("items").each do |item|
puts item.fetch("fields").fetch("name")
end
break unless page.fetch("truncated")
page = tool_data(
application, "tmux_snapshot", {cursor: page.fetch("next_cursor")}
)
end
ensure
begin
application&.close
rescue StandardError => error
failures << "MCP cleanup failed: #{describe_failure(error)}"
end
end
end
end.wait
rescue StandardError => error
failures << describe_failure(error)
ensure
begin
server&.close
rescue StandardError => error
failures << "Server cleanup failed: #{describe_failure(error)}"
end
end
abort failures.join("\n") unless failures.empty?
```
Run it with tmux available on `PATH`:
```console
$ ruby -W:no-experimental snapshot-sessions.rb
```
Expected output:
```text
Endpoint: example
build
editor
```
The Ruby option suppresses the experimental [`IO::Buffer`](https://docs.ruby-lang.org/en/4.0/IO/Buffer.html) warning. It does not
suppress operation or cleanup errors. A failure produces a nonzero exit and
diagnostics on stderr; earlier stdout may contain partial results.
## Pagination
[`Application`]() requires a [`LibTmux::Async::Server`]() facade inside its owning
Async scheduler. [`LibTmux::Async.open`]() supplies that facade and closes the
scope after the block. The application and its calls stay in the same
scheduler throughout the example.
The first [`tmux_snapshot`]() call selects session metadata with a page limit of
one. A continuation sends only the returned `next_cursor`. It keeps the same
capture and selection; it does not relist sessions. The private daemon has
two sessions, so this example receives two pages. `truncated: false` ends the
loop.
Every call checks the application's `structured_content` envelope before
using `"data"`. Tool failures include a code, delivery state, and message. A
missing or expired cursor is an error to handle, not an instruction to
silently restart the listing. The [snapshot topic](https://libtmux.org/en/ruby/v0.1.1/mcp/topics/snapshots-and-references/)
explains cursor lifetime, capacity, and reference identity.
## Resource ownership
`Server.start` creates a private socket directory and foreground tmux daemon
with an empty configuration. The program creates its own `build` and `editor`
sessions running `cat`; no existing socket, session, or pane is required.
The MCP application uses its default capability and snapshot tools. The two
sessions are ordinary Ruby API setup, not mutations admitted through the MCP
tool policy. Embedding the application does not constrain what other Ruby
code in the host can do.
Startup and each session creation have separate five-second deadlines.
The application uses `request_timeout: 5.0` for metadata acquisition during
capability discovery and fresh snapshot calls. Continuations read retained
data. Shutdown first closes the application, then its Async scope, then the
owned daemon. Each cleanup attempt still runs if an earlier operation or
cleanup fails, and the program reports the collected errors.
The [pinned application implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-mcp/lib/libtmux/mcp/application.rb)
and [Async scope](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-async/lib/libtmux/async.rb)
define these ownership and request contracts.
---
# Guides
Source: https://libtmux.org/en/ruby/v0.1.1/guides/
> Guides for libtmux.
Connect to tmux and work with its sessions, windows, and panes.
- [Getting started](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/): Install the gem, create a session, and query a snapshot.
- [Attaching to tmux](https://libtmux.org/en/ruby/v0.1.1/guides/attaching-to-tmux/): Connect to an existing socket and leave its server running.
- [Server bindings](https://libtmux.org/en/ruby/v0.1.1/guides/core/): Own a private server or connect to an existing socket.
---
# Ruby MCP guides
Source: https://libtmux.org/en/ruby/v0.1.1/mcp/guides/
> Connect an MCP client to a private tmux server and select its tools.
**MCP concept example** A working example of the core library. It can change without notice.
Start with a private tmux server and the default read-only catalog. The client
guide includes the complete launcher, dependency setup, client configuration,
and connection checks.
## Select the tmux server
`--socket PATH` selects an explicit socket; `--socket-name NAME` selects a
named socket. `--endpoint` assigns the public alias used in discovery and
resource URIs. It does not choose the socket.
The [client launcher](https://libtmux.org/en/ruby/v0.1.1/mcp/guides/connect-client/) creates and owns its tmux daemon.
When an application already owns the daemon, the MCP command borrows its
endpoint: end of input closes the protocol clients without stopping tmux.
The owning application remains responsible for the daemon's lifetime.
## Enable observation
Screen capture and waits are absent from the default catalog. In the client
guide's `run-mcp.rb`, add `"--enable-tool", "`[`tmux_capture`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_capture/)`"` or
`"--enable-tool", "`[`tmux_wait`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_wait/)`"` to the array passed to [`LibTmux::MCP::CLI.run`]().
The launcher does not forward arguments from the client configuration.
When launching the installed `libtmux-mcp` executable directly, pass
`--enable-tool `[`tmux_capture`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_capture/) or `--enable-tool `[`tmux_wait`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_wait/) as command arguments.
[`tmux_wait`]() can observe screen text or a process exit. Strong process tracking
requires tmux 3.3a or newer and a supported native identity backend. A refusal
means the required evidence is unavailable; it is not a successful wait.
See the [tool reference](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/) for arguments and results.
## Enable mutations
Add another `--enable-tool` pair for [`tmux_create`](), [`tmux_send`](), or [`tmux_close`]()
in the same launcher array or executable arguments.
Creation accepts command argument arrays. Sending text and sending named keys
are separate variants. A dispatch receipt does not claim that the pane program
completed.
The [source-owned MCP guide](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/) covers tool policy and shell
enrollment. The [snapshot topic](https://libtmux.org/en/ruby/v0.1.1/mcp/topics/snapshots-and-references/) explains
the references used to address observed objects.
- [Connect a client](https://libtmux.org/en/ruby/v0.1.1/mcp/guides/connect-client/): Build a pinned launcher, inspect its tools, and close its private daemon with the client.
---
# Getting started
Source: https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/
> Install the gem, create a session, and query a snapshot.
Create tmux sessions, split windows, send input, and capture pane output from
Ruby. Read server state into a [snapshot][snapshot], then query it with
[`where`][selection-where], [`select`][selection-select], and the rest of
`Enumerable`.
[Quick start](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#quick-start) · [Find things](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#find-things-in-a-snapshot) ·
[Packages](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#packages) · [Guide](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/index.md) ·
[API reference](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/api.md) · [Recipes](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/) ·
[API on libtmux.org](https://libtmux.org/en/ruby/latest/reference/)
[](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml)
[](https://rubygems.org/gems/libtmux)
[](https://libtmux.org/en/ruby/)
[](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/LICENSE)
Releases are numbered `0.1.N`, and each release increments `N`. The API is not
settled: any release may change or remove exported identifiers, and the
changelog marks each break. Pin an exact version to avoid one.
See the [changelog](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/CHANGELOG.md) and [release guide](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/releasing.md).
It suits scripts, tests that need a real tmux, and tools that drive a terminal
program. An [MCP server][mcp-application], an Async adapter and a workspace
loader ship beside the core, under [Packages](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#packages).
**Contents** — [Quick start](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#quick-start) ·
[Find things](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#find-things-in-a-snapshot) ·
[Your own server](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#use-the-tmux-server-you-already-run) ·
[Packages](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#packages) · [Build from source](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#build-from-source) ·
[Leaves out](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#what-it-leaves-out) · [Support](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#support-and-status) ·
[Documentation](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#documentation)
## Quick start
Install the core gem from RubyGems. It needs Ruby 3.3 or newer and tmux
3.3a or newer on `PATH`:
```console
$ gem install libtmux
```
This program creates a session, adds a window, splits it, and prints each
window with its panes. [`Server.start`][server-start] closes its private tmux
server when the block exits, and the snapshot stays readable afterward:
```ruby
require "libtmux"
snapshot = LibTmux::Server.start do |server|
session = server.new_session(name: "work", window_name: "main", command: ["/bin/cat"])
window = session.new_window(name: "logs", command: ["/bin/cat"])
window.split(direction: :horizontal, size: "40%", command: ["/bin/cat"])
server.snapshot
end
snapshot.windows.each do |window|
puts "#{window.name}: #{window.panes.map(&:id).join(', ')}"
end
```
From a checkout, run the [complete example](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/quickstart.rb):
```console
$ mise exec -- bundle exec ruby examples/quickstart.rb
```
It prints:
```text
main: %0
logs: %1, %2
```
Pane commands take argument arrays. [`Server.start`][server-start] gives its
tmux server a socket and an empty configuration of its own, so it never
touches a tmux you already use.
## Find things in a snapshot
### [`Server#snapshot`][server-snapshot]: one read, many queries
Continue with the snapshot above. [`where`][selection-where] accepts criteria
as data, and [`select`][selection-select] accepts a Ruby block. These queries
make no tmux calls:
```ruby
panes = snapshot.panes
active_ids = panes.where(active: true).map(&:id)
wide_panes = panes.select { |pane| pane.width >= 40 }
panes_by_window = panes.group_by { |pane| pane.window.name }
logs = snapshot.windows.one(name: "logs")
missing = snapshot.windows.one_or_nil(name: "missing")
```
[`one`][selection-one] raises [`NoMatchError`][nomatcherror] or
[`MultipleMatchesError`][multiplematcheserror] unless exactly one record
matches. [`one_or_nil`][selection-one_or_nil] returns `nil` for no match and
still rejects duplicates. Both errors live under `LibTmux`.
A selection keeps the membership it captured. Call
[`server.snapshot`][server-snapshot] again while the server is open to read
later changes.
The [field catalog][doc-fields] lists query fields and wire names, and the
[list/filter recipe](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/list_filter.rb) covers exact matches and queries
after close.
## Use the tmux server you already run
### [`Server.new`][server-new]: borrow a daemon
[`Server.open`][server-open] and [`Server.new`][server-new] bind an endpoint
that already exists, such as `Server.open(socket_path: ...)`. Closing the
binding leaves the daemon running. The outer [`Server.start`][server-start]
block here only supplies a daemon to borrow:
```ruby
require "libtmux"
LibTmux::Server.start do |owner|
owner.new_session(name: "work", command: ["/bin/cat"])
server = LibTmux::Server.new(endpoint: owner.endpoint)
begin
puts "connected sessions: #{server.list_sessions.length}"
ensure
server.close
end
puts "sessions after client close: #{owner.list_sessions.length}"
end
```
It prints:
```text
connected sessions: 1
sessions after client close: 1
```
See [ownership and errors][doc-ownership] for what close and failure mean.
## Packages
Start with `libtmux`. Add the companion for your caller:
| Gem | Require | Use it for |
| --- | --- | --- |
| [libtmux](https://libtmux.org/en/ruby/v0.1.1/guides/core/) | `libtmux` | Blocking scripts, snapshots, and control connections |
| [libtmux-async](https://libtmux.org/en/ruby/v0.1.1/guides/async/) | `libtmux/async` | Concurrent commands and bounded streams in Async tasks [](https://rubygems.org/gems/libtmux-async) |
| [libtmux-mcp](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/) | `libtmux/mcp` | An MCP server with explicit endpoints and tool policy [](https://rubygems.org/gems/libtmux-mcp) |
| [libtmux-workspace](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/) | `libtmux/workspace` | YAML/JSON workspace plans and a CLI to apply them [](https://rubygems.org/gems/libtmux-workspace) |
Requiring a gem starts no tmux process, scheduler, or protocol server. The
companion gems install separately. [Execution modes][doc-modes] explains
blocking calls, Async tasks, and control subscriptions.
### [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux): the core
Blocking [server][server], [session][session], [window][window] and
[pane][pane] handles, immutable [snapshots][snapshot], and
[control connections][controlconnection]. Require `libtmux`.
[API reference](https://libtmux.org/en/ruby/latest/reference/) ·
[Guides](https://libtmux.org/en/ruby/latest/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux)
```console
$ gem install libtmux
```
The [quick start](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#quick-start) and
[snapshot queries](https://libtmux.org/en/ruby/v0.1.1/guides/getting-started/#find-things-in-a-snapshot) above run against this gem.
### [libtmux-async](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-async): concurrent commands
Concurrent commands and bounded control streams inside an Async task that your
application owns. Require `libtmux/async`, then open a scope with
[`LibTmux::Async.open`][async-open].
[API reference](https://libtmux.org/en/ruby/latest/reference/libtmux-async/) ·
[Guide](https://libtmux.org/en/ruby/latest/guides/async/) ·
[RubyGems](https://rubygems.org/gems/libtmux-async)
```console
$ gem install libtmux-async
```
Capture every pane concurrently with [`Scope#map`][async-scope-map]. `server`
is an open [`LibTmux::Server`][server]:
```ruby
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
refs = scope.server.list_panes.map(&:ref)
screens = scope.map(refs, concurrency: 2) { |ref| scope.server.pane(ref).capture }
Example.check(screens.length == refs.length && screens.all?(&:success?), "concurrent captures failed")
end
end.wait
```
### [libtmux-mcp](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-mcp): a tmux server for MCP clients
An MCP server over stdio for one tmux server, with an explicit tool policy.
Installs `libtmux-mcp`; require `libtmux/mcp` to embed it through
[`LibTmux::MCP::Application`][mcp-application].
[API reference](https://libtmux.org/en/ruby/latest/mcp/reference/) ·
[Tools](https://libtmux.org/en/ruby/latest/mcp/tools/) ·
[Guides](https://libtmux.org/en/ruby/latest/mcp/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux-mcp)
```console
$ gem install libtmux-mcp
```
The server borrows a tmux server that is already running. Start one on a
socket of its own:
```console
$ tmux -L agent new-session -d -s work
```
Add the server to Claude Code. Until you enable more tools, it serves
[`tmux_capabilities`][tool-tmux_capabilities] and
[`tmux_snapshot`][tool-tmux_snapshot]:
```console
$ claude mcp add tmux -- libtmux-mcp --socket-name agent --endpoint dev
```
The [MCP README](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/) covers other clients, every tool
and authored shell commands. Tracked captures, waits and authored runs need
tmux 3.3a or newer and native process identity.
### [libtmux-workspace](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-workspace): YAML workspaces
YAML or JSON workspaces, validated, planned offline and applied to an open
server. Installs `libtmux-workspace`; require `libtmux/workspace` to use it
from Ruby through [`LibTmux::Workspace`][workspace].
[API reference](https://libtmux.org/en/ruby/latest/workspace/reference/) ·
[Command reference](https://libtmux.org/en/ruby/latest/workspace/cli/) ·
[Guides](https://libtmux.org/en/ruby/latest/workspace/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux-workspace)
```console
$ gem install libtmux-workspace
```
Save this workspace as `workspace.yaml`:
```yaml
session_name: work
environment:
PROJECT_MODE: development
windows:
- window_name: editor
window_index: 1
layout: tiled
panes:
- shell_command: printf 'editor ready\n'
- {}
```
Check it without contacting tmux:
```console
$ libtmux-workspace validate workspace.yaml
```
```text
Workspace configuration is valid.
```
The [workspace README](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/) loads it into a
running server and lists every option, including how `--switch` moves a
client.
## Build from source
Use the Ruby pinned in [.tool-versions](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/.tool-versions) and have `tmux` on your
`PATH`. From this checkout, install the development bundle:
```console
$ mise install
```
```console
$ mise exec -- bundle config set --local path vendor/bundle
```
```console
$ mise exec -- bundle install
```
To use the core outside this checkout, build the artifacts:
```console
$ mise exec -- bundle exec rake build
```
Install into the Ruby environment that will run your application. The core's
runtime dependencies must already be installed for this local-only command:
```console
$ gem install \
--local \
--no-document \
pkg/libtmux-0.1.1.gem
```
Companion gems need their declared runtime dependencies too. The
[packaging check](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/.github/CONTRIBUTING.md#checks) verifies each gem in an
isolated installation and runs the recipes outside the checkout.
## What it leaves out
- **Live objects.** A [snapshot][snapshot] record is what tmux said when you
read it, and a selection keeps the membership it captured. Read again to see
a change.
- **A terminal emulator.** [`Pane#capture`][pane-capture] returns what tmux's
[`capture-pane`](https://libtmux.org/en/tmux/latest/manual/capture-pane/) prints for the pane.
- **A server it starts on its own.** [`Server.new`][server-new] and
[`Server.open`][server-open] bind a running server and never start one. With
no socket named, [`Endpoint.new`][endpoint-new] picks the one tmux would:
`LIBTMUX_SOCKET_PATH`, `LIBTMUX_SOCKET`, the server `TMUX` names, then tmux's
default socket. The recipes name a socket or own one.
- **Whole-program typing.** RBS declarations ship with each gem, and selected
installed calls are checked. See the [public methods][doc-api].
- **Throughput claims.** The [benchmarks][doc-benchmark] list their workloads,
measurements and limits.
## Support and status
Releases are numbered `0.1.N`, and each release increments `N`. The API is not
settled: any release may change or remove exported identifiers, and the
changelog marks each break. Pin an exact version to avoid one.
- **Ruby 3.3 or newer** and **tmux 3.3a or newer.** Unpatched 3.3 is below the
minimum. A [server handle][server] checks the connected daemon's release, not
the client's, and refuses an older daemon before any requested command runs.
tmux release candidates are not supported: a feature can require one only
until its release ships, and a candidate such as `X.Y-rcN` ranks below
`X.Y`. See [Release candidates][rc-policy].
- **Linux, including WSL, and macOS.** The [compatibility workflow][compat]
runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04. A push
and a pull request run the oldest and newest of those releases, 3.3a and 3.8,
and a release runs all of them. macOS is supported on a best-effort basis, and CI coverage for it is highly
limited.
- **Control connections need tmux 3.7 or newer.** On an older daemon
`open_control` raises [`UnsupportedFeatureError`]() before a client attaches,
and `Server#supports_control?` returns false. Other optional features carry
their own releases in the [optional-feature table][doc-optional-features];
[`capabilities.json`][capabilities-json] holds the same records for tools.
- **tmux master and old daemons.** On Ubuntu the workflow also runs the suite
against a pinned tmux master, and checks that tmux 3.0a, 3.1c, 3.2a and 3.3
daemons are refused.
- **Per-revision results.** Check the workflow's results for your revision
before relying on one combination.
## Documentation
- [Send text and capture output](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/layout_io.rb): split panes, wait for
output events, and round-trip binary buffers.
- [Work with linked windows](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/window_links.rb): address one window at
several session indexes.
- [Capture concurrently](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/async_cancel.rb): read panes while another
request waits, then cancel it.
- [Load a workspace](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/workspace_apply.rb): parse a configuration, plan,
and apply it.
- [All recipes](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/): cancellation, control streams, command groups,
and MCP.
- [API reference](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/api.md): public methods, source links, and
behavioral contracts. RBS declarations ship with each gem; selected installed
calls are checked, without a whole-program typing guarantee.
- [Benchmarks](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/benchmark.md): workloads, measurements, and their limits.
See [Contributing](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/.github/CONTRIBUTING.md) for setup and checks.
## Attribution
Please use the following BibTeX template to cite libtmux-ruby in scientific discourse:
```bibtex
@misc{libtmux-ruby,
author = {Tony Narlock},
year = {2026},
note = {https://libtmux.org/en/ruby/},
title = {libtmux-ruby: Ruby wrapper for tmux}
}
```
## License
MIT. See [LICENSE](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/LICENSE).
[async-open]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-open/
[async-scope-map]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-scope-map/
[compat]: https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml
[controlconnection]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlconnection/
[doc-api]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/api.md
[doc-benchmark]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/benchmark.md
[doc-fields]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/fields.md
[doc-modes]: https://libtmux.org/en/ruby/v0.1.1/concepts/transports/
[doc-optional-features]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#optional-tmux-features
[capabilities-json]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/capabilities.json
[doc-ownership]: https://libtmux.org/en/ruby/v0.1.1/topics/errors-and-exceptions/
[endpoint-new]: https://libtmux.org/en/ruby/latest/reference/libtmux-endpoint-new/
[mcp-application]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application/
[multiplematcheserror]: https://libtmux.org/en/ruby/latest/reference/libtmux-multiplematcheserror/
[nomatcherror]: https://libtmux.org/en/ruby/latest/reference/libtmux-nomatcherror/
[pane]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-14z6/
[pane-capture]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-capture/
[rc-policy]: https://libtmux.org/en/tmux/guides/tmux-support/#release-candidates
[selection-one]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-one/
[selection-one_or_nil]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-one_or_nil/
[selection-select]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-select/
[selection-where]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-where/
[server]: https://libtmux.org/en/ruby/latest/reference/libtmux-server/
[server-new]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-new/
[server-open]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-open/
[server-snapshot]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-snapshot/
[server-start]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-start/
[session]: https://libtmux.org/en/ruby/latest/reference/libtmux-session-nvps/
[snapshot]: https://libtmux.org/en/ruby/latest/reference/libtmux-snapshot/
[tool-tmux_capabilities]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capabilities/
[tool-tmux_snapshot]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_snapshot/
[window]: https://libtmux.org/en/ruby/latest/reference/libtmux-window-1vfx/
[workspace]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace/
---
# Attaching to tmux
Source: https://libtmux.org/en/ruby/v0.1.1/guides/attaching-to-tmux/
> Connect to an existing tmux server and find a session with Ruby.
Connect a `Server` to an explicit socket and find the existing `work` session.
The program prints its name and leaves the tmux server running. It reports an
error if the connection fails or the session is absent.
This controls tmux from your program. To open a session in your terminal, use
[`tmux attach-session`](https://libtmux.org/en/tmux/latest/manual/attach-session/); the [shared guide](https://libtmux.org/en/tmux/guides/attaching-to-tmux/) covers
interactive attachment and detaching.
## Connect to an existing server
Save the complete program as `connect.rb`. `LIBTMUX_SOCKET_PATH` selects
the existing server. The launcher below supplies a private socket for trying
the example.
```ruby title="connect.rb"
require "libtmux"
LibTmux::Server.open(socket_path: ENV.fetch("LIBTMUX_SOCKET_PATH")) do |server|
session = server.snapshot.sessions.find { |candidate| candidate.name == "work" }
raise "The work session does not exist" unless session
puts session.name
end
```
## Setup and run
Use an empty directory on Linux with Git and tmux 3.3a or newer installed.
This example was checked with Ruby 4.0.7, Bundler.
Save this file beside the program using the displayed filename.
```ruby title="Gemfile"
source "https://rubygems.org"
gem "libtmux", path: "libtmux-source/gems/libtmux"
```
Save the launcher as `run.sh`. It starts an isolated tmux server, runs the
program, checks that the session still exists, then stops only that server.
Cleanup runs after failures too. A failed shutdown keeps its socket directory
and prints its location for inspection.
```sh title="run.sh"
#!/bin/sh
set -eu
binary=$(command -v tmux)
directory=$(mktemp -d "${TMPDIR:-/tmp}/libtmux-ruby-attach.XXXXXX")
socket="$directory/tmux.sock"
cleanup() {
status=$?
trap - 0 HUP INT TERM
if [ -S "$socket" ] && ! "$binary" -S "$socket" kill-server; then
printf 'Cannot stop tmux; kept %s\n' "$directory" >&2
exit 1
fi
rm -rf "$directory" || exit 1
exit "$status"
}
trap cleanup 0
trap 'exit 1' HUP INT TERM
unset TMUX TMUX_PANE
export LIBTMUX_SOCKET_PATH="$socket" TMUX_BIN="$binary"
"$binary" -S "$socket" -f /dev/null new-session -d -s work /bin/cat
"$@"
"$binary" -S "$socket" has-session -t '=work'
```
Fetch the verified library revision, build, and run:
```console
$ git clone https://github.com/libtmux/libtmux-ruby libtmux-source &&
git -C libtmux-source checkout 2599d45369515aaf2fd5793bfe71cdc20de641b6 &&
bundle config set --local path vendor/bundle &&
bundle install &&
sh run.sh bundle exec ruby connect.rb
```
The program prints `work`. To use an existing server of your own, set
`LIBTMUX_SOCKET_PATH` to its socket and run the program without the launcher.
That launcher is responsible for the demonstration server's lifetime.
## Find or create a session
The example only looks up a session. If your application creates a session
after an unsuccessful lookup, another client may create the same name between
those operations. Handle the creation error instead of assuming the lookup
reserves the name.
For a complete program that starts and owns its server, see
[Capture pane output](https://libtmux.org/en/ruby/v0.1.1/examples/capture-pane-output/).
---
# MCP server guide
Source: https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/
> MCP server guide: libtmux documentation.
**MCP concept example** A working example of the core library. It can change without notice.
Expose an existing tmux server over MCP stdio. Read snapshots, capture pane
output and wait for events, or enable creation, input and shell commands one
tool at a time.
[API reference](https://libtmux.org/en/ruby/latest/mcp/reference/) ·
[Tools](https://libtmux.org/en/ruby/latest/mcp/tools/) ·
[Guides](https://libtmux.org/en/ruby/latest/mcp/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux-mcp)
[](https://libtmux.org/en/ruby/latest/mcp/)
[](https://rubygems.org/gems/libtmux-mcp)
Part of [libtmux for Ruby](https://github.com/libtmux/libtmux-ruby#readme).
Built on [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux)
and [libtmux-async](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-async).
An MCP client starts `libtmux-mcp` and talks to it over stdin and stdout. The
official MCP SDK handles the protocol, and bounded Async tasks handle
transport.
**Contents** — [Quick start](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#quick-start) ·
[Connect a client](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#connect-a-client) ·
[Choose the server](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#choose-which-tmux-server-it-serves) ·
[The tools](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#the-tools) · [Read](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#read-the-layout-and-the-screen) ·
[Create](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#create-type-and-close) · [Run](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#run-a-script-in-a-pane) ·
[Embed](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#embed-it-in-ruby) · [Leaves out](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#what-it-leaves-out) ·
[Support](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#support-and-status) · [Documentation](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#documentation)
## Quick start
Install the gem and its `libtmux-mcp` executable. It needs Ruby 3.3 or newer
and tmux 3.3a or newer:
```console
$ gem install libtmux-mcp
```
The server borrows a tmux server that is already running. Start one on a
socket of its own:
```console
$ tmux -L agent new-session -d -s work
```
Add the server to Claude Code. Until you enable more tools, it serves
[`tmux_capabilities`][tool-tmux_capabilities] and
[`tmux_snapshot`][tool-tmux_snapshot]:
```console
$ claude mcp add tmux -- libtmux-mcp --socket-name agent --endpoint dev
```
Run by hand, `libtmux-mcp` waits for input until EOF. `libtmux-mcp --help`
lists every option.
## Connect a client
### `claude mcp add`: Claude Code
The command in the quick start registers the server with Claude Code. Start the
tmux server first, as above.
### `codex mcp add`: Codex CLI
Codex CLI takes the same command after `--`:
```console
$ codex mcp add tmux -- libtmux-mcp --socket-name agent --endpoint dev
```
### `mcpServers`: clients that read JSON
A client that reads a JSON configuration, such as Claude Desktop's
`claude_desktop_config.json`, takes the same command as an `mcpServers` entry:
```json
{
"mcpServers": {
"tmux": {
"command": "libtmux-mcp",
"args": ["--socket-name", "agent", "--endpoint", "dev"]
}
}
}
```
If the client does not start servers with your shell's `PATH`, set `command` to
the absolute path that `command -v libtmux-mcp` prints.
## Choose which tmux server it serves
### `--socket` and `--socket-name`: pick the daemon
Set `TMUX_SOCKET` to an existing tmux socket. This command borrows that daemon
and serves MCP on stdin and stdout:
```console
$ libtmux-mcp \
--socket "$TMUX_SOCKET" \
--endpoint local
```
Use `--socket-name NAME` instead of `--socket PATH` to select a named socket,
as the quick start does:
```console
$ libtmux-mcp \
--socket-name agent
```
- With neither flag, the server borrows the socket in `LIBTMUX_SOCKET_PATH`,
else the name in `LIBTMUX_SOCKET`, else the server `TMUX` names when it runs
inside tmux, else tmux's default socket. When both variables are set, the
path wins, and an empty variable counts as unset. A flag ignores both
variables, and the two flags are refused together.
- The server never starts a tmux server. When none answers at the socket it
chose, it exits with status 1.
- Started from a tmux pane with no flag or variable, the server drives that
pane's server. Pass `--socket-name` to give an agent a server of its own.
- EOF retires the clients the server owns and leaves the daemon running.
- `--endpoint` sets the public alias in discovery and resource URIs, and
defaults to `local`.
## The tools
Each tool name links to its page on libtmux.org, with the arguments, limits and
result schema. Repeat `--enable-tool` for each additional tool.
| Tools | Default | Purpose |
| --- | --- | --- |
| [`tmux_capabilities`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capabilities/), [`tmux_snapshot`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_snapshot/) | Enabled | Discover capabilities and query captured metadata |
| [`tmux_capture`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capture/), [`tmux_wait`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_wait/) | Disabled | Capture a screen or wait for text/process exit |
| [`tmux_create`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_create/), [`tmux_send`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_send/), [`tmux_close`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_close/) | Disabled | Create entities, send text/keys and tear down exact targets |
| [`tmux_run`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_run/) | Disabled | Run a script in an explicitly enrolled zsh shell |
- Disabled tools are absent from discovery and denied on direct application
calls.
- An unknown tool name is refused when the server starts.
The [complete protocol recipe](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/mcp_protocol.rb) exercises
discovery, snapshots, default denial, enabled mutations, cancellation and EOF
cleanup through actual pipes, including the installed executable.
## Read the layout and the screen
### [`tmux_snapshot`][tool-tmux_snapshot]: the layout, in pages
- Pages come from one immutable capture, and a cursor continues that capture.
- An expired, evicted or foreign cursor returns an error. The server never
substitutes a fresh listing.
### [`tmux_capture`][tool-tmux_capture]: a pane's screen
Enable the screen tools together:
```console
$ libtmux-mcp \
--socket "$TMUX_SOCKET" \
--enable-tool tmux_capture \
--enable-tool tmux_wait
```
- It reads a bounded screen and keeps its line endings. Bytes that are not
valid UTF-8 come back base64-encoded.
- With tracking, a call returns a cursor, and later calls return a splice
against that exact captured state. A cursor from another server generation
or from a respawned pane is refused.
- It refuses a pane whose effective [`after-capture-pane`](https://libtmux.org/en/tmux/latest/catalog/hooks/after-capture-pane/) hook is not empty,
including inherited entries.
- On tmux 3.3a to 3.4 the hook is checked before the capture command runs, so
keep hook configuration stable while observing.
### [`tmux_wait`][tool-tmux_wait]: wait for text or exit
- It waits for screen text, or for the pane process to exit. Screen text
comes from control events on tmux 3.7 and newer, and from polling the pane
every 50 ms on older daemons, which take no control client. Process exit
uses process descriptors.
- Wrapped rows are joined, so text that the pane width split across rows
still matches.
- If the pane, or the tmux server holding it, ends before the text appears,
the call fails with `pane_died`, `delivery: observed` and the last rows it
read in the error's [`capture`](). If only the control stream ends while the
pane lives, such as after the client detaches, the failure is
`observation_lost`.
- A cancelled or expired wait closes its observers and never signals the pane
program.
- Process tracking needs a native identity backend: Linux peer pidfds with
matching PID namespaces, or Darwin kqueue. Without one, the call is refused.
### [Resource templates][mcp-resources]: the same data as URIs
[`tmux_metadata`][resource-tmux_metadata],
[`tmux_metadata_page`][resource-tmux_metadata_page] and
[`tmux_screen`][resource-tmux_screen] expose metadata pages and pane screens
under endpoint and generation URIs, with the same policy and limits as their
tools.
- The server does not advertise resource subscriptions.
- [Snapshots, cursors and references][mcp-snapshots-topic] has the full
contract.
## Create, type and close
### [`tmux_create`][tool-tmux_create]: new sessions, windows and panes
Enable the three tools together:
```console
$ libtmux-mcp \
--socket-name agent \
--enable-tool tmux_create \
--enable-tool tmux_send \
--enable-tool tmux_close
```
- It takes argument arrays as [`argv`]().
- Results carry the references tmux returned.
### [`tmux_send`][tool-tmux_send]: literal text or named keys
- Text and keys are separate variants.
- It reports `dispatch_only`: the input was dispatched, and the command has not
necessarily finished.
- After a cancellation, the effect stays unknown and is not reported as none.
### [`tmux_close`][tool-tmux_close]: an exact target
- It takes an exact session, window or pane reference, and never selects by
name.
## Run a script in a pane
### [`tmux_run`][tool-tmux_run]: authored commands in an enrolled zsh
[`tmux_run`][tool-tmux_run] runs a POSIX script in a pane's interactive zsh
5.9. It needs both the tool and an enrolled pane.
List the pane IDs of the server:
```console
$ tmux -L agent list-panes -a -F '#{pane_id}'
```
```text
%0
```
Enable the tool and write a setup file for one exact pane:
```console
$ libtmux-mcp \
--socket-name agent \
--enable-tool tmux_run \
--enroll-pane %0=enroll.zsh
```
Then, in that pane's zsh, run `source enroll.zsh`. A relative path resolves
from the directory where the server starts.
- `--enroll-pane` requires `--enable-tool `[`tmux_run`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_run/), and each pane and each
file may appear once.
- The server writes a private file (mode `0600`) and never types into the
terminal. It refuses an existing file or a symlink.
- Setup files expire. `--enrollment-timeout SECONDS` sets the lifetime, at most
300.
- EOF retires pending enrollment and removes only the files the server wrote.
- A call takes an exact pane [`target`](), a POSIX `script`, and optional
`stdout_limit` and `stderr_limit` byte counts.
- The script runs with the shell's working directory and exported environment.
- Stdout and stderr come back separately, as UTF-8 or base64.
- A nonzero exit or a signal is a completion result. Output over its limit is
an error with completion unobserved, and never a truncated success.
- Cancelling a run does not prove that its child processes stopped.
## Embed it in Ruby
Require `libtmux/mcp`. Imports start no tmux process, scheduler or MCP server.
The [execution guide](https://libtmux.org/en/ruby/v0.1.1/concepts/transports/) covers result and ownership
boundaries.
### [`LibTmux::MCP::Application`][mcp-application]: serve from your own program
[`Application`][mcp-application] borrows an application-owned
[`LibTmux::Async::Server`][async-server]. Its
[`sdk_server`][mcp-application-sdk_server] supplies the SDK server that
[`StdioTransport`][mcp-stdiotransport] consumes:
```ruby
application = LibTmux::MCP::Application.new(server: scope.server, endpoint_name: "example")
sdk = application.sdk_server
input, client_input = IO.pipe
client_output, output = IO.pipe
transport = LibTmux::MCP::StdioTransport.new(server: sdk, parent: parent, input: input, output: output)
runner = parent.async { transport.run }
```
The [complete program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/mcp_protocol.rb) runs this excerpt inside
an Async task with an open [`LibTmux::Async`][async] scope. It sends real
JSON-RPC messages through the pipes, checks the replies, and prints
`PASS mcp_protocol`.
### [`Application#invite_shell`][mcp-application-invite_shell]: enroll a shell from Ruby
Call [`invite_shell`]() with a pane reference, `timeout:` and `expires_in:`, then
pass the invitation to [`accept_shell`][mcp-application-accept_shell].
- The invitation's `shell_arguments` make up the setup command to source in
that pane's zsh.
- `timeout:` bounds acquisition, and `expires_in:` sets the enrollment
lifetime.
- The application owns invitations and accepted connections until
[`close`][mcp-application-close]. Direct enrollment calls enforce tool
policy.
## What it leaves out
- **Tools you have not enabled.** Disabled tools are absent from discovery,
and a direct application call is denied. See [The tools](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/#the-tools).
- **Resource subscriptions.** The server does not advertise them. See the
[resource templates][mcp-resources].
- **Typing into your terminal.** The server writes a private setup file and
never types into a pane's terminal to enroll it. See
[`tmux_run`][tool-tmux_run].
- **A guaranteed stop.** Cancelling a run does not prove that its child
processes stopped. See [`tmux_run`][tool-tmux_run].
## Support and status
Releases are numbered `0.1.N`, and each release increments `N`. The API is not
settled: any release may change or remove exported identifiers, and the
changelog marks each break. Pin an exact version to avoid one.
- **Ruby 3.3 or newer.** The gem depends on `libtmux` and `libtmux-async` at
the same version, the `mcp` SDK 1.5, `digest` and `optparse`.
- **tmux 3.3a or newer.** The server refuses an older daemon when it starts,
before it serves any tool, and prints the reason and the remedy on stderr.
- **A daemon that changes while it runs.** A tool call returns an
`unsupported` result with `delivery: not_sent`. The result names the floor
and says to upgrade tmux and restart the server.
- **The `screen_contains` wait.** It uses control events on tmux 3.7 and
newer and polls the pane screen on older daemons, which take no control
client.
- **Linux, including WSL, and macOS.** The core
[compatibility workflow][compat] runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a
through 3.8 on Ubuntu 24.04. A push and a pull request run the oldest and
newest of those releases, 3.3a and 3.8, and a release runs all of them. macOS
is supported on a best-effort basis, and CI coverage for it is highly
limited.
## Documentation
| | |
| --- | --- |
| [Reference](https://libtmux.org/en/ruby/latest/mcp/reference/) | The embedding types and methods |
| [Tools](https://libtmux.org/en/ruby/latest/mcp/tools/) | One page per tool, with arguments, limits and result schema |
| [Guides][mcp-guides] | Connect a client to a private tmux server and select its tools |
| [Resources][mcp-resources] | Metadata pages and pane screens as URIs |
| [Public behavior][behavior-mcp-application] | The application, transport and command |
[async]: https://libtmux.org/en/ruby/latest/reference/libtmux-async/
[async-server]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-server/
[behavior-mcp-application]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#mcp-application
[compat]: https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml
[mcp-application]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application/
[mcp-application-accept_shell]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-accept_shell/
[mcp-application-close]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-close/
[mcp-application-invite_shell]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-invite_shell/
[mcp-application-sdk_server]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-sdk_server/
[mcp-guides]: https://libtmux.org/en/ruby/latest/mcp/guides/
[mcp-resources]: https://libtmux.org/en/ruby/latest/mcp/resources/
[mcp-snapshots-topic]: https://libtmux.org/en/ruby/latest/mcp/topics/snapshots-and-references/
[mcp-stdiotransport]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-stdiotransport/
[resource-tmux_metadata]: https://libtmux.org/en/ruby/latest/mcp/resources/templates/tmux_metadata/
[resource-tmux_metadata_page]: https://libtmux.org/en/ruby/latest/mcp/resources/templates/tmux_metadata_page/
[resource-tmux_screen]: https://libtmux.org/en/ruby/latest/mcp/resources/templates/tmux_screen/
[tool-tmux_capabilities]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capabilities/
[tool-tmux_capture]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capture/
[tool-tmux_close]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_close/
[tool-tmux_create]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_create/
[tool-tmux_run]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_run/
[tool-tmux_send]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_send/
[tool-tmux_snapshot]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_snapshot/
[tool-tmux_wait]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_wait/
---
# Workspace guide
Source: https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/
> Workspace guide: libtmux documentation.
**Workspace Manager concept example** A working example of the core library. It can change without notice.
[Load][workspace-load] bounded YAML or JSON, inspect an immutable
[creation plan][workspace-plan-class], then explicitly
[apply][workspace-plan-apply] it to an open libtmux server.
[API reference](https://libtmux.org/en/ruby/latest/workspace/reference/) ·
[Command reference](https://libtmux.org/en/ruby/latest/workspace/cli/) ·
[Guides](https://libtmux.org/en/ruby/latest/workspace/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux-workspace)
[](https://libtmux.org/en/ruby/latest/workspace/)
[](https://rubygems.org/gems/libtmux-workspace)
Part of [libtmux for Ruby](https://github.com/libtmux/libtmux-ruby#readme).
Built on [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux).
Check a workspace file, see the ordered steps a load would run, and create the
session on a running tmux server, from the `libtmux-workspace` command or from
Ruby.
Validation and offline planning need no running daemon, and imports start no
tmux process.
**Contents** — [Quick start](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#quick-start) ·
[Write the file](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#write-the-file) ·
[Create the session](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#create-the-session) ·
[Failed apply](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#recover-from-a-failed-apply) ·
[Attach or switch](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#attach-or-switch-to-the-new-session) ·
[Scripts](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#script-it) · [Leaves out](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#what-it-leaves-out) ·
[Support](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#support-and-status) · [Documentation](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/#documentation)
## Quick start
Install the gem and its `libtmux-workspace` executable. It needs Ruby 3.3 or
newer, and tmux 3.3a or newer to load a plan:
```console
$ gem install libtmux-workspace
```
Save this declared subset of tmuxp-style data as `workspace.yaml`. Relative
directories resolve against the configuration file's directory.
```yaml
session_name: work
environment:
PROJECT_MODE: development
windows:
- window_name: editor
window_index: 1
layout: tiled
panes:
- shell_command: printf 'editor ready\n'
- {}
```
Check the file. [`validate`][cli-validate] reads no tmux state:
```console
$ libtmux-workspace validate workspace.yaml
```
```text
Workspace configuration is valid.
```
Print the ordered operations a load would run. Offline [`plan`][cli-plan] does
not contact tmux either:
```console
$ libtmux-workspace plan workspace.yaml
```
```text
offline_create: 11 ordered steps
1. create_session session (creation)
2. set_session_option session (configuration)
3. set_window_option window:0 (configuration)
4. move_initial_window window:0 (configuration)
5. split_pane pane:0:0 (creation)
6. select_layout window:0 (configuration)
7. send_command pane:0:0 (dispatch_only)
8. unset_window_option window:0 (configuration)
9. select_pane pane:0:0 (configuration)
10. select_window window:0 (configuration)
11. unset_session_option session (configuration)
```
## Write the file
### [`Workspace.load`][workspace-load]: the declared subset
[`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) reads a regular file of bounded size and infers JSON or YAML from its
extension. [`Workspace.parse`][workspace-parse] takes the text and a base
directory instead. Both return an immutable workspace:
- Root: [`session_name`](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/), nonempty `windows`, `options`, `window_options`.
- Window: [`window_name`](https://libtmux.org/en/tmux/latest/catalog/formats/window_name/), nonempty `panes`, [`window_index`](https://libtmux.org/en/tmux/latest/catalog/formats/window_index/), `focus`, [`layout`](),
`options`.
- Pane: a command string, or a mapping with `focus`, `split` and [`size`]().
- Root, windows and panes accept `start_directory`, `environment`,
`shell_command` and `shell_command_before`.
- Layouts are `even-horizontal`, `even-vertical`, `main-horizontal`,
`main-vertical` and `tiled`.
- At most one window and one pane per window declare focus.
- Without a filename, the command looks for exactly one `.tmuxp.yaml`,
`.tmuxp.yml` or `.tmuxp.json` in the current directory.
- An unknown field or an unsupported feature fails with a
[`ConfigError`][workspace-configerror] that names its position in the file.
- YAML tags, anchors and aliases are rejected, and no Ruby, ERB, plugins or
callbacks run.
- `${NAME}` substitution is opt-in: pass
`expand_environment: true, environment: {"NAME" => "value"}` to [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) or
[`parse`](), or `--expand-environment --env NAME=VALUE` to the command.
- [`workspace.to_h`][workspace-to_h] exports an immutable, normalized and
reloadable configuration, with expanded values and absolute directories.
The full field, option and limit lists are in
[Workspace parsing][behavior-workspace-parsing].
## Create the session
### [`libtmux-workspace load`][cli-load]: build it on a running server
[`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) needs a running server. It never starts one, so start a private server
with a keepalive session and no user configuration:
```console
$ WORKSPACE_TMP=$(mktemp -d)
```
```console
$ tmux -S "$WORKSPACE_TMP/tmux.sock" -f /dev/null new-session -d -s keepalive
```
Create the workspace's session on that socket. It stays detached:
```console
$ libtmux-workspace load \
--socket "$WORKSPACE_TMP/tmux.sock" \
workspace.yaml
```
```text
Workspace created; 11 steps completed. Shell commands were dispatched.
```
The new `work` session holds the `editor` window:
```console
$ tmux -S "$WORKSPACE_TMP/tmux.sock" list-windows -t work
```
Stop only the private server and remove its directory:
```console
$ tmux -S "$WORKSPACE_TMP/tmux.sock" kill-server
```
```console
$ rm -rf "$WORKSPACE_TMP"
```
Loading the same file again fails with status 1: a plan creates a new session
and never reconciles an existing one.
### [`Plan#apply`][workspace-plan-apply]: the same from Ruby
[`Workspace.load`][workspace-load] reads the file,
[`workspace.plan`][workspace-plan-method] plans it against a snapshot, and
[`apply`]() creates the session. Calling [`apply`]() authorizes the configuration's
shell commands:
```ruby
require "libtmux"
require "libtmux/workspace"
LibTmux::Server.start do |server|
workspace = LibTmux::Workspace.load(File.expand_path("../workspace.yaml", __dir__))
plan = workspace.plan(snapshot: server.snapshot)
puts "planned steps: #{plan.steps.length}"
result = plan.apply(server: server)
puts "applied: #{result.success?}"
puts "sessions: #{server.list_sessions.map { |session| session.snapshot.name }.join(', ')}"
end
```
The program runs next to the [`workspace.yaml`]() above, on a private server, and
prints:
```text
planned steps: 11
applied: true
sessions: work
```
- A snapshot makes the plan reject a name that already exists, and every
[`apply`]() takes a fresh snapshot and checks again.
- The plan creates a new session. It does not reconcile, replace or remove an
existing one.
- Success shows that tmux accepted the dispatch of each shell command. It does
not show that the command finished.
The [complete program](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/workspace_apply.rb) also runs the
installed command with `--switch`. The contract is in
[Workspace apply][behavior-workspace-apply].
## Recover from a failed apply
### [`ApplyError#result`][workspace-applyerror-result]: what exists after a failure
A failed apply raises [`ApplyError`][workspace-applyerror]. Its immutable
[`result`]() holds the completed step IDs, the
[`created_refs`][workspace-applyresult-created_refs] tmux returned, the
effects it observed, and the failed action. Pass `compensate: true` to remove
only the session the apply created:
```ruby
require "json"
require "libtmux"
require "libtmux/workspace"
LibTmux::Server.start do |server|
server.new_session(name: "borrowed", command: ["/bin/cat"])
crowded = LibTmux::Workspace.parse(
JSON.generate({session_name: "crowded", windows: [{window_name: "small", panes: Array.new(40) { {} }}]}),
format: :json, base_directory: __dir__
)
begin
crowded.plan.apply(server: server, compensate: true)
rescue LibTmux::Workspace::ApplyError => error
puts "failed action: #{error.result.failed_action}"
puts "created a session: #{error.result.created_refs.key?('session')}"
puts "compensation: #{error.result.compensation}"
end
puts "sessions: #{server.list_sessions.map { |session| session.snapshot.name }.join(', ')}"
end
```
That many panes do not fit in one window, so tmux refuses a split after the
session exists. It prints:
```text
failed action: split_pane
created a session: true
compensation: completed
sessions: borrowed
```
- Compensation removes the new session only after a tmux guard shows that
every window and pane in it has a positively identified created reference.
- An unknown initial entity, or a borrowed entity moved into the session,
makes it refuse.
- Compensation cannot undo what shell commands did. Without it, the partial
state stays for you to inspect.
- Diagnostics omit command payloads and paths. Names are never used to guess
ownership.
The command takes `--compensate`. The contracts are in
[Workspace results][behavior-workspace-results].
## Attach or switch to the new session
### [`load --switch`][cli-load]: move a client to the session
- [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/)` `[`--attach`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#options-attach) opens the CLI's `/dev/tty` after creation and runs an owned
terminal client until you detach. It requires a valid `TERM`.
- [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/)` `[`--switch`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/#options-switch-client)` CLIENT` switches a current tmux client to the created session
after apply, keeping the session's environment.
- `CLIENT` is a client name, a full TTY path or a TTY path without `/dev/`.
Use `--switch=VALUE` for a selector that begins with `-`.
- A missing client, or a failure to open or attach the terminal, keeps the
created session and returns status 3.
- Attach and switch are mutually exclusive, and neither infers a latest
client. The library's [`Plan#apply`][workspace-plan-apply] does neither.
The selector rules are in [Client switching][behavior-client-switching].
## Script it
### [`--json`][cli-load]: one object on stdout
Check the installed gem version without reading a configuration or contacting
tmux:
```console
$ libtmux-workspace --version
```
Human plans list ordered operations and their effects. `--json` prints the
same plan data as [`Plan#to_h`][workspace-plan-to_h], including configured
command text and paths:
```console
$ libtmux-workspace plan \
--json \
workspace.yaml
```
[`load`][cli-load] and [`plan --live`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/plan/) act on an existing server. Without
`--socket`, they use the socket in `LIBTMUX_SOCKET_PATH`, else the name in
`LIBTMUX_SOCKET`, else the server `TMUX` names when run inside tmux, else
tmux's default socket. When both variables are set, the path wins, an empty
variable counts as unset, and `--socket` ignores both. A command run from a tmux
pane with none of these set acts on that pane's server. This detached creation
example names its socket with a `TMUX_SOCKET` value:
```console
$ libtmux-workspace load \
--socket "$TMUX_SOCKET" \
--json \
workspace.yaml
```
`libtmux-workspace --help` lists every option. The
[command reference][ws-cli] has a page for each command.
| Option | Effect |
| --- | --- |
| `--json` | Write one structured result or error object to stdout |
| `--socket PATH` | Select the existing server's socket for [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) or [`plan --live`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/plan/); the default follows the socket variables, then `TMUX`, then tmux's default socket |
| `--live` | Capture a snapshot before planning |
| `--timeout SECONDS` | Bound each apply, live capture or switch operation; the default is 5 |
| `--compensate` | Run guarded cleanup after an apply failure |
| `--attach` | Attach this CLI's terminal after a successful [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) |
| `--switch CLIENT` | Switch an explicit current client to the new session after [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) |
| `--expand-environment` | Expand `${NAME}` in directories and environment values; requires `--env` |
| `--env NAME=VALUE` | Supply a value for expansion; ambient environment variables are not copied |
| Exit status | Meaning |
| --- | --- |
| 0 | Validation, planning or apply succeeded; requested attach/switch succeeded |
| 1 | Execution failed before known application effects |
| 2 | Configuration or arguments are invalid |
| 3 | Application was partial or uncertain, or a later attach/switch/cleanup failed |
| 130 | Interrupted; available effect ledger is retained |
Apply errors include [`ApplyResult#to_h`][workspace-applyresult-to_h]. Human
errors write the diagnostic and any ledger to stderr. Diagnostics omit
configuration payloads; plan rendering and canonical export contain those
values by design.
## What it leaves out
- **Reconciliation.** A plan creates a new session and never replaces or
removes an existing workspace. The contract is in
[Workspace plans][behavior-workspace-plans].
- **Shell command completion.** Apply records a shell command as dispatched.
Embedded newlines can execute while the text is inserted. See
[Workspace apply][behavior-workspace-apply].
- **Rollback of shell effects.** Compensation removes only tmux objects the
apply created. See [Workspace results][behavior-workspace-results].
- **Features beyond the declared subset.** Plugins, ERB and callbacks are
rejected, and a [`ConfigError`][workspace-configerror] names the position.
- **A guessed client.** Attach and switch need an explicit client. See
[Client switching][behavior-client-switching].
## Support and status
Releases are numbered `0.1.N`, and each release increments `N`. The API is not
settled: any release may change or remove exported identifiers, and the
changelog marks each break. Pin an exact version to avoid one.
- **Ruby 3.3 or newer.** The gem depends on the same version of `libtmux`,
JSON 3.0 and Psych 5.5.
- **tmux 3.3a or newer** to load or apply a plan.
- **Older daemons.** [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) creates nothing and prints the refusal with the
floor and the remedy. With `--json`, the refusal is in `error.message` and
`error.kind` is `tmux_unsupported`.
- **Linux, including WSL, and macOS.** The library and the command share the
core [compatibility workflow][compat]: Ruby 3.3, 3.4 and 4.0 with tmux 3.3a
through 3.8 on Ubuntu 24.04, with per-revision results. A push and a pull
request run the oldest and newest of those releases, 3.3a and 3.8, and a
release runs all of them. macOS is supported on a best-effort basis, and CI
coverage for it is highly limited.
- **History limit.** On tmux 3.3a to 3.6 the reused initial pane keeps the
global [`history-limit`](https://libtmux.org/en/tmux/latest/catalog/options/history-limit/). Set the server's global value before you apply to
get one history size.
## Documentation
| | |
| --- | --- |
| [Reference](https://libtmux.org/en/ruby/latest/workspace/reference/) | Every public workspace class and method |
| [Command reference][ws-cli] | A page for [`validate`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/validate/), [`plan`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/plan/) and [`load`](https://libtmux.org/en/ruby/v0.1.1/workspace/cli/load/) |
| [Guides][workspace-guides] | Validate, inspect and explicitly apply a plan |
| [Topics][workspace-topics] | Format, planning boundaries, effects and failure ledgers |
| [Public behavior][behavior-workspace-parsing] | Parsing, plans, apply, results and the command |
| [Recipes][doc-recipes] | The workspace plan, load and failure program |
[behavior-client-switching]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#client-switching
[behavior-workspace-apply]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#workspace-apply
[behavior-workspace-parsing]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#workspace-parsing
[behavior-workspace-plans]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#workspace-plans
[behavior-workspace-results]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#workspace-results
[cli-load]: https://libtmux.org/en/ruby/latest/workspace/cli/load/
[cli-plan]: https://libtmux.org/en/ruby/latest/workspace/cli/plan/
[cli-validate]: https://libtmux.org/en/ruby/latest/workspace/cli/validate/
[compat]: https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml
[doc-recipes]: https://libtmux.org/en/ruby/v0.1.1/examples/recipes/
[workspace-applyerror]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-applyerror/
[workspace-applyerror-result]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-applyerror-result/
[workspace-applyresult-created_refs]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-applyresult-created_refs/
[workspace-applyresult-to_h]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-applyresult-to_h/
[workspace-configerror]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-configerror/
[workspace-guides]: https://libtmux.org/en/ruby/latest/workspace/guides/
[workspace-load]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-load/
[workspace-parse]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-parse/
[workspace-plan-apply]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-plan-apply/
[workspace-plan-class]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-plan-1qko/
[workspace-plan-method]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-plan-1c5c/
[workspace-plan-to_h]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-plan-to_h/
[workspace-to_h]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace-to_h/
[workspace-topics]: https://libtmux.org/en/ruby/latest/workspace/topics/
[ws-cli]: https://libtmux.org/en/ruby/latest/workspace/cli/
---
# Server bindings
Source: https://libtmux.org/en/ruby/v0.1.1/guides/core/
> Own a private server or connect to an existing socket.
Drive tmux from Ruby: [sessions][session], [windows][window] and [panes][pane]
as handles, [snapshots][snapshot] you filter in plain Ruby, and
[control connections][controlconnection] that report what tmux does.
[API reference](https://libtmux.org/en/ruby/latest/reference/) ·
[Guides](https://libtmux.org/en/ruby/latest/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux)
[](https://libtmux.org/en/ruby/)
[](https://rubygems.org/gems/libtmux)
Part of [libtmux for Ruby](https://github.com/libtmux/libtmux-ruby#readme).
This gem is the core; the [packages below](https://libtmux.org/en/ruby/v0.1.1/guides/core/#packages) build on it.
[Create][server-new_session] and [split][pane-split] windows,
[read][server-snapshot] and [filter][selection-where] what a server holds, and
[type into a pane][pane-send_text] and capture what it shows.
It suits scripts, tests that need a real tmux, and tools that drive a terminal
program. [Cancel][cancellation-cancel] a request that is waiting, and
[follow events][controlconnection-subscribe] as tmux emits them.
**Contents** — [Quick start](https://libtmux.org/en/ruby/v0.1.1/guides/core/#quick-start) ·
[Find things](https://libtmux.org/en/ruby/v0.1.1/guides/core/#find-sessions-windows-and-panes) ·
[Create](https://libtmux.org/en/ruby/v0.1.1/guides/core/#create-sessions-windows-and-panes) ·
[Type and read](https://libtmux.org/en/ruby/v0.1.1/guides/core/#type-into-a-pane-and-read-it-back) ·
[Your own server](https://libtmux.org/en/ruby/v0.1.1/guides/core/#use-the-tmux-server-you-already-run) ·
[Cancel](https://libtmux.org/en/ruby/v0.1.1/guides/core/#wait-time-out-and-cancel) ·
[Link windows](https://libtmux.org/en/ruby/v0.1.1/guides/core/#link-a-window-into-several-sessions) ·
[Options](https://libtmux.org/en/ruby/v0.1.1/guides/core/#read-and-set-options-and-hooks) ·
[Watch tmux](https://libtmux.org/en/ruby/v0.1.1/guides/core/#watch-tmux-change) ·
[Groups](https://libtmux.org/en/ruby/v0.1.1/guides/core/#run-several-commands-in-order) ·
[Packages](https://libtmux.org/en/ruby/v0.1.1/guides/core/#packages) · [Leaves out](https://libtmux.org/en/ruby/v0.1.1/guides/core/#what-it-leaves-out) ·
[Support](https://libtmux.org/en/ruby/v0.1.1/guides/core/#support-and-status) · [Documentation](https://libtmux.org/en/ruby/v0.1.1/guides/core/#documentation)
## Quick start
Install the gem. It needs Ruby 3.3 or newer and tmux 3.3a or newer on
`PATH`:
```console
$ gem install libtmux
```
This program creates a session, adds a window, splits it, and prints each
window with its panes. [`Server.start`][server-start] closes its private tmux
server when the block exits, and the snapshot stays readable afterward:
```ruby
require "libtmux"
snapshot = LibTmux::Server.start do |server|
session = server.new_session(name: "work", window_name: "main", command: ["/bin/cat"])
window = session.new_window(name: "logs", command: ["/bin/cat"])
window.split(direction: :horizontal, size: "40%", command: ["/bin/cat"])
server.snapshot
end
snapshot.windows.each do |window|
puts "#{window.name}: #{window.panes.map(&:id).join(', ')}"
end
```
It prints:
```text
main: %0
logs: %1, %2
```
Requiring the gem starts no tmux process, scheduler or MCP server.
[`Server.start`][server-start] gives its tmux server a socket and an empty
configuration of its own, so it never touches a tmux you already use.
## Find sessions, windows, and panes
### [`Server#list_sessions`][server-list_sessions]: what tmux has now
One call asks tmux for every session and returns frozen handles in numeric ID
order. [`list_windows`][server-list_windows] and
[`list_panes`][server-list_panes] are its siblings, and
[`Session#list_windows`][session-list_windows] and
[`Window#list_panes`][window-list_panes] narrow them:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
server.new_session(name: "work", command: ["/bin/cat"])
server.new_session(name: "logs", command: ["/bin/cat"])
sessions = server.list_sessions
puts "sessions: #{sessions.map(&:id).sort.join(', ')}"
end
```
It runs on a private server and prints:
```text
sessions: $0, $1
```
An empty array means tmux answered with nothing to list. A listing that fails
raises [`CommandError`][commanderror].
### [`Server#snapshot`][server-snapshot]: one read, many queries
Read the server once, then filter that read with
[`where`][selection-where], [`select`][selection-select],
[`one`][selection-one] and [`one_or_nil`][selection-one_or_nil]. These queries
make no tmux calls. Continue with the snapshot from the quick start:
```ruby
panes = snapshot.panes
active_ids = panes.where(active: true).map(&:id)
wide_panes = panes.select { |pane| pane.width >= 40 }
panes_by_window = panes.group_by { |pane| pane.window.name }
logs = snapshot.windows.one(name: "logs")
missing = snapshot.windows.one_or_nil(name: "missing")
```
[`one`]() raises [`NoMatchError`][nomatcherror] or
[`MultipleMatchesError`][multiplematcheserror] unless exactly one record
matches. [`one_or_nil`]() returns `nil` for no match and still rejects duplicates.
A selection keeps the membership it captured, so call [`server.snapshot`]() again
to read later changes. The [field catalog](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/fields.md)
lists every query field and wire name.
### [`Server#search_panes`][server-search_panes]: the matching panes in one call
`search_panes` captures the server and answers a selection of the panes whose
fields match `where:`. [`explain_panes`][server-explain_panes] shows the plan
without running tmux. This program also uses the snapshot queries above:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
work = server.new_session(name: "work", command: ["/bin/cat"], receipt: true)
server.new_session(name: "logs", command: ["/bin/cat"])
snapshot = server.snapshot
session = snapshot.sessions.where(name: "work").one
puts "matching session: #{session.name}"
missing = snapshot.sessions.one_or_nil(name: "missing")
puts "missing session: #{missing.nil?}"
panes = server.search_panes(where: {id: work.pane.id})
puts "matching pane: #{panes.one.id}"
end
```
It prints:
```text
matching session: work
missing session: true
matching pane: %0
```
## Create sessions, windows, and panes
### [`Server#new_session`][server-new_session]: a session and its first window
`new_session` starts a detached session that runs `command:`, an argv array.
Ask for a receipt to read the IDs tmux assigned to the session, window and
pane:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
created = server.new_session(
name: "work", window_name: "main", command: ["/bin/cat"], receipt: true
)
puts "session: #{created.entity.id}"
puts "window: #{created.window.id}"
puts "pane: #{created.pane.id}"
end
```
It prints:
```text
session: $0
window: @0
pane: %0
```
Creation returns once tmux has assigned the IDs. It says nothing about whether
the program started or exited cleanly.
### [`Session#new_window`][session-new_window]: add a window
[`Pane#split`][pane-split] and [`Window#split`][window-split] add panes the
same way. A new window takes a `name:` and the same `command:` as a session:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
session = server.new_session(name: "work", command: ["/bin/cat"])
window = session.new_window(name: "logs", command: ["/bin/cat"])
puts "window: #{window.snapshot.name}"
puts "session windows: #{session.list_windows.length}"
end
```
It prints:
```text
window: logs
session windows: 2
```
- `cwd:` names a directory on the machine running Ruby, and is checked before
the command is sent.
- `environment:` is a String-to-String map of literal values.
- [`new_window(index:)`]() refuses a slot that is taken.
- [`Pane#split`][pane-split] targets that exact pane, and `size:` is a cell
count or a percentage string such as `"40%"`.
- New windows and splits keep focus unless you pass `focus: true`.
The full contract is in [Creation][behavior-creation].
## Type into a pane and read it back
### [`Pane#send_text`][pane-send_text]: type, then read what the pane showed
[`send_text`]() types literal text, and [`send_keys`][pane-send_keys] sends tmux key
names such as `Enter`. [`Pane#capture`][pane-capture] reads the screen.
To wait for output, poll the pane until the line appears. A
[control connection][server-open_control] reports it as an event instead, but
needs tmux 3.7 or newer:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
created = server.new_session(
name: "capture", command: ["/bin/sh"],
environment: {"ENV" => "/dev/null"}, receipt: true
)
pane = created.pane
pane.send_text("printf '\\nlibtmux capture ready\\n'")
pane.send_keys("Enter")
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
captured = pane.capture.stdout.lines(chomp: true)
until captured.include?("libtmux capture ready")
raise "Output did not arrive within five seconds" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
sleep 0.05
captured = pane.capture.stdout.lines(chomp: true)
end
raise "Expected output is absent from the pane" unless captured.include?("libtmux capture ready")
puts "libtmux capture ready"
end
```
It prints:
```text
libtmux capture ready
```
- Sending input does not show that the shell finished the command.
- [`capture`]() returns the visible screen. `start:` and `finish:` choose a range,
and `join:` joins wrapped lines.
- `escapes:`, `escape_bytes:`, `preserve_trailing:` and `trim_trailing:` change
the bytes it returns.
- `mode_screen:`, `alternate:` and `pending:` read a copy-mode screen, the saved
screen and an unfinished escape sequence. Pass at most one.
- Binary buffers keep their trailing newlines. See
[`write_buffer`][server-write_buffer] and
[`read_buffer`][server-read_buffer] in the
[layout and bytes recipe](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/#layout-and-bytes).
The full contract is in [Pane input and capture][behavior-capture].
## Use the tmux server you already run
### [`Server.new`][server-new]: borrow a daemon
[`Server.open`][server-open] and [`Server.new`][server-new] bind an endpoint
that already exists. Closing the binding retires its clients and leaves the
daemon running; [`Server#kill`][server-kill] stops it.
The outer [`Server.start`][server-start] block here only supplies a daemon to
borrow:
```ruby
require "libtmux"
LibTmux::Server.start do |owner|
owner.new_session(name: "work", command: ["/bin/cat"])
server = LibTmux::Server.new(endpoint: owner.endpoint)
begin
puts "connected sessions: #{server.list_sessions.length}"
ensure
server.close
end
puts "sessions after client close: #{owner.list_sessions.length}"
end
```
It prints:
```text
connected sessions: 1
sessions after client close: 1
```
- [`Endpoint.new`][endpoint-new] takes a socket path or socket name. With
neither, it picks the socket tmux would: `LIBTMUX_SOCKET_PATH`,
`LIBTMUX_SOCKET`, the server `$TMUX` names, then tmux's default socket.
[`Endpoint.from_env`][endpoint-from_env] follows `$TMUX` alone.
- A binding keeps a private link to the socket it bound, so commands never
reach a different daemon that later takes the same path.
- A ref is valid on the server that issued it. Another server raises
[`TargetNotFoundError`][targetnotfounderror].
See [ownership and errors](https://libtmux.org/en/ruby/v0.1.1/topics/errors-and-exceptions/).
## Wait, time out, and cancel
### [`Cancellation.new`][cancellation-new]: wake a request that is waiting
Typed operations take `timeout:` and `cancel:`. Pass a
[`Cancellation`][cancellation] token as `cancel:`, call
[`cancel`][cancellation-cancel] from another thread to wake the request, join
its callers, then [`close`][cancellation-close] the token:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
server.new_session(name: "work", command: ["/bin/cat"])
cancellation = LibTmux::Cancellation.new
waiting = Thread.new do
server.run(["wait-for", "-S", "ready", ";", "wait-for", "held"], timeout: 5, cancel: cancellation)
rescue LibTmux::Cancelled => error
error
end
server.wait_for("ready", timeout: 5)
cancellation.cancel
failure = waiting.value
puts "request: #{failure.class}"
puts "delivery: #{failure.delivery}"
cancellation.close
end
```
It prints:
```text
request: LibTmux::Cancelled
delivery: possibly_sent
```
[`Cancelled`][cancelled] reports `delivery` as `:not_sent` or `:possibly_sent`.
Cancelling does not undo what tmux already queued.
The [plain-Ruby recipe](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/examples/cancel.rb) also checks that the client
was reaped. The contracts are in
[Cancellation tokens][behavior-cancellation] and
[ownership and errors](https://libtmux.org/en/ruby/v0.1.1/topics/errors-and-exceptions/).
## Link a window into several sessions
### [`Session#link_window`][session-link_window]: one window at several indexes
A [`WindowLink`][windowlink] keeps a session, an index and a window ID
together, so a window linked twice stays one window. [`link_window`]() needs an
explicit `index:`:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
session = server.new_session(name: "links", command: ["/bin/cat"])
window = session.list_windows.fetch(0)
session.link_window(window.ref, index: 4)
session.link_window(window.ref, index: 9)
links = session.list_window_links
puts "indexes: #{links.map(&:index).join(', ')}"
puts "windows: #{links.map { |link| link.window.id }.uniq.join(', ')}"
end
```
It prints:
```text
indexes: 0, 4, 9
windows: @0
```
- Select, unlink, move, swap and display check the whole link identity in the
same tmux queue turn as the operation.
- A hook that waits before dispatch cannot turn a stale link into its
replacement.
- Rewriting command aliases during an operation is outside that guarantee, so
coordinate configuration changes.
The contract is in [Window links][behavior-window-links], and the
[linked-windows recipe](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/#one-window-at-three-indexes)
runs this at three indexes.
## Read and set options and hooks
### [`Session#options`][session-options]: a value with its inheritance
[`Options#set`][options-set] writes an option at the scope of its accessor.
[`Options#get`][options-get] reads it back as an
[`OptionValue`][optionvalue], and [`as`][optionvalue-as] converts it strictly.
[`Hooks#set`][hooks-set] stores a tmux command:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
session = server.new_session(name: "work", command: ["/bin/cat"])
session.options.set("history-limit", 5000)
limit = session.options.get("history-limit")
puts "history-limit: #{limit.as(:integer)}"
puts "inherited: #{limit.inherited?}"
session.hooks.set("after-new-window", command: "display-message created")
puts "hook: #{session.hooks.get('after-new-window').as(:string)}"
end
```
It prints:
```text
history-limit: 5000
inherited: false
hook: display-message created
```
- An [`OptionValue`][optionvalue] keeps the raw bytes, whether the value is
inherited, and the array index. [`as`]() takes `:bytes`, `:string`, `:integer` or
`:boolean`.
- A missing array index raises [`NoMatchError`][nomatcherror], and a present
empty value stays present.
- Reading an array without an index raises
[`MultipleMatchesError`][multiplematcheserror] when it has several entries.
- Hook values stay tmux command strings, and indexed hooks run in index order.
The contracts are in [Options and hooks][behavior-options] and
[Option values][behavior-option-values].
## Watch tmux change
### [`Server#open_control`][server-open_control]: events as they happen
A [`ControlConnection`][controlconnection] runs a command with
[`exchange`][controlconnection-exchange] and streams what tmux reports through
[`subscribe`][controlconnection-subscribe]. The subscription's
[`next`][controlsubscription-next] waits for the next
[`ControlEvent`][controlevent]:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
session = server.new_session(name: "work", command: ["/bin/cat"])
window = session.list_windows.fetch(0)
server.open_control(session: session.ref) do |control|
reply = control.exchange("display-message -p ready", timeout: 5)
puts "reply: #{reply.blocks.last.body.strip}"
events = control.subscribe(max_events: 64, max_bytes: 65_536)
window.rename("renamed")
event = events.next(timeout: 5) until event&.raw&.start_with?("%window-renamed")
puts "event: #{event.raw.strip}"
end
end
```
It prints:
```text
reply: ready
event: %window-renamed @0 renamed
```
- `exchange` answers a [`GuardedReply`][guardedreply]. It holds the blocks seen
between two boundaries, and a hook can print into the same interval.
- A subscription is bounded. A reliable one raises
[`SubscriptionOverflow`][subscriptionoverflow] when it falls behind, and a
tail one reports the lost range as a `:gap` event.
- [`pause_output`][controlconnection-pause_output] and
[`resume_output`][controlconnection-resume_output] return a
[`GuardedReply`][guardedreply]. A reply does not show that the action took
effect.
- To reconnect, close the old connection and pass it as
`open_control(session: ref, reconnect: old_connection)`. The replacement has
a new `generation`, and every new subscription starts with a `:reconnect`
gap. Nothing is replayed.
The contracts are in [Control connections][behavior-control], and the
[control overflow recipe](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/#control-overflow) runs both
subscription modes.
## Run several commands in order
### [`Server#run_group`][server-run_group]: several commands through one client
A group submits several tmux commands in order through one client. The
[`GroupResult`][groupresult] holds one aggregate status, and each step reports
`unknown`:
```ruby
require "libtmux"
LibTmux::Server.start do |server|
server.new_session(name: "work", command: ["/bin/cat"])
group = server.run_group([
["set-option", "-g", "@before", "kept"],
["select-pane", "-t", "%4294967294"],
["set-option", "-g", "@after", "never run"]
])
puts "success: #{group.success?}"
puts "steps: #{group.steps.map { |step| step.fetch(:outcome) }.join(', ')}"
puts "@before: #{server.options(scope: :session).get('@before').as(:string)}"
end
```
It prints:
```text
success: false
steps: unknown, unknown, unknown
@before: kept
```
The first command keeps its effect when the second fails, and the third never
runs. The [group recipe](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/#failure-inside-a-group) checks
the same.
## Packages
Start with `libtmux`. Add the companion for your caller:
| Gem | Require | Use it for |
| --- | --- | --- |
| [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux) | `libtmux` | Blocking scripts, snapshots, and control connections |
| [libtmux-async](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-async) | `libtmux/async` | Concurrent commands and bounded streams in Async tasks |
| [libtmux-mcp](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-mcp) | `libtmux/mcp` | An MCP server with explicit endpoints and tool policy |
| [libtmux-workspace](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-workspace) | `libtmux/workspace` | YAML/JSON workspace plans and a CLI to apply them |
### [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux): the core
This gem: blocking [server][server], [session][session], [window][window] and
[pane][pane] handles, immutable [snapshots][snapshot], and
[control connections][controlconnection]. The sections above run against it.
[API reference](https://libtmux.org/en/ruby/latest/reference/) ·
[Guides](https://libtmux.org/en/ruby/latest/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux)
### [libtmux-async](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-async): concurrent commands
Concurrent commands and bounded control streams inside an Async task that your
application owns. Require `libtmux/async`, then open a scope with
[`LibTmux::Async.open`][async-open].
[API reference](https://libtmux.org/en/ruby/latest/reference/libtmux-async/) ·
[Guide](https://libtmux.org/en/ruby/latest/guides/async/) ·
[RubyGems](https://rubygems.org/gems/libtmux-async)
```console
$ gem install libtmux-async
```
Capture every pane concurrently with [`Scope#map`][async-scope-map]. `server`
is an open [`LibTmux::Server`][server]:
```ruby
Async do |parent|
LibTmux::Async.open(parent: parent, server: server) do |scope|
refs = scope.server.list_panes.map(&:ref)
screens = scope.map(refs, concurrency: 2) { |ref| scope.server.pane(ref).capture }
Example.check(screens.length == refs.length && screens.all?(&:success?), "concurrent captures failed")
end
end.wait
```
### [libtmux-mcp](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-mcp): a tmux server for MCP clients
An MCP server over stdio for one tmux server, with an explicit tool policy.
Installs `libtmux-mcp`; require `libtmux/mcp` to embed it through
[`LibTmux::MCP::Application`][mcp-application].
[API reference](https://libtmux.org/en/ruby/latest/mcp/reference/) ·
[Tools](https://libtmux.org/en/ruby/latest/mcp/tools/) ·
[Guides](https://libtmux.org/en/ruby/latest/mcp/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux-mcp)
```console
$ gem install libtmux-mcp
```
The server borrows a tmux server that is already running. Start one on a
socket of its own:
```console
$ tmux -L agent new-session -d -s work
```
Add the server to Claude Code. Until you enable more tools, it serves
[`tmux_capabilities`][tool-tmux_capabilities] and
[`tmux_snapshot`][tool-tmux_snapshot]:
```console
$ claude mcp add tmux -- libtmux-mcp --socket-name agent --endpoint dev
```
The [MCP README](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/) covers other clients, every tool and
authored shell commands.
### [libtmux-workspace](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-workspace): YAML workspaces
YAML or JSON workspaces, validated, planned offline and applied to an open
server. Installs `libtmux-workspace`; require `libtmux/workspace` to use it
from Ruby through [`LibTmux::Workspace`][workspace].
[API reference](https://libtmux.org/en/ruby/latest/workspace/reference/) ·
[Command reference](https://libtmux.org/en/ruby/latest/workspace/cli/) ·
[Guides](https://libtmux.org/en/ruby/latest/workspace/guides/) ·
[RubyGems](https://rubygems.org/gems/libtmux-workspace)
```console
$ gem install libtmux-workspace
```
Save this workspace as `workspace.yaml`:
```yaml
session_name: work
environment:
PROJECT_MODE: development
windows:
- window_name: editor
window_index: 1
layout: tiled
panes:
- shell_command: printf 'editor ready\n'
- {}
```
Check it without contacting tmux:
```console
$ libtmux-workspace validate workspace.yaml
```
```text
Workspace configuration is valid.
```
The [workspace README](https://libtmux.org/en/ruby/v0.1.1/workspace/source-guide/) loads it into a running
server and lists every option.
## What it leaves out
- **Live objects.** A [snapshot record][snapshot] is what tmux said when you
read it, and a [handle][server-list_sessions] reads tmux again each time you
list. See [Captured records][behavior-records] and
[Live handles][behavior-live-handles].
- **A terminal emulator.** [`Pane#capture`][pane-capture] returns the bytes
tmux holds for the screen, with the flags above to shape them.
- **A server it starts on its own.** [`Server.new`][server-new] and
[`Server.open`][server-open] bind a running server and never start one. With
no socket named, [`Endpoint.new`][endpoint-new] picks the one tmux would:
`LIBTMUX_SOCKET_PATH`, `LIBTMUX_SOCKET`, the server `TMUX` names, then tmux's
default socket. The recipes name a socket or own one.
- **Replay.** A reconnected control connection reports a gap and never replays
requests or missed output. See [Control flow][behavior-control-flow].
- **Transactions.** A [group][server-run_group] runs in order and keeps the
effects of the steps before a failure.
- **Whole-program typing.** RBS declarations ship with the gem and selected
installed calls are checked. See [Signature proof][behavior-signature].
## Support and status
Releases are numbered `0.1.N`, and each release increments `N`. The API is not
settled: any release may change or remove exported identifiers, and the
changelog marks each break. Pin an exact version to avoid one.
See the [changelog](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/CHANGELOG.md).
- **Ruby 3.3 or newer.** The gem depends on `json`, `tmpdir`, `securerandom`,
`fiddle` and `io-console`.
- **tmux 3.3a or newer.** Unpatched 3.3 is below the minimum. A
[server handle][server] checks the connected daemon's release before it sends
a command, and refuses an older daemon even when its client is newer.
- **Control connections need tmux 3.7 or newer.** An older daemon refuses
[`open_control`][server-open_control] before a control client attaches.
Subprocess operations and snapshots do not need it. The
[optional feature table](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#optional-tmux-features)
lists the other per-release requirements.
- **Linux, including WSL, and macOS.** The [compatibility workflow][compat]
runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04. A push
and a pull request run the oldest and newest of those releases, 3.3a and 3.8,
and a release runs all of them. macOS is supported on a best-effort basis, and CI coverage for it is highly
limited.
- **tmux master and old daemons.** On Ubuntu a release also runs the suite
against a pinned tmux master, and checks that tmux 3.0a, 3.1c, 3.2a and 3.3
daemons are refused.
- **Per-revision results.** Read the workflow's results for your revision
before relying on one combination.
## Documentation
| | |
| --- | --- |
| [Reference](https://libtmux.org/en/ruby/latest/reference/) | Every public class and method, one page each |
| [Guides](https://libtmux.org/en/ruby/latest/guides/) | Getting started, server bindings and the Async scope |
| [Recipes](https://libtmux.org/en/ruby/v0.1.1/examples/recipes/) | Complete programs with checked excerpts |
| [Execution modes](https://libtmux.org/en/ruby/v0.1.1/concepts/transports/) | Blocking calls, Async tasks and control subscriptions |
| [Ownership and errors](https://libtmux.org/en/ruby/v0.1.1/topics/errors-and-exceptions/) | What close and failure mean |
| [Public behavior](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md) | Defaults, ownership, blocking and failure of each call |
| [Field catalog](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/fields.md) | Query fields and wire names |
| [Public methods](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/api.md) | Each exported method, its source and its contract |
| [Contributing](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/.github/CONTRIBUTING.md) | Local builds, checks and the test loops |
MIT. See [LICENSE](https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/gems/libtmux/LICENSE).
[async-open]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-open/
[async-scope-map]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-scope-map/
[behavior-cancellation]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#cancellation-tokens
[behavior-capture]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#pane-input-and-capture
[behavior-control]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#control-connections
[behavior-control-flow]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#control-flow
[behavior-creation]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#creation
[behavior-live-handles]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#live-handles
[behavior-option-values]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#option-values
[behavior-options]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#options-and-hooks
[behavior-records]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#captured-records
[behavior-signature]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#signature-proof
[behavior-window-links]: https://github.com/libtmux/libtmux-ruby/blob/da7b6191b9439e3c84fa1c9eeaa9168263ee7af4/docs/reference/behavior.md#window-links
[cancellation]: https://libtmux.org/en/ruby/latest/reference/libtmux-cancellation/
[cancellation-cancel]: https://libtmux.org/en/ruby/latest/reference/libtmux-cancellation-cancel/
[cancellation-close]: https://libtmux.org/en/ruby/latest/reference/libtmux-cancellation-close/
[cancellation-new]: https://libtmux.org/en/ruby/latest/reference/libtmux-cancellation-new/
[cancelled]: https://libtmux.org/en/ruby/latest/reference/libtmux-cancelled/
[commanderror]: https://libtmux.org/en/ruby/latest/reference/libtmux-commanderror/
[compat]: https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml
[controlconnection]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlconnection/
[controlconnection-exchange]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlconnection-exchange/
[controlconnection-pause_output]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlconnection-pause_output/
[controlconnection-resume_output]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlconnection-resume_output/
[controlconnection-subscribe]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlconnection-subscribe/
[controlevent]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlevent/
[controlsubscription-next]: https://libtmux.org/en/ruby/latest/reference/libtmux-controlsubscription-next/
[endpoint-from_env]: https://libtmux.org/en/ruby/latest/reference/libtmux-endpoint-from_env/
[endpoint-new]: https://libtmux.org/en/ruby/latest/reference/libtmux-endpoint-new/
[groupresult]: https://libtmux.org/en/ruby/latest/reference/libtmux-groupresult/
[guardedreply]: https://libtmux.org/en/ruby/latest/reference/libtmux-guardedreply/
[hooks-set]: https://libtmux.org/en/ruby/latest/reference/libtmux-hooks-set/
[mcp-application]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application/
[multiplematcheserror]: https://libtmux.org/en/ruby/latest/reference/libtmux-multiplematcheserror/
[nomatcherror]: https://libtmux.org/en/ruby/latest/reference/libtmux-nomatcherror/
[options-get]: https://libtmux.org/en/ruby/latest/reference/libtmux-options-get/
[options-set]: https://libtmux.org/en/ruby/latest/reference/libtmux-options-set/
[optionvalue]: https://libtmux.org/en/ruby/latest/reference/libtmux-optionvalue/
[optionvalue-as]: https://libtmux.org/en/ruby/latest/reference/libtmux-optionvalue-as/
[pane]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-14z6/
[pane-capture]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-capture/
[pane-send_keys]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-send_keys/
[pane-send_text]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-send_text/
[pane-split]: https://libtmux.org/en/ruby/latest/reference/libtmux-pane-split/
[selection-one]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-one/
[selection-one_or_nil]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-one_or_nil/
[selection-select]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-select/
[selection-where]: https://libtmux.org/en/ruby/latest/reference/libtmux-selection-where/
[server]: https://libtmux.org/en/ruby/latest/reference/libtmux-server/
[server-explain_panes]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-explain_panes/
[server-kill]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-kill/
[server-list_panes]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-list_panes/
[server-list_sessions]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-list_sessions/
[server-list_windows]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-list_windows/
[server-new]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-new/
[server-new_session]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-new_session/
[server-open]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-open/
[server-open_control]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-open_control/
[server-read_buffer]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-read_buffer/
[server-run_group]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-run_group/
[server-search_panes]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-search_panes/
[server-snapshot]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-snapshot/
[server-start]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-start/
[server-write_buffer]: https://libtmux.org/en/ruby/latest/reference/libtmux-server-write_buffer/
[session]: https://libtmux.org/en/ruby/latest/reference/libtmux-session-nvps/
[session-link_window]: https://libtmux.org/en/ruby/latest/reference/libtmux-session-link_window/
[session-list_windows]: https://libtmux.org/en/ruby/latest/reference/libtmux-session-list_windows/
[session-new_window]: https://libtmux.org/en/ruby/latest/reference/libtmux-session-new_window/
[session-options]: https://libtmux.org/en/ruby/latest/reference/libtmux-session-options/
[snapshot]: https://libtmux.org/en/ruby/latest/reference/libtmux-snapshot/
[subscriptionoverflow]: https://libtmux.org/en/ruby/latest/reference/libtmux-subscriptionoverflow/
[targetnotfounderror]: https://libtmux.org/en/ruby/latest/reference/libtmux-targetnotfounderror/
[tool-tmux_capabilities]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capabilities/
[tool-tmux_snapshot]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_snapshot/
[window]: https://libtmux.org/en/ruby/latest/reference/libtmux-window-1vfx/
[window-list_panes]: https://libtmux.org/en/ruby/latest/reference/libtmux-window-list_panes/
[window-split]: https://libtmux.org/en/ruby/latest/reference/libtmux-window-split/
[windowlink]: https://libtmux.org/en/ruby/latest/reference/libtmux-windowlink-tusj/
[workspace]: https://libtmux.org/en/ruby/latest/workspace/reference/libtmux-workspace/
---
# Connect a Ruby MCP client
Source: https://libtmux.org/en/ruby/v0.1.1/mcp/guides/connect-client/
> Launch a pinned Ruby MCP server on its own private tmux socket, inspect the default tools, and close it with the client.
**MCP concept example** A working example of the core library. It can change without notice.
Let your MCP client launch the Ruby server on a private tmux socket. The
launcher below creates one `mcp-example` session, offers capability discovery
and metadata snapshots, and stops its owned tmux daemon when the client closes
the connection.
## Prepare the project
Use Ruby 4.0.7 with its development headers, Bundler, Git, a C compiler and
Make, and tmux 3.3a or newer on a Unix host. The bundle includes native
extensions such as `io-event`, `json`, and `fiddle`. This recipe uses the
source revision whose MCP contract is documented here; it does not
assume that a newer checkout and an installed release have identical APIs.
Create an empty directory:
```console
$ mkdir ruby-mcp-client && cd ruby-mcp-client
```
Fetch the source:
```console
$ git clone https://github.com/libtmux/libtmux-ruby libtmux-source
```
Select the documented revision:
```console
$ git -C libtmux-source checkout 9b1545562a112353c2c893a1d3e8c0d9b4b51f8d
```
Install the dependencies from its lockfile:
```console
$ BUNDLE_FROZEN=true BUNDLE_GEMFILE=./libtmux-source/Gemfile bundle install
```
This installs the source checkout's core and MCP gems together. Keep the
checkout beside the launcher so the client's working directory does not affect
which gems it loads.
## Save the launcher
Save this complete program as `run-mcp.rb` in the project directory:
```ruby title="run-mcp.rb"
# frozen_string_literal: true
ENV["BUNDLE_GEMFILE"] = File.expand_path("libtmux-source/Gemfile", __dir__)
require "bundler/setup"
require "libtmux/mcp/cli"
def describe_failure(error)
details = ["#{error.class}: #{error.message}"]
if error.is_a?(LibTmux::Error)
details.concat(error.cleanup_errors.map { |message| "Cleanup: #{message}" })
end
details.join("\n")
end
server = nil
failures = []
begin
server = LibTmux::Server.start(timeout: 5.0)
server.new_session(name: "mcp-example", command: ["/bin/cat"], timeout: 5.0)
status = LibTmux::MCP::CLI.run([
"--socket", server.endpoint.socket_path,
"--endpoint", "docs",
"--timeout", "5"
])
failures << "MCP stopped with status #{status}" unless status.zero?
rescue StandardError => error
failures << describe_failure(error)
ensure
begin
server&.close
rescue StandardError => error
failures << "Cleanup failed: #{describe_failure(error)}"
end
end
abort failures.join("\n") unless failures.empty?
```
`Server.start` owns a new socket directory and foreground daemon with an empty
tmux configuration. It starts no session by itself; `new_session` creates the
example's pane running `cat`. Closing this owned server stops its daemon.
The MCP command borrows that endpoint and closes its own clients when stdin
ends. The outer launcher then closes the daemon it created.
The five-second startup deadline, five-second session-creation deadline, and
five-second MCP request deadline govern separate operations. The server keeps
serving while the client remains connected. Diagnostics go to stderr; stdout
contains MCP messages only. Operation and cleanup failures are both reported.
Run it directly to check startup:
```console
$ ruby -W:no-experimental run-mcp.rb
```
It waits for a client. Send EOF to close it. The Ruby option suppresses the
runtime's experimental [`IO::Buffer`](https://docs.ruby-lang.org/en/4.0/IO/Buffer.html) warning; it does not suppress exceptions.
## Configure a client
For a client that accepts an `mcpServers` configuration, generate the entry
using this project's absolute launcher path and the current Ruby executable:
```console
$ ruby -rjson -rrbconfig -e '
puts JSON.pretty_generate(
"mcpServers" => {
"tmux-ruby" => {
"command" => RbConfig.ruby,
"args" => ["-W:no-experimental", File.expand_path("run-mcp.rb")]
}
}
)
'
```
Add that entry to the client's existing configuration and reconnect. Other
clients ask for a command and argument list separately; use the same values.
The client needs tmux available on its process `PATH`. If its environment
cannot find the intended executable, give the launcher an absolute `executable:`
argument in `Server.start` and pass that same path with the MCP CLI's `--tmux`
option.
## Verify the connection
List the tools. The default catalog contains exactly [`tmux_capabilities`]() and
[`tmux_snapshot`](). Capture, wait, create, send, close, and authored-run tools are
absent until explicitly enabled.
Call [`tmux_capabilities`]() with an empty argument object. Its successful
`structuredContent` has `ok: true`; `data.endpoint` is `docs`. That alias labels
discovery and resource URIs. The `--socket` argument selects the actual daemon.
Call [`tmux_snapshot`]() with this argument object:
```json
{"entity": "session"}
```
The successful result has one item in [`structuredContent.data.items`](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/tmux_snapshot/). Its
`fields.name` is `mcp-example`, and its `ref` includes the session identity and
server generation. Check `ok` before reading successful data; a JSON-RPC
response alone does not establish that the tool succeeded.
Close the MCP connection. The launcher exits, stops its private daemon, and
removes its owned socket directory. Reconnecting runs the launcher again and
creates a fresh session, so do not reuse references from the previous server.
## Connect to a server your application owns
When the application already owns a tmux daemon, use the installed
`libtmux-mcp` executable with one explicit `--socket PATH` or `--socket-name NAME`
selector. It borrows the daemon: EOF closes the MCP clients without stopping
that daemon. The application's lifecycle remains responsible for it.
[Tool reference](https://libtmux.org/en/ruby/v0.1.1/mcp/tools/) describes the argument and result schemas.
[Source-owned MCP guide](https://libtmux.org/en/ruby/v0.1.1/mcp/source-guide/) explains policy, observation and
shell enrollment. The pinned [CLI implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux-mcp/lib/libtmux/mcp/cli.rb)
and [owned-server implementation](https://github.com/libtmux/libtmux-ruby/blob/9b1545562a112353c2c893a1d3e8c0d9b4b51f8d/gems/libtmux/lib/libtmux/owned.rb)
define these lifecycle boundaries.
---
# Linking and consuming our docs
Source: https://libtmux.org/en/ruby/v0.1.1/meta/linking/
> Find documentation exports for search engines, bots, LLMs and other documentation sites.
We welcome bots, search engines, SEO tools, LLMs and other clients to use our
documentation. Read a page in your browser, fetch its Markdown, discover
pages through a manifest, or link to documented objects from another site.
## Choose an export
The page footer links to the exports for its documentation tree. Use those
links to select a language, version or preview.
| File or format | Use it for |
| --- | --- |
| [llms.txt](https://libtmux.org/en/llms.txt) | Discover guides, API references and agent prompts through a Markdown index. |
| [llms-full.txt](https://libtmux.org/en/llms-full.txt) | Fetch guides and MCP contracts together as Markdown. API references are linked separately. |
| [docs.json](https://libtmux.org/en/docs.json) | Discover page titles, URLs, descriptions, headings and Markdown URLs in a structured manifest. |
| Markdown | Read one page as text using its footer's **Markdown** link. |
| Raw source | Inspect the source file using its footer's **Raw source** link, where available. |
| [objects-full.inv](https://libtmux.org/en/objects-full.inv) | Resolve Sphinx references across shared docs, each port's default version, or its latest until the default has published a catalog, and tmux reference versions. |
| objects.inv | Resolve Sphinx references within one port and version, or one tmux documentation tree. |
| [targets.json](https://libtmux.org/en/targets.json) | Browse inventory keys, display names and destination URLs as JSON. A scoped catalog sits beside each root inventory. The root one lists fallbacks and ports left out. |
The root exports above cover the assembled site. Port exports describe the
selected port and version. For a single API declaration, start with
`docs.json` and follow its reference and symbol-index links.
## Link from Sphinx
The [Intersphinx guide](https://libtmux.org/en/meta/intersphinx/) explains how to configure
inventories, choose native roles or portable labels, and preserve references
when a page moves or changes its title.
Browse [Intersphinx examples](https://libtmux.org/en/meta/intersphinx/examples/) for copyable RST
and MyST references to library APIs, tmux C declarations, hooks, format
variables, CLI commands and schema fields, paired with their destinations.
## Discover and index pages
Use the [sitemap](https://libtmux.org/en/sitemap-index.xml) to discover indexable pages. Follow
[robots.txt](https://libtmux.org/robots.txt) and each page's robots metadata:
pull-request previews, component demos and search-index shards are excluded
from crawling, and alternate versions can carry `noindex`.
Choose a versioned documentation tree when a reference needs a particular
release. The full inventory follows each port's advertised default; a
port's `objects.inv` describes the selected version.
---
# Link to libtmux from Sphinx
Source: https://libtmux.org/en/ruby/v0.1.1/meta/intersphinx/
> Use Intersphinx inventories to reference libtmux APIs and documentation.
Sphinx can resolve references to libtmux APIs, guides, headings, MCP objects,
schema fields, and other documented targets. The footer links to the full
inventory and the inventory for the page's port or tmux version.
Browse [Intersphinx examples](https://libtmux.org/en/meta/intersphinx/examples/) for copyable RST
and MyST references paired with their destinations.
For Markdown exports, manifests and crawler discovery, see
[Linking and consuming our docs](https://libtmux.org/en/meta/linking/).
## Configure Intersphinx
Enable the built-in extension in your project's `conf.py`:
`None` discovers `objects.inv` at the configured base URL. The port mapping
above selects the Python `latest` tree. Copy the base URL of another port
or version to select its inventory.
The full inventory includes shared documentation, the configured default
version of each assembled port, and distinct tmux reference versions.
It is generated after site assembly. Each port inventory includes the
rendered targets in that exact version.
The tmux inventory includes the shared tmux guides and the latest CLI manual,
options, hooks, formats, and C source reference. A version mapping such as
`tmux-3-7c` includes only that release's manual, catalog, and C declarations.
Manual and C reference pages link to the same version inventory in the footer.
Use mapping names without dots when writing MyST reference roles.
## Choose a reference
Every exported target has a portable label that works with `std:ref`, without
installing a language or MCP domain. Open `targets.json` beside the inventory
and copy its `key`. Its `href` and `display` identify the destination.
For example, the Python [`libtmux.Pane`]() declaration has this reStructuredText
reference:
```rst
:external+libtmux:ref:`Pane `
```
With MyST enabled in your project, the same label works in Markdown:
```markdown
{external+libtmux:ref}`Pane `
```
Native Python, C++, JavaScript, and C objects also have aliases for their
built-in roles. A scoped Python inventory supports:
```rst
:external+libtmux-python:py:class:`libtmux.Pane`
```
The tmux mapping provides native C roles and portable manual references:
```rst
:external+tmux:c:struct:`cmd`
:external+tmux:ref:`capture-pane `
```
Command options also work with Sphinx's program context:
```rst
.. program:: tmuxp load
:external+libtmux-python:option:`--append`
```
The full inventory qualifies native names by port, such as
[`py:libtmux.Pane`](). A fully qualified alias includes the product too:
[`py:core:libtmux.Pane`](). Short aliases are offered only when they identify
one destination. Configuration values, command options, environment
variables, and glossary terms use standard-domain types where their
renderers identify those semantics.
## Stable labels
A portable key contains `scope:product:category:identity`. Its encoding
preserves uppercase letters, punctuation, and Unicode, because `std:ref`
lowercases targets. `%50ane` and `pane` therefore remain distinct.
Copy the key from the catalog without decoding its percent escapes.
Keys are independent of display titles and destination URLs. API public IDs,
protocol wire identities, JSON Pointers, named parameters, and native method
identities supply their names. Authored documents can retain `inventoryId`
and explicit HTML fragment IDs when their titles or routes change.
Contributors preserve renamed keys in the inventory alias catalog and use
the existing page redirect mechanism for retired routes.
### Preserve published links
If `meta/intersphinx.md` moves to `meta/linking.md`, retain its logical ID
and list the retired route in the new page's frontmatter:
```yaml
inventoryId: intersphinx
aliases:
- meta/intersphinx
```
The document key remains `site:core:doc:intersphinx`, and the old route
redirects to the new page. Retain existing heading fragment IDs when their
text changes.
When an identity or fragment changes intentionally, map the former keys to
current targets in `site/src/data/inventory-aliases.json`. For example,
choosing `inventoryId: linking` and changing `stable-labels` to
`stable-references` needs these entries:
```json
[
{
"scope": "site",
"from": "site:core:doc:intersphinx",
"to": "site:core:doc:linking"
},
{
"scope": "site",
"from": "site:core:heading:intersphinx%23stable-labels",
"to": "site:core:heading:linking%23stable-references",
"fragment": "stable-labels"
}
]
```
The document entry preserves `std:ref` and `std:doc` references. The heading
entry preserves `std:ref`; `fragment` also installs the retired HTML anchor
at the new destination. Keep the route alias too: inventory aliases do not
redirect retired URLs.
Copy `to` from the current target catalog. Retain earlier `from` entries
and update their destinations when renaming again. Former native role names,
such as a Python class name, need preservation in the native metadata.
Existing `objects.inv` and `reference/objects.inv` endpoints remain compatibility
exports. New integrations should use the full inventory, a port's root
inventory, or a tmux root inventory to obtain the complete target catalog
and case-safe labels.
See the [Sphinx Intersphinx documentation](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html)
for named inventories and cross-reference syntax.
---
# Intersphinx examples
Source: https://libtmux.org/en/ruby/v0.1.1/meta/intersphinx/examples/
> Compare Sphinx and MyST references to APIs, tmux C declarations, hooks, format variables, CLI commands and schema fields.
import { referenceHref } from '../../../../lib/api-models'
Copy a reference and compare its destination below. These examples cover
native language roles, portable labels, full and scoped inventories, and
links into tmux C declarations, hooks, format variables and CLI commands.
## Configure the inventories
Add these mappings to your Sphinx project's `conf.py`:
`None` discovers `objects.inv` at the mapping's base URL. The full inventory
uses each port's advertised default version. The `py` mapping explicitly
selects `latest`; `tmux` selects the latest tmux manual and C reference.
Use the root URL from an inventory's footer link when selecting a preview
or another release.
## Native Python roles
A named inventory selects the Python tree:
```rst
:external+py:py:class:`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
The full inventory qualifies native names by port:
```rst
:external+full:py:class:`py:libtmux.Pane`
```
Destination: Pane in the Python default version.
For automatic lookup, use the ordinary role. A local definition takes
precedence over Intersphinx:
```rst
:py:class:`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
## Portable labels
Portable labels work with the standard reference role, including languages
that have no built-in Sphinx domain. Copy the key from `targets.json`.
```rst
:external+full:ref:`Pane `
:external+py:ref:`Pane `
```
Destinations: Full inventory: Pane,
[Scoped inventory: Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
The external role searches the configured inventories without selecting one.
This native name is unqualified only in the scoped Python inventory:
```rst
:external:py:class:`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
## MyST roles and inventory URLs
Enable `myst_parser` alongside `sphinx.ext.intersphinx` for Markdown sources.
Named roles accept the same targets as reStructuredText:
```markdown
{external+py:py:class}`libtmux.Pane`
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
Portable labels retain their percent escapes inside reference roles:
```markdown
{external+full:ref}`Pane `
```
Destination: Pane in the Python default version.
MyST's inventory URL syntax also supports native aliases:
```markdown
[Pane](inv:py:py:class#libtmux.Pane)
```
Destination: [Pane](https://libtmux.org/en/py/latest/reference/libtmux-pane/#libtmux.Pane).
Use role syntax for percent-encoded portable keys: MyST normalizes escapes
in `inv:` URLs. Keep mapping names free of dots, as in `tmux-3-7c`, so MyST
can parse the role name. When using an inventory URL, specify its domain and
object type if a name is both a document and a label.
## tmux C declarations
The tmux mapping provides Sphinx's built-in C roles:
```rst
:external+tmux:c:struct:`cmd`
:external+tmux:c:func:`cmd_find_target`
```
Destinations: [cmd](https://libtmux.org/en/tmux/latest/reference/c-struct-cmd/#c%3Astruct%3Acmd),
[cmd_find_target](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target).
Select a release with its own mapping. The full inventory also offers
version-qualified native aliases:
```rst
:external+tmux-3-7c:c:func:`cmd_find_target`
:external+full:c:func:`tmux@3.7c:cmd_find_target`
:external+full:c:func:`tmux@latest:cmd_find_target`
```
Destinations: [Scoped: cmd_find_target in 3.7c](https://libtmux.org/en/tmux/3.7c/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[Full: cmd_find_target in 3.7c](https://libtmux.org/en/tmux/3.7c/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[Full: cmd_find_target in latest](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target).
MyST supports the same C roles and a typed inventory URL:
```markdown
{external+tmux:c:struct}`cmd`
{external+tmux:c:func}`cmd_find_target`
{external+full:c:func}`tmux@3.7c:cmd_find_target`
[cmd_find_target](inv:tmux:c:function#cmd_find_target)
```
Destinations: [MyST: cmd](https://libtmux.org/en/tmux/latest/reference/c-struct-cmd/#c%3Astruct%3Acmd),
[MyST: cmd_find_target](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[MyST full: cmd_find_target in 3.7c](https://libtmux.org/en/tmux/3.7c/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target),
[Inventory URL: cmd_find_target](https://libtmux.org/en/tmux/latest/reference/c-function-cmd_find_target/#c%3Afunction%3Acmd_find_target).
The C reference links each declaration to its source file and revision.
Portable labels also address its signatures, parameters, and sections.
## tmux hooks
Link a hook name to its catalog page with a portable label. The same key
works in the scoped tmux inventory and the full inventory:
```rst
:external+tmux:ref:`after-new-session `
:external+full:ref:`after-new-session `
```
Destinations: [Scoped: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/),
[Full: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/).
In MyST, use the same key inside a reference role. Preserve its percent
escapes:
```markdown
{external+tmux:ref}`after-new-session `
{external+full:ref}`after-new-session `
```
Destinations: [MyST scoped: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/),
[MyST full: after-new-session](https://libtmux.org/en/tmux/latest/catalog/hooks/after-new-session/).
## tmux formats
Link a format variable such as [`#{session_name}`](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/) to its catalog entry:
```rst
:external+tmux:ref:`session_name `
:external+full:ref:`session_name `
```
Destinations: [Scoped: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/),
[Full: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/).
The MyST references select the same scoped or full inventory:
```markdown
{external+tmux:ref}`session_name `
{external+full:ref}`session_name `
```
Destinations: [MyST scoped: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/),
[MyST full: session_name](https://libtmux.org/en/tmux/latest/catalog/formats/session_name/).
## tmux CLI manual
The same tmux inventory covers command pages:
```rst
:external+tmux:ref:`capture-pane `
```
Destination: [capture-pane](https://libtmux.org/en/tmux/latest/manual/capture-pane/).
The document role works when the target is a whole page:
```rst
:external+tmux:doc:`Full manual `
```
Destination: [Full tmux manual](https://libtmux.org/en/tmux/latest/manual/full/).
Select a command's manual page with the document role. Its portable key
also works in the full inventory:
```rst
:external+tmux:doc:`tmux new-session `
:external+full:doc:`tmux new-session `
```
Destinations: [Scoped: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/),
[Full: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/).
Use the same document role in MyST:
```markdown
{external+tmux:doc}`tmux new-session `
{external+full:doc}`tmux new-session `
```
Destinations: [MyST scoped: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/),
[MyST full: new-session](https://libtmux.org/en/tmux/latest/manual/new-session/).
## Command options, environment variables, and terms
The Python port's workspace documentation exports standard-domain objects.
Set the program context before referencing an option:
```rst
.. program:: tmuxp load
:external+py:option:`--append`
```
Destination: [tmuxp load --append](https://libtmux.org/en/py/latest/workspace/cli/load/#arguments-and-flags-a-append).
Environment variables and glossary terms use their own roles. Qualify the
product when a term appears in more than one glossary:
```rst
:external+py:envvar:`COLUMNS`
:external+py:term:`async `
```
Destinations: [COLUMNS](https://libtmux.org/en/py/latest/workspace/configuration/environment/#cli-and-runtime-variables-columns-lines-rows),
[async](https://libtmux.org/en/py/latest/reference/glossary/#async).
## MCP tools and schema fields
Portable labels also address protocol objects and individual JSON Schema
fields. No custom MCP domain is required:
```markdown
{external+py:ref}`Read tools batch `
{external+py:ref}`on_error `
```
Destinations: [Read tools batch](https://libtmux.org/en/py/latest/mcp/tools/call_read_tools_batch/#mcp.tool.call_read_tools_batch),
[on_error](https://libtmux.org/en/py/latest/mcp/tools/call_read_tools_batch/#mcp.tool.call_read_tools_batch.input%2Fproperties%2Fon_error).
## Shared documentation and headings
The full inventory includes the shared documentation:
```rst
:external+full:doc:`Inventory guide `
:external+full:ref:`Configure Intersphinx `
```
Destinations: [Inventory guide](https://libtmux.org/en/meta/intersphinx/),
[Configure Intersphinx](https://libtmux.org/en/meta/intersphinx/#configure-intersphinx).
## Local files and fallback locations
Download an inventory and pass its filename as the second mapping value.
The first value remains the documentation URL used in generated links:
An ordered list tries a local copy before the HTTP inventory:
See the [inventory guide](https://libtmux.org/en/meta/intersphinx/) for key stability and the
[Sphinx Intersphinx reference](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html)
for configuration options.
---
# AI policy
Source: https://libtmux.org/en/ruby/v0.1.1/meta/ai-policy/
> Use the libtmux packages with AI tools. When contributing, check what you submit and wait for a maintainer's invitation before preparing a pull request.
Last revised October 25, 2026
Use these packages with AI tools as you like. When contributing, check what
you submit and wait for a maintainer's invitation before preparing a pull
request.
This policy covers the libraries, ports, and documentation in the
[libtmux](https://github.com/libtmux) and
[tmux-python](https://github.com/tmux-python) organizations, including their
issues, comments, discussions, reviews, and pull requests.
## 1. For users: About these packages
We encourage you to use AI tools with our libraries and documentation,
including to generate code that calls the libraries. We want these projects to
be useful in the way you work. Some of what we publish supports that workflow:
- [MCP integrations](https://libtmux.org/en/py/latest/mcp/) that let assistants interact with tmux.
- Markdown documentation and `llms.txt` and `llms-full.txt` resources linked
from documentation pages.
- [`AGENTS.md`]() and other repository instructions for tools working on the
codebase.
Use the documentation for the package, language, and version you are working
with.
### How the ports are made
The ports build on [tmuxp](https://tmuxp.git-pull.com/), started in 2013, and
[libtmux](https://libtmux.git-pull.com/), started in 2016. Both projects
predate today's LLM coding tools. Current work on the original projects and the
ports uses AI assistance.
Maintainers guide the design, review changes, test behavior against real tmux,
and decide what ships. They remain responsible for what the projects include
and maintain.
### Stability
Packages in the [libtmux organization](https://github.com/libtmux) are
experimental unless their README says otherwise. Names and APIs may change
between releases. Pin an exact version and review changes before upgrading.
## 2. For contributors: How to report issues, ideas, suggestions, and security issues
A useful issue is a contribution in its own right. Your experience helps us see
what breaks, what feels awkward, and what is missing. You do not need to arrive
with a patch.
Open bugs, ideas, and suggestions in the repository of the package they
concern. Report suspected security vulnerabilities privately using the route
below.
### Bugs
Tell us what you tried, what happened, and what you expected. Include a small
example we can run, the library and tmux versions, and the actual output.
Include relevant logs and error messages as they appeared, and distinguish what
you observed from possible explanations.
Bug reports are welcome whether the code you were running was handwritten or
generated. Include the code that ran. A conversation transcript is usually
unnecessary; include relevant prompts or tool calls when they are needed to
reproduce the problem.
### Ideas and suggestions
Explain what you wanted to accomplish and where the current design gets in
your way. We welcome feedback on:
- Usability: what was hard to find or get right, and where the documentation
misled you.
- Language conventions: whether a port feels natural in the language you use.
- Aesthetics: how an API, a page, or an error message reads.
- Missing capabilities: what you could not accomplish with the current API.
If an API you expected is missing, describe the task and why that API would
help.
### Security issues
Report suspected security vulnerabilities privately. Use **Report a
vulnerability** on the affected repository's security page where available.
Include the affected package and version, the tmux version, what you observed,
and reproduction steps or relevant code references if available. Distinguish
verified behavior from suspected impact. Please coordinate public disclosure
with us so we can investigate and, where needed, prepare a fix.
Command execution explicitly requested through documented features is expected
behavior and does not, by itself, establish a vulnerability. Explain any
additional failure, such as data being interpreted as commands unexpectedly or
an intended security boundary being crossed.
### Writing your report
Short reports, typos, and imperfect English are fine. Translation and writing
assistance are welcome; check that the result says what you mean and preserves
the facts you observed. Your experience matters more than a polished report.
### Using AI tools
If AI materially helped prepare your issue, comment, discussion, review, or
pull request, say which tools you used, what they did, and what you checked
yourself. Using these tools does not count against a contribution. You remain
responsible for what you submit.
Do not send generated code, reports, or explanations you have not checked.
### Pull requests
Before preparing a pull request, open an issue or comment on an existing issue
about the proposed change. Wait for a maintainer to invite a PR for an agreed
scope. Opening an issue alone is not an invitation. PRs submitted without that
agreement may be closed without review. Arrange security fixes through the
private reporting route.
Even when we agree that something should change, we may prefer to write the
implementation ourselves. Review must consider correctness, supported tmux
versions, language conventions, documentation, and ongoing maintenance.
Checking those can take more time than writing the change, whether it was
handwritten or generated.
If we invite a PR, understand the changes, test them, and be able to explain
them. Follow the project's contribution instructions, and describe the tests
you ran and the versions you used. Generated code and tests can share the same
mistake, so passing tests alone do not show that a change fits the project.
Submit only material you have the right to contribute on terms compatible with
the project's license. Identify outside code, tests, or text incorporated into
the contribution, including material adapted with AI assistance, and provide
its source and license. Keep required notices, and raise unclear permissions
before including the material. These source and licensing requirements apply
to maintainers too.