Skip to content

Repository files navigation

libtmux for TypeScript

Typed control of tmux for Bun and TypeScript — immutable snapshots, declarative queries, zero runtime dependencies.

QuickstartQueryingPackagesAPI referenceExamplesChangelog

npm downloads typescript tmux dependencies license


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.

Is this for you?

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.

Quickstart

$ bun add libtmux
npm, pnpm, yarn
$ npm i libtmux
$ pnpm add libtmux
$ yarn add libtmux

Requires Bun 1.3.14+ or Node 22+, and tmux 3.2a or newer.

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)?.sessionName);

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;

What querying looks like

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, so they serialize — the same object can come from a config file, an MCP call, or a CLI flag.

Packages

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. npm
@libtmux/mcp An MCP server exposing tmux to an AI agent. npm
@libtmux/workspace Declarative workspace builder, tmuxp-shaped config. npm
examples Runnable examples, executed as tests.

libtmux — the library

$ bun add libtmux
import { 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

Add it to any MCP client — this is the whole configuration:

{
  "mcpServers": {
    "tmux": {
      "command": "npx",
      "args": ["-y", "@libtmux/mcp"],
      "env": { "LIBTMUX_SOCKET_NAME": "agent" }
    }
  }
}
Claude Code, in one command
$ claude mcp add tmux --env LIBTMUX_SOCKET_NAME=agent -- npx -y @libtmux/mcp

The tools an agent reaches for first:

Tool What it does
run_command Runs a shell command, waits for it, reports its real exit status
wait_for_text Blocks until a pane prints something, streaming tmux notifications
wait_for_text_task The same wait as an MCP task: a handle now, the result later
observe Only what a pane printed since your cursor
whoami Which pane the server runs in, and which panes a person is watching
build_workspace A session and all its windows in one tmux invocation

Panes, windows, sessions, layouts, options, buffers and environment are covered too, and the server is browsable: tmux:// resources, subscribable pane contents, prompts, and completions.

Read next: Why it exists · Configuration · Choosing the right tool · Long waits

@libtmux/workspace — declarative sessions

Describe a session; apply it. Applying twice converges rather than duplicating.

$ bun add @libtmux/workspace
import { 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 examples

How commands travel

Transport, chaining and concurrency are independent, each one token at the call site, and none of them changes what you get backthe full table is here.

Mode Turn it on When to use it
spawning the default A script that runs a few commands and exits
connected await server.connect() Anything long-lived, or a loop reacting to events
watching server.watch() Reacting to a change rather than polling to find it
planned .plan + server.batch([…]) Creating or changing several things at once

Twelve windows, measured: one-at-a-time costs 64 tmux invocations and about a second; batched costs 5 and about 40 ms. Same answer, different cost — reproduce it with bun packages/libtmux/scripts/bench-modes.ts.

What this package promises

  • 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.

Repository

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.

License

MIT — a port of tmux-python/libtmux.

About

Alpha. Typed tmux control for Bun and TypeScript: immutable snapshots, declarative queries, zero runtime dependencies. Ships an MCP server for AI agents.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages