tmux-agent
Allows sending notifications to Slack when monitored tmux sessions change state (e.g., idle, stuck, dead) via a NotifySink implementation.
Core orchestration integration: spawn and manage tmux sessions on an isolated socket, with verified input injection, pane capture, state classification, logging, and cross-session messaging.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tmux-agentspawn a worker to run the tests, then show me the fleet status"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tmux-agent
Unified tmux orchestration + observability for AI agent workflows.
A supervisor agent (or you) spawns tmux sessions running agent CLIs — Claude Code, Codex, aider,
anything — and needs to know: who owns what, is it working or stuck, what did it type, what did it
cost, and how do I talk to it safely. tmux-agent is that layer: spawn with lineage, verified
input injection, durable structured logs, state classification, cross-agent messaging, pluggable
notifications, waste analytics — all exposed as both a CLI and an MCP server, with a full audit
trail and safe-by-default policy.
Distilled from months of running a 12–26 session agent fleet in production. Every "weird" design choice below is a paid-for lesson.
Why not X?
Plain tmux MCP servers (tmux-mcp and friends) give you
send-keysandcapture-pane— no lineage, no verified input, no logging, no analytics, no audit, and the most popular one is unmaintained with an open command-injection issue. We keep the good ideas (marker-based exec, resources) and build the missing layers, injection-proof (execFileargv only, never a shell).Full orchestration frameworks (e.g. awslabs/cli-agent-orchestrator) own your whole workflow.
tmux-agentis a substrate: bring your own supervisor brain; we make the tmux layer safe, observable, and machine-readable.
Related MCP server: herdr-mcp
Install
tmux -V # needs tmux >= 3.2
node --version # and node >= 20
git clone https://github.com/dmvevents/tmux-agent-public && cd tmux-agent-public
npm install && npm run build && npm install -g . # today's path: from the clone
tmux-agent help # every verb; `tmux-agent help web` lists one verb's flagsStatus: v0.1.0, not published to npm. The npm package named
tmux-agentis a different project, so do notnpm installit; the clone above IS the install (itsnpm linkputstmux-agentandtmux-agent-mcpon your PATH), andtest/smoke-readme.shruns this block and the demo below from a fresh home on every push.
5-minute demo (no config)
# 1. spawn a monitored child session in the current directory (isolated agent socket; your tmux is untouched)
tmux-agent spawn worker-1 --cwd "$PWD" --parent me --task "run the tests"
# 2. run something in it and BLOCK until it finishes (exit code propagates)
tmux-agent exec worker-1 "ls -la"
# 3. see the whole fleet: state, context%, lineage — and publish status.json
tmux-agent status
# 4. watch until it goes idle/stuck/dead; get a notification (Slack if configured)
tmux-agent watch worker-1 --idle-secs 5
# 5. what happened? durable log + audit ledger + waste report
tmux-agent log worker-1
tmux-agent waste --since 2026-08-18T00:00:00Z
# 6. done — kill what you spawned (your own tmux was never touched)
tmux-agent kill worker-1Every verb's usage is tmux-agent help; one verb's flags, tmux-agent help <verb> (or <verb> --help).
MCP server (drive it from any agent)
// .mcp.json / claude_desktop_config.json
{
"mcpServers": {
"tmux-agent": {
"command": "tmux-agent-mcp",
"env": { "TMUX_AGENT_ACTOR": "my-supervisor" }
}
}
}Tools: spawn-session, send-input (verified two-step), exec-sync (blocking, exit code),
capture-pane (redacted), session-status, fleet-status, kill-session (ownership-checked),
list-lineage, message-send/message-receive, waste-report, read-log, policy-status
(what the active policy hides from this surface, and why).
The model
supervisor (you, or an agent via MCP)
└─ spawn-session: every session records @agent_parent / @agent_owner / @agent_task
├─ verified input: paste → verify it landed → Enter (kills the lost-Enter race)
├─ pipe-pane → ~/.tmux-agent/logs/<name>.log (byte-faithful, chmod 600)
├─ state: WORKING / IDLE / BLOCKED / ERROR / DEAD / GONE / UNKNOWN (profile-driven, event-ledgered)
└─ ledger: ~/.tmux-agent/ledger/events-YYYY-MM-DD.ndjson ← analytics + log-consumer inputIsolated by default: everything runs on tmux socket
-L tmux-agent(--socket, elseTMUX_AGENT_SOCKET, names another; a value that is not a-Lname —[A-Za-z0-9._-], 1 or more — is refused with exit 64 before anything is touched). Your interactive tmux server is invisible to it. Opt into the default server with--default-server(adoption mode).Safe by default: kill refuses sessions another actor owns; a JSON policy file adds read-only mode and per-action allow/deny lists; denied tools vanish from MCP
tools/list; every control action (and every refusal) is a ledger event; pane content is secret-redacted before it leaves the tool.Agnostic core: per-CLI specifics (context %, spinner/menu detection) live in an
AgentProfileadapter — add one for your CLI in ~20 lines. Slack is oneNotifySinkimplementation; webhook/Telegram are drop-ins.The built-in profiles, tried in this order:
Profile
Status
claude-codeverified against live sessions
codexverified — spawn/send/exec/status driven through a real
codexCLI; fixtures are captured pane textaiderbest-effort — written from documented prompts, not yet run against a live session
generic-shellfallback; holds no opinion, so CPU-delta signals decide
Why this matters concretely: a live Codex session parked on its
› 1. Update nowprompt was reported IDLE by the Claude Code profile (Codex uses›, Claude uses❯), so a supervisor would never learn it was waiting on a human. That is the class of bug a profile prevents.
Messaging between agents
Three tiers, best-available wins at runtime:
Native cross-session messaging when both ends are Claude Code and the feature is present (it is version- and provider-gated: not available on Bedrock/GCP/Foundry deployments — probe, don't assume).
File mailbox (built in): one message = one file, archive-on-read — works for any CLI, cross-team, cross-host via a shared path.
Verified
send-input: the universal floor into any CLI's stdin.
Web dashboard (loopback-only)
tmux-agent web # http://127.0.0.1:7337 (7337 busy? it scans forward and the banner names the port)
tmux-agent web --port 7338 # or pick the port yourself; --strict-port exits 75 instead of scanning
ssh -L 7337:localhost:7337 user@host # then open http://localhost:7337 locallyThe banner prints the URL with the token; the page sets it as an HttpOnly; SameSite=Strict cookie. A script
reaches the same API with ?token=… or Authorization: Bearer … (the token is at ~/.tmux-agent/web-token,
mode 0600): /api/health is the daemon's vitals, build and tmux verdict, /api/fleet the rows the table shows,
/api/session/<name> one session's pane, process and events. --no-token opts out on a single-user machine.
Serving it through another tool's proxy instead? Set TMUX_AGENT_PUBLIC_URL=http://localhost:8000/tmux-agent/
(or a whole sentence in TMUX_AGENT_PUBLIC_HINT) and the page's access note and the startup banner say that in
place of the ssh -L line. The bind stays 127.0.0.1 either way.
A single-file GUI over the same data: parent/child fleet tree with live state dots and
context-pressure warnings — every row says where its state came from, profile <name> · <age> when the daemon's
health tick read the pane and a profile spoke, cpu-only when it is the CPU-delta verdict alone — workstream history (active and decommissioned sessions,
reconstructed from the ledger), top-waste table, and per-session drill-in — live pane (redacted),
cwd + process, recent events, capture-log tail — plus a send box that goes through the exact
same verified/policy-checked/audited path as the CLI and MCP (read-only mode disables it).
Security posture (loopback is not a trust boundary — we treat it as one only for network reachability, never for authorization):
Binds
127.0.0.1; any other host is refused unless you pass both--dangerously-allow-remoteand--token. The intended path is an SSH tunnel, always.CSRF + DNS-rebinding guard on every request (
Sec-Fetch-Site+Originallowlist + loopbackHostassertion). Without it, any page your browser visits can POST to/api/sendand type into your agents — we proved that exploit before fixing it.A token is generated on first run (
~/.tmux-agent/web-token, 0600) and required by default, because any local UID on a shared host can reach a loopback port. The startup line prints a URL with the token in it;--no-tokenopts out on a single-user machine.Capture logs and the ledger are 0600 inside a 0700 home; pane content is redacted on every read path (raw
pipe-panebytes stay local and mode-restricted). For a full interactive terminal alongside it, pair with ttyd (MIT) on the same tunnel:ttyd -i 127.0.0.1 tmux -L tmux-agent attach.
Making it fit YOUR fleet (no fork required)
The shipped profiles describe the CLIs we run, and the built-in redaction patterns cover our
cloud. Neither is your fleet. $TMUX_AGENT_HOME/lexicon.json holds the vocabulary the tool reasons
with — write it yourself, or have calibrate propose it from your live fleet:
tmux-agent calibrate --out lexicon.draft.json # PROPOSES, with the evidence for each rule
# review it, then:
cp lexicon.draft.json ~/.tmux-agent/lexicon.json
tmux-agent doctor # vocabulary in effect + whether the dashboard on this host runs the build on disk and still sees its tmux server{
"version": 1,
"profiles": [{ // tried BEFORE the built-ins, so you can override ours
"name": "my-cli",
"blocked": ["^\\s*»\\s*\\d+\\."], // what "waiting on a human" looks like in YOUR CLI
"working": ["thinking…"],
"error": ["quota exceeded"],
"contextPct": "budget (\\d+)% left", "contextPctInverted": true
}],
"parents": { "nonSession": ["my-boss"], // your supervisors are processes, not tmux sessions
"nonSessionPattern": "^worker-\\d+$" },
"redact": [{ "name": "my-key", "pattern": "MYC_[A-Z0-9]{10}", "replacement": "[REDACTED]" }]
}Three properties worth knowing: redaction is additive only (a lexicon can strengthen it, never
weaken it — there is no config path to disable a built-in); failures are loud (a bad pattern
names its rule and exits 78, and so does a file you named with --lexicon or TMUX_AGENT_LEXICON
that is not there, is a directory or cannot be read — never silently the defaults); and no lexicon
file at the default path means behaviour identical to the defaults.
Why any of this exists, and the framework for deriving it: docs/AGNOSTIC-DESIGN.md.
If most inspected sessions report "classified by no profile" in the dashboard, that is the tool
being honest that it is guessing — their state came from core signals, so a session waiting on you
can read as IDLE. That banner is your cue to calibrate. And when the chain picks the wrong profile
for one session, name the right one from outside: --profile on status and watch (a lexicon
profile or a built-in, exact) judges by that one profile alone, as does profile on the MCP
session-status and fleet-status tools — a name the tool does not hold is refused with exit 64
(or an MCP error), never silently the chain's verdict.
Status & analytics
status.json (machine-readable fleet state: each session's session, state, at, lastLine,
classifiedBy, contextPct, logBytes, rssBytes, cpuPerSec, reason, parent, task,
owner, meta — those fields and no others, all of them on every record with a null where the value
is unknown rather than an absent key, each explained in item 4 of VERSIONING.md) is
published on every status/fleet-status call — point your Kanban/dashboard at it instead of
attaching to tmux; status --json FILE publishes to FILE instead, and a FILE the tool cannot write
is refused (exit 64, the reason named) before the fleet is read. Its top-level tmux block —
{ reachable, since, error, checkedAt } — is the verdict of the very listing that produced
sessions: sessions: [] with reachable: false is a dead or absent tmux server (error in tmux's
own words), with reachable: true an idle fleet. The --socket name (default tmux-agent) is a tmux -L
name: its file lives at $TMUX_TMPDIR/tmux-<uid>/<name> (/tmp by default), and a Unix socket path is capped at
108 bytes — a name whose path is longer is reported UNREACHABLE in tmux's own words (File name too long), like
a server that is not running, never as a raw error. The MCP fleet-status reply carries the same block
beside sessions and statusFile, and the status verb prints one stderr line, status: tmux server UNREACHABLE on <socket> — <error> (exit 0 — the fact rides the file). Per session, a GONE says which
failure it was: reason carries tmux's own words when the server did not answer, and is null when
the server answered and that one session is simply absent — so watch (exit 2 either way, plus one
watch: tmux server UNREACHABLE on <socket> — <error> line on stderr), session-status and
/api/session/<name> tell "restart this session" from "there is no server to restart it on". The NDJSON ledger is the
log-consumer contract: flat, versioned events
(session.spawned, input.sent, session.state, exec.done, exec.timeout, control.denied, ...) that
aggregate into the built-in waste report (error events, state churn, inputs per session).
Using it from aws-do-agent
The aws-do-agent harness serves this dashboard behind its home page at /tmux-agent/, built from this repository
at container start: TMUX_AGENT_SRC names the source (this repository's URL), TMUX_AGENT_REF the branch, tag or
commit to build, and TMUX_AGENT_INSTALL_DIR where the checkout and the build live. Its installer fetches the ref,
runs npm ci and npm run build, checks that dist/bin/tmux-agent.js --version answers, and starts the web verb
loopback-only behind its proxy, with TMUX_AGENT_PUBLIC_URL naming the proxied address in the page's access note.
Nothing in this repository is specific to that harness: any process that can run node dist/bin/tmux-agent.js web
on a loopback port can serve it the same way.
Development
npm install && npm run build && npm test # build + unit + changelog check + smoke + web-smoke
npm run lint # eslint (enforces the CONTRIBUTING rules) + tsc --noEmit
npm run test:coverage # per-file line / branch / function coverageSuite sizes and coverage numbers are deliberately not printed here: they change with every commit;
the suite's layers and how to run each one are in docs/TESTING.md.
(A literal on this page went two weeks stale before anyone noticed; test/readme-contract.test.js
now fails the build if a count or percentage comes back — and if this page's MCP tool list, verbs,
flags, npm run scripts or links stop matching the built tree.)
Two further layers need a browser and gate CI on ubuntu/node-22 (they catch what no node suite can — a JS throw, an unbound handler, composited contrast):
pip install playwright && python3 -m playwright install chromium
npm run test:ui # acceptance criteria: real clicks on a real fleet
npm run test:browser-audit # headless-chromium audit, 0 findings requiredEverything runs on an isolated tmux socket and a throwaway TMUX_AGENT_HOME — your own tmux
sessions are never touched. See CONTRIBUTING.md for the six design rules
(two of them are enforced by custom lint rules, not honour system).
Roadmap & planning
Doc | What it holds |
what this is and what it refuses to become — the one sentence, ordered priorities, anti-goals, and the operator directive ledger. Read this first; everything below serves it. | |
the north star put past three independent frontier models prompted to refute it — verdict, confirmed drifts, and the board gate each became | |
assets, actors, all 9 findings as case studies, accepted residual risk | |
how to report a vulnerability (14-day response SLA) | |
what the public API is, so semver means something |
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Securely control computers you explicitly pair through files, terminals, processes, screenshots, desktop UI/input, clipboard, browser automation, diagnostics, and document tools.
Scoped agent execution. Server-side credentials, policy, budgets and verifiable receipts.
Develop, manage, and debug Railway projects, services, and deployments from within agents.
Related MCP Servers
- AlicenseBqualityDmaintenanceAgent-native SSH control plane with a local Web Terminal, human-in-the-loop secret input, keychain-backed profiles, and user-confirmed uploads for Codex, Claude Code, and MCP-compatible coding agents.181Apache 2.0
- FlicenseAqualityBmaintenanceEnables coding agents to control and inspect a running Herdr terminal session, including listing workspaces, tabs, panes, and agents, reading pane output, prompting agents, sending keys and commands, waiting for output or state, and splitting or closing panes.14-
- AlicenseNot gradedqualityBmaintenanceEnables AI agents or human operators to spawn, monitor, drive, and hook into child terminal sessions—such as CLI tools, interactive shells, and polyglot runtimes—via POSIX pseudo-terminals, providing multiplexed control, event interception, and memory-bounded observability for orchestration.MIT
- AlicenseAqualityAmaintenanceEnables AI assistants to spawn and control persistent local shells, SSH sessions, REPLs, debuggers, and serial/UART devices with zero-loss output capture, timestamped history, and atomic expect matching for command execution and prompt synchronization.1218 PyPIMIT