Typed control of tmux for Bun and TypeScript — immutable snapshots, declarative queries, zero runtime dependencies.
Quickstart • Querying • Packages • API reference • Examples • Changelog
Warning
Alpha. Releases carry an -alpha prerelease tag. The API is not
settled, and any release may change or remove exported identifiers without a
deprecation period. Pin an exact version. Not recommended for production.
Read the changelog before you upgrade.
Yes, if you drive tmux from a program — an agent that runs commands and reads what they printed, a workspace launcher, a test harness, a dashboard — and you want the terminal's state as typed data rather than as parsed strings.
Probably not, if you want a .tmux.conf generator or a TUI. This is a
library for controlling a running server, not for configuring one.
The idea in one line: read the whole server once into an immutable snapshot, then query it like data.
$ bun add --exact libtmux@0.1.0-alpha.8npm, pnpm, yarn
$ npm i --save-exact libtmux@0.1.0-alpha.8$ pnpm add --save-exact libtmux@0.1.0-alpha.8$ yarn add --exact libtmux@0.1.0-alpha.8Requires Bun 1.3.14+ or Node 22+, and tmux 3.2a or newer.
Linux is the only supported host for real tmux control. The macOS CI lane checks package artifacts without exercising tmux; macOS runtime behavior is unproven. WSL is untested.
import { Server } from "libtmux";
const server = new Server();
const snapshot = await server.snapshot();
// No further tmux calls: everything below resolves against the snapshot.
const editors = snapshot.panes.where({ currentCommand: "vim" });
console.log(editors.count(), editors.at(0)?.session?.name);Building something rather than reading it looks like this — and this block is a
literal excerpt of examples/quickstart/quickstart.ts, which the
integration suite runs against a real tmux server:
const session = await server.newSession({ name: "quickstart" });
const editor = await session.newWindow({ name: "editor" });
await editor.split();
const snapshot = await server.snapshot();
const found = snapshot.windows.where({ name: "editor" }).one();
const paneCount = found.panes.length;This is the part worth judging the library on. .where() takes declarative,
serializable criteria; .filter() takes an ordinary predicate. They are
never overloaded into each other.
// Equality, string operators, AND/OR/NOT, and regular expressions as data.
snapshot.sessions.where({
AND: [
{ name: { startsWith: "prod" } },
{ windows: { some: { name: { regex: { pattern: "^log", flags: "" } } } } },
],
});
// Quantifiers over relations: some / every / none, and is / isNot.
snapshot.windows.where({ session: { is: { name: "work" } } });
// Case-insensitive when you ask for it.
snapshot.sessions.where({ name: { contains: "API", mode: "insensitive" } });A Selection is immutable, ordered, replayable, and Iterable — but it is
deliberately not an Array:
selection.length;
selection.at(-1);
selection.toArray();
[...selection];
selection.one({ name: "work" }); // throws NoMatchError / MultipleMatchesError
selection.oneOrUndefined({ name: "work" });
selection.exists({ name: "work" });Criteria are data. encodeWhereDocument writes a model-tagged query;
decodeWhereDocument validates one read from a config file, MCP call, or CLI.
Three packages, released together, each usable on its own.
| Package | What it is | npm |
|---|---|---|
| libtmux | The library. Server, session, window, pane and client handles over a snapshot. | |
| @libtmux/mcp | An MCP server exposing tmux to an AI agent. | |
| @libtmux/workspace | Declarative workspace builder, tmuxp-shaped config. | |
| examples | Runnable examples, executed as tests. | — |
libtmux — the library
$ bun add --exact libtmux@0.1.0-alpha.8import { Server } from "libtmux";
const server = new Server();
const session = await server.newSession({ name: "work" });
const editor = await session.newWindow({ name: "editor" });
await editor.split();
await editor.panes.at(0)?.sendKeys("echo hello");
const lines = await editor.panes.at(0)?.capture();Read next: Snapshots · Querying · Operations · Watching · Recipes · Errors · API reference
@libtmux/mcp — tmux for an AI agent
A stdio MCP server. Point it at a socket and an agent can list sessions, read a pane, send keys, and wait for output rather than polling for it.
$ npx -y @libtmux/mcp@0.1.0-alpha.8Add it to any MCP client — this is the whole configuration:
{
"mcpServers": {
"tmux": {
"command": "npx",
"args": ["-y", "@libtmux/mcp@0.1.0-alpha.8"],
"env": { "LIBTMUX_SOCKET": "agent" }
}
}
}Claude Code, in one command
$ claude mcp add tmux --env LIBTMUX_SOCKET=agent -- \
npx -y @libtmux/mcp@0.1.0-alpha.8The tools an agent reaches for first:
| Tool | What it does |
|---|---|
list_sessions |
Lists stable session identities and metadata |
capture_since |
Returns only pane output after a cursor |
wait_for_text |
Waits for bounded literal or regular-expression matches |
run_shell_command |
Runs a command through a pane and reports output and exit status |
create_session |
Creates a session without accepting a command or environment value |
The 45 tools are split into inspect, manage, execute, and teardown.
tmux://capabilities is the only resource; the server exposes no prompts,
resource templates, background jobs, or generic mutation tools.
Read next: Why it exists · Configuration · Toolsets and trust · Choosing the right tool
@libtmux/workspace — declarative sessions
Describe a session; apply it. Applying twice converges rather than duplicating.
$ bun add --exact @libtmux/workspace@0.1.0-alpha.8 libtmux@0.1.0-alpha.8import { Server } from "libtmux";
import { applyWorkspace } from "@libtmux/workspace";
const server = new Server();
await applyWorkspace(server, {
session_name: "api",
windows: [
{ window_name: "editor", panes: ["vim", "git status"] },
{ window_name: "server", panes: [{ shell_command: "bun dev", focus: true }] },
],
});Read next: The config shape · Converging
examples — runnable, and run
Four programs covering acquisition, control-mode watching, the act-then-wait loop an agent needs, and building a workspace. Each is executed by the integration suite, so the code there is the code that runs.
$ bun test examplesObservation, planning and concurrency compose around the same command engine — the full table is here.
| Mode | Turn it on | When to use it |
|---|---|---|
| connected | await server.connect() |
Pairing commands with a persistent event observer |
| watching | server.watch() |
Reacting to a change rather than polling to find it |
| pipeline | server.pipeline([…]) |
Ordered commands when printed output is enough |
| planned | .plan + server.batch([…]) |
Ordered mutations that must return typed handles |
| concurrent | Promise.all |
Independent work that may safely overlap |
For its create-twelve-windows-and-query workload, the benchmark uses 25
invocations one at a time, 13 through pipeline, and 14 through batch; the
last includes the snapshot that resolves typed handles. It reports
machine-specific timings beside those deterministic counts:
$ bun packages/libtmux/scripts/bench-modes.ts- Zero runtime dependencies. A property under test, not an aspiration.
- Real tmux, every commit. CI runs the suite against every tmux release the badge above names — no mocks stand in for a server.
- Documentation is a gate. Every public symbol carries a compiled example, the API reference is generated from the source that implements it, and every link, install command and recipe on this page is checked on each run.
- Softly tracks Python libtmux. Names and shapes follow 0.62.0 where TypeScript agrees with them; each departure is a decision a gate holds to the code.
packages/libtmux the library
packages/mcp the MCP server
packages/workspace the workspace builder
examples runnable examples, used as tests
attic reference notes
Working here: AGENTS.md routes to the policy that governs a change. The layout and the change discipline live there; how we write is in .github/WRITING.md, and the gates, real tmux, and releases are in .github/CONTRIBUTING.md.
MIT — a port of tmux-python/libtmux.