RubyHome

Choose documentation 3

v0.1.2

Current version

v0.1.2
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

libtmux for Ruby

Ruby-based tmux client

tmux client library for Ruby. Control tmux, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents

Product
Capability: Navigate tmux via a typed object graph
Example details
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|
pane_ids = window.panes.map(&:id).join(", ")
puts "#{window.name}: #{pane_ids}"
end
require "libtmux"
begin
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|
pane_ids = window.panes.map(&:id).join(", ")
puts "#{window.name}: #{pane_ids}"
end
rescue StandardError => error
warn "tmux quickstart failed: #{error.message}"
raise
end
require "libtmux"
server = LibTmux::Server.start
begin
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"]
)
snapshot = server.snapshot
snapshot.windows.each do |window|
pane_ids = window.panes.map(&:id).join(", ")
puts "#{window.name}: #{pane_ids}"
end
ensure
server.close
end
require "libtmux"
server = LibTmux::Server.start
begin
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"]
)
snapshot = server.snapshot
snapshot.windows.each do |window|
pane_ids = window.panes.map(&:id).join(", ")
puts "#{window.name}: #{pane_ids}"
end
rescue StandardError => error
warn "tmux quickstart failed: #{error.message}"
raise
ensure
server.close
end
Run this example

POSIX shell, tmux 3.3a or newer, Ruby 4.0.7, Bundler 4.0.20, a C build toolchain for gems and access to rubygems.org.

Save the program above as quickstart.rb in a new directory, along with the files below.

The Ruby block closes its server automatically. Cleanup shows the explicit ensure form. The program stops its private tmux server and removes any temporary files it created when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.

Gemfile
source "https://rubygems.org"
gem "libtmux", "~> 0.1"
Gemfile.lock
GEM
remote: https://rubygems.org/
specs:
fiddle (1.1.8)
fileutils (1.8.1)
io-console (0.9.4)
json (3.0.2)
libtmux (0.1.0)
fiddle (~> 1.1)
io-console (~> 0.8)
json (~> 3.0.2)
securerandom (~> 0.4)
tmpdir (~> 0.3)
securerandom (0.4.1)
tmpdir (0.3.2)
fileutils
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
libtmux (~> 0.1)
CHECKSUMS
fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
fileutils (1.8.1) sha256=2d541a1ee34a9ed5c4f054be8fb819a26b5d654488a31dc8806917cf448b9cc3
io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1
json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
libtmux (0.1.0) sha256=e1cc928de46d517fc9956ba6161a5b9d205ae8ed337bfd31689b1adb75f3ef00
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
tmpdir (0.3.2) sha256=e276ef386182169f8d417d9e90f12dba82a54e81646df0caf21de4a7987a0c27
BUNDLED WITH
4.0.20

Setup and run

bundle config set --local path vendor/bundle
bundle install --jobs 2
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
bundle exec ruby quickstart.rb

Expected output

main: %0
logs: %1, %2

Library revision: c8b839595d5a.

examples/api/capture.rb
# frozen_string_literal: true
begin
# docs:begin main
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
# docs:end main
rescue StandardError => error
warn "#{error.class}: #{error.message}"
exit 1
end

The whole program, examples/api/capture.rb. The port's CI runs it in .github/workflows/compatibility.yml: bundle exec scripts/matrix.

examples/api/new_window.rb
# frozen_string_literal: true
begin
# docs:begin main
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
# docs:end main
rescue StandardError => error
warn "#{error.class}: #{error.message}"
exit 1
end

The whole program, examples/api/new_window.rb. The port's CI runs it in .github/workflows/compatibility.yml: bundle exec scripts/matrix.

Install libtmux

$ gem install libtmux
gem "libtmux"

MCP

The Ruby server lets an agent work with tmux through 8 tools.

Worked example

Given this prompt, an agent connected to the Ruby server makes these calls and gets these results.

tmux window of 1 pane
%0 cat
@0

tmux_snapshot read the panes; tmux itself is unchanged.

you

List the panes on my tmux server.

agentAsk the server which tools and limits are enabled.

Result
{
"ok": true,
"data": {
"endpoint": "docs",
"server_identity": {
"generation": "2b7aa398dd617620e4e735be8ff694bc",
"pid": 2482776,
"start_time": 1793692756,
"tmux_version": "3.8a"
},
"enabled_tools": [
"tmux_capabilities",
"tmux_snapshot",
"tmux_send",
"tmux_create",
"… 4 more"
],
"criteria_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:libtmux-ruby:where:1",
"title": "Ruby libtmux criteria version 1",
"description": "The Ruby decoder additionally enforces byte, depth and node limits, rejects duplicate keys and floating-point tokens for integer fields.",
"oneOf": [
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
"… 1 more"
],
"$defs": {
"session.id": {
"anyOf": "…"
},
"session.name": {
"anyOf": "…"
},
"session.created": {
"anyOf": "…"
},
"session.attached": {
"anyOf": "…"
},
"session.window_count": {
"anyOf": "…"
},
"session": {
"type": "object",
"additionalProperties": false,
"properties": "…"
},
"window.id": {
"anyOf": "…"
},
"window.name": {
"anyOf": "…"
},
"window.width": {
"anyOf": "…"
},
"window.height": {
"anyOf": "…"
},
"window.pane_count": {
"anyOf": "…"
},
"window.layout": {
"anyOf": "…"
},
"…": "30 more"
}
},
"limits": {
"max_captures": 16,
"max_capture_bytes": 8388608,
"capture_ttl_seconds": 30,
"request_timeout_seconds": 5,
"max_response_bytes": 1048576,
"max_mutation_bytes": 65536,
"max_observers": 16,
"max_enrollments": 8,
"default_enrollment_seconds": 60,
"max_enrollment_seconds": 300,
"max_script_bytes": 65536,
"default_run_output_bytes": 65536,
"…": "2 more"
},
"owns_daemon": false,
"resource_subscriptions": false,
"observation": {
"screen": "bounded_rows",
"history_continuity": "unknown",
"process_cursor": "conditional",
"requirements": [
"tmux >= 3.3a",
"64-bit Linux with peer pidfds (kernel >= 6.6)",
"same PID namespace and matching procfs",
"live pane process",
"… 3 more"
],
"wait_conditions": [
"screen_contains",
"process_exit"
]
},
"authored_run": {
"availability": "conditional",
"shell_profile": "zsh-5.9-zle",
"enrollment": "explicit_source",
"authorization": "exact_generation_at_queue_grant",
"stdin": "closed",
"persistent_shell_changes": false,
"descendant_termination": "unobserved",
"requirements": [
"tmux >= 3.3a",
"64-bit Linux with peer pidfds (kernel >= 6.6)",
"same PID namespace and matching procfs",
"live pane process",
"… 5 more"
]
}
}
}

agentList the panes.

tmux_snapshot {"entity":"pane","limit":20}
Result
{
"ok": true,
"data": {
"capture_id": "aec1a3d6e62a4ceb2c8326b8a54dcbc7",
"server_identity": {
"generation": "2b7aa398dd617620e4e735be8ff694bc",
"pid": 2482776,
"start_time": 1793692756,
"tmux_version": "3.8a"
},
"entity": "pane",
"coverage": {
"session": "complete",
"window": "complete",
"pane": "complete",
"window_link": "complete",
"client": "complete"
},
"interval": {
"clock": "monotonic_seconds",
"started": 216410.154280461,
"finished": 216410.167168026,
"reads": 5
},
"items": [
{
"kind": "pane",
"fields": {
"id": "%0",
"windowId": "@0",
"index": 0,
"pid": 2482777,
"currentCommand": "cat",
"currentPath": "/home/user/project",
"title": "host",
"active": true,
"dead": false,
"deadStatus": null,
"width": 80,
"height": 24
},
"ref": {
"generation": "2b7aa398dd617620e4e735be8ff694bc",
"kind": "pane",
"id": "%0"
}
}
],
"truncated": false
}
}

Calls and results are recorded against libtmux/libtmux-ruby at 20744401e, on a private tmux 3.8a server. The preview draws only what those results state. The agent's narration is illustrative. This page documents 46474e8a2, a different revision. The calls have the same input and output schemas there, but the results at that revision may differ from the recorded ones. Long values in a result are shortened here; the recording keeps them. The sandbox's project directory is shown as /home/user/project.

Install libtmux-mcp

Choose your client and configuration scope, then run the command or paste the configuration.

With RubyGems installed:

$ claude mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (this project)

With RubyGems installed:

$ claude mcp add tmux --scope user -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (all projects)

With RubyGems installed:

$ claude mcp add tmux --scope project -- libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .mcp.json (in repo, version-controlled)

With Bundler installed:

$ claude mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (this project)

With Bundler installed:

$ claude mcp add tmux --scope user -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (all projects)

With Bundler installed:

$ claude mcp add tmux --scope project -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .mcp.json (in repo, version-controlled)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: claude_desktop_config.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: claude_desktop_config.json

With RubyGems installed:

$ codex mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.codex/config.toml

With RubyGems installed:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "libtmux-mcp"
args = ["--socket-name", "libtmux-docs", "--endpoint", "local"]

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .codex/config.toml (in repo)

With Bundler installed:

$ codex mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.codex/config.toml

With Bundler installed:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "bundle"
args = ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .codex/config.toml (in repo)

With RubyGems installed:

$ gemini mcp add --scope user tmux libtmux-mcp -- --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/settings.json

With RubyGems installed:

$ gemini mcp add --scope project tmux libtmux-mcp -- \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .gemini/settings.json (in repo)

With Bundler installed:

$ gemini mcp add --scope user tmux bundle -- exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/settings.json

With Bundler installed:

$ gemini mcp add --scope project tmux bundle -- exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .gemini/settings.json (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .cursor/mcp.json (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.cursor/mcp.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .cursor/mcp.json (in repo)

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.cursor/mcp.json

With RubyGems installed:

$ grok mcp add --scope user tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.grok/config.toml

With RubyGems installed:

$ grok mcp add --scope project tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ./.grok/config.toml (in repo)

With Bundler installed:

$ grok mcp add --scope user tmux -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.grok/config.toml

With Bundler installed:

$ grok mcp add --scope project tmux -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ./.grok/config.toml (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/config/mcp_config.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/config/mcp_config.json

With RubyGems installed:

$ opencode mcp add tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.config/opencode/opencode.jsonc

With Bundler installed:

$ opencode mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.config/opencode/opencode.jsonc

Embed the server in a program
examples/mcp_protocol.rb
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 }

Excerpt of examples/mcp_protocol.rb, which bundle exec scripts/matrix runs in the repository's CI (.github/workflows/compatibility.yml).

Workspace Manager

Tiled layout

The tiled layout arranges the panes in a grid.

session_name: work
environment:
PROJECT_MODE: development
windows:
- window_name: editor
window_index: 1
layout: tiled
panes:
- shell_command: printf 'editor ready\n'
- {}
{
"session_name": "work",
"environment": {
"PROJECT_MODE": "development"
},
"windows": [
{
"window_name": "editor",
"window_index": 1,
"layout": "tiled",
"panes": [
{
"shell_command": "printf 'editor ready\\n'"
},
{}
]
}
]
}

Converted from the YAML; the tests load the YAML.

1:editor
Window 1, editor: 2 panes, tiled❯ printf 'editor ready\n'
layout tiled

Save the file as workspace.yaml and load it with:

libtmux-workspace load workspace.yaml

Workspace file: examples/workspace.yaml. Tested by test_installed_imports_use_declared_dependencies_without_starting_resources (test/packaging/package_test.rb; a test runs the command).

Install libtmux-workspace

The workspace package is separate from the core library.

$ gem install libtmux-workspace
gem "libtmux-workspace"
Build the workspace from a program
examples/workspace_apply.rb
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")

Excerpt of examples/workspace_apply.rb, which bundle exec scripts/matrix runs in the repository's CI (.github/workflows/compatibility.yml).

Packages and integrations

API references

All Ruby versions and install instructions

Esc

Type to search.

Reference previewOpen reference