Configurable tmux agent launcher. Define AI agents (or any CLI) in TOML; sessions auto-launch the correct agent, tabs are colour-coded per agent, and prefix m cycles through the list.
Clone the repo, open it in Claude Code, and run:
/agentmux:install
The command checks dependencies, runs the installer, and interactively wires your shell config, ~/.tmux.conf, and Claude Code hooks. It then self-installs to ~/.claude/commands/ so /agentmux:install is available globally for future updates from any directory.
curl -fsSL https://raw.githubusercontent.com/lockyc/agentmux/main/install.sh | bashThis clones agentmux into ~/.agentmux/ (a git clone — update later with
amux --update). You can also clone the repo and run bash install.sh directly.
Then complete the setup:
1. Install dependencies (if not already present):
which toml2json || brew install go-toml
which jq || brew install jq2. Add to your shell config:
bash/zsh (~/.zshrc or ~/.bashrc):
source ~/.agentmux/shell/agentmux.shfish (~/.config/fish/config.fish):
source ~/.agentmux/shell/agentmux.fish3. Add to your ~/.tmux.conf:
source-file ~/.agentmux/tmux/agentmux.conf
4. Edit ~/.agentmux/amux.toml to define your agents (created from the example by the installer).
5. Reload:
source ~/.zshrc # or restart your shell
tmux source ~/.tmux.conf # or start a new tmux server6. Launch your first session:
amux- tmux
toml2json:brew install go-tomljq:brew install jq- A local OpenAI-compatible LLM endpoint (optional — for AI summary status lines; e.g. LM Studio, Ollama)
reattach-to-user-namespace(optional, macOS — only if usingreattach = truein amux.toml)osascript(optional, macOS — only for--notifydesktop alerts in the Claude Code hooks)
agentmux is driven two ways: amux … shell commands you type at a prompt to launch and manage sessions, and tmux key bindings you press once you're inside a session. Never used tmux? Read Inside a session: tmux basics first — you don't need to know tmux going in.
Normal shell commands — type them at a prompt.
| Command | Effect |
|---|---|
amux |
New/attach session; agent auto-selected from the current directory (see below), else the first agent in the list |
amux -<flag> |
New/attach session, agent matching flag (e.g. -w for flag = "w") |
amux <agent> |
New/attach session, agent by name |
amux <agent> <session> |
New/attach named session with specified agent |
amux --sessions |
List agentmux agent sessions (name, agent, windows, attach state) |
amux --log |
Durable roster of agent windows amux has opened, grouped by project — recover sessions lost to a server kill or reboot. Prints one cd per project, then a resume command per window tagged ● live / ✗ lost |
amux --kill [session] |
Kill an agent session and its frame + terminal (default: current dir) |
amux --kill-all |
Kill every agent session + all frames/terminals (asks first) |
amux --update |
Update to the latest agentmux (git pull --ff-only of ~/.agentmux) |
amux --colours [grid|pick] |
Preview the colour palette (curated names + 256 codes). pick [agent] interactively builds a paste-ready colour = line |
amux --frame [agent] [session] |
Side-terminal layout: bare shell (left) + amux (right) as a nested tmux |
amux --no-frame |
One-off plain launch when [frame] default = true is set (skips the frame) |
amux --frames |
List active --frame wrappers (they live on a separate tmux socket) |
amux --frame-kill [session] |
Tear down a frame (wrapper + its left terminal); the agent keeps running |
amux --frame-kill-all |
Tear down ALL frames + scratch terminals at once; agents keep running |
Set [update] check = true in ~/.agentmux/amux.toml to enable a once-daily
check that notifies (notify-only) when a newer agentmux is available on GitHub.
Sessions are named after basename $PWD (dots → underscores) by default — run amux in your project directory and it picks up the name automatically. Pass an explicit name with amux <agent> <session> to override. agentmux sessions get a coloured status bar, AI summary rows, and tab-state emojis; plain tmux sessions are left unstyled.
amux launches your agent inside tmux, which keeps the session running in the background — even if you close the terminal window. You drive tmux with a handful of keyboard shortcuts; here's everything you need, no prior tmux knowledge assumed.
Reading the shortcuts. C-b means hold Ctrl and tap b. Likewise C-f is Ctrl+f. That's the whole notation.
The prefix. tmux can't act on its shortcuts directly — they'd collide with the program running inside (your agent wants Ctrl+C, Ctrl+R, and so on for itself). So you first press a prefix key to get tmux's attention, release it, then press the command key. The default prefix is C-b (Ctrl+b). When you see prefix c, it means: press Ctrl+b, let go, then press c. To use a different key for amux sessions, set [amux] prefix (e.g. prefix = "C-a") — it applies only to amux's own sessions, not your other tmux work. It can be overridden per-directory with an [amux.dirs."<path>"] block, exactly like [frame.dirs] (see Side-terminal layout).
The three keys you'll actually use — each pressed after the prefix:
c— create a new tab (auto-launches the current agent)x— close the current tab (exit)d— detach: leave everything running in the background and drop back to your shell
Detaching is how you step away without stopping the agent. Press prefix d (Ctrl+b, then d) and your agent keeps working; re-run amux in the same directory to reattach. Closing the terminal window detaches too — it never kills the session. (Inside a --frame the prefix is C-f, so you detach with C-f d — see Side-terminal layout.)
All of these are pressed after the prefix (C-b by default).
| Press | Effect |
|---|---|
prefix c |
New tab — auto-launches the current @agent-mode agent (tmux's built-in new-window key; agentmux just hooks it to launch the agent) |
prefix x |
Close the current tab. In an agentmux session's last pane it respawns + relaunches the agent instead of destroying the session; everywhere else it's tmux's kill-pane |
prefix d |
Detach — leave the session running and return to your shell |
prefix m |
Cycle @agent-mode through your defined agents (agentmux sessions only) |
Give an agent a dirs list and a bare amux (no flag, no agent name) auto-selects it based on the current directory — no flag or prefix m toggle needed:
[[agents]]
name = "work"
cmd = "CLAUDE_CONFIG_DIR=~/.claude-work claude"
dirs = ["~/work", "~/clients"]A pattern matches when $PWD is that directory or any subdirectory of it (~/work covers ~/work/acme/api). ~ expands to $HOME. When more than one agent matches, the longest (most specific) path wins, so you can nest a specific rule inside a broader one. If nothing matches, amux falls back to the first agent in the list. An explicit -<flag> or <agent_name> argument always overrides directory routing.
Matching uses the logical path — $PWD as your shell shows it — and symlinks are not resolved. Write patterns the way you actually cd into the directory (e.g. via the symlink path, not its target).
amux --frame [agent] [session] puts a persistent scratch terminal in the left
pane beside amux in the right. It's a nested tmux on its own socket
(agentmux-frame, session <session>-frame), so amux runs completely unchanged
on the right. There are three status bars: a thin full-width outer bar (the
frame, showing the project + clock), and a per-pane bar under each side — the left
terminal's own tab bar and amux's. (Requires tmux ≥ 3.1 for the -l % split.)
- The left pane is its own dedicated tmux on a separate
agentmux-termsocket (configtmux/term.conf): its own tab bar, and it never dies — exit the shell and it respawns. It's a bare tmux (not your~/.tmux.conf), kept isolated on purpose. - Prefixes: the frame uses
C-f("f" for frame —C-f h/lfocus left/right,C-f j/kmove within the split left column,C-f H/Lresize,C-f Qquit,C-f ddetach), overridable via[frame] prefix. The two inner tmuxes use their own prefix (C-bby default, or amux's[amux] prefixfor the right pane), which the frame passes through to whichever pane is focused: left → the terminal's tabs, right → amux. A strayprefix don either pane is harmless — the pane re-attaches in place rather than vanishing (useC-f dto detach the whole frame). The[amux]and[frame]prefixes must differ: the frame grabs its own prefix before the inner amux can see it, so a colliding[amux] prefixis ignored (with a warning). - Set the left-pane width with
[frame] left = <percent>(default30). Optionally split the left column top/bottom with[frame] left_vertical_split = <percent>(the top sub-pane's height,10–90; unset = single left pane). The top sub-pane is a plain shell; the scratch terminal (with its tab bar) stays in the bottom sub-pane — reach the top shell with the mouse. Two more layout fields:[frame] focuspicks which pane starts focused —"agent"(right, the default) or"terminal"(the left column);[frame] status_positionplaces the frame's outer status bar at"bottom"(default) or"top". Frame config applies when the frame is created — a persistent frame keeps its layout, so after changing it, tear the frame down (C-f Qoramux --frame-kill <session>) and relaunch. Killing only the agent session leaves the wrapper, which reattaches at the old size. - Open frames by default. Set
[frame] default = trueto make a bareamux(run from a plain terminal) behave likeamux --frame; useamux --no-framefor a one-off plain launch. It's ignored whenamuxruns inside an existing tmux (a frame can't nest there) — that case falls through to a normal in-tmux launch. - Nesting inside tmux. By default
--framerefuses to run inside an existing tmux: the frame is its own tmux server, so nesting it stacks prefixes (your outerC-b, the frame'sC-f, the scratch terminal's). Advanced users already living in tmux can opt in with[frame] allow_nested = true, which lifts the guard (and thedefaultskip above, so a default frame opens in-tmux too). The frame's own panes already clear$TMUX, so the inner amux/terminal still launch cleanly. - Per-directory overrides. Any
[frame]field can be overridden for a directory (and its subtree) with a[frame.dirs."<path>"]block — same matching as an agent'sdirs(~expands, longest path wins), resolved per-field, falling back to the base[frame]values. e.g. a taller split that starts focused on the terminal in one project:[frame.dirs."~/Developer/github.com/lockyc/agentmux"] left_vertical_split = 30 focus = "terminal"
- Run it from a plain terminal, not from inside tmux. Reattach with the same
amux --frame <session>(a closed pane is rebuilt). - Three sessions across three sockets.
<session>— your agent, on the default socket (what plaintmux lsshows).<session>-frame— the wrapper, onagentmux-frame.<session>-term— the left terminal, onagentmux-term. The frame is the outer layer in the nesting sense, but each lives on its own socket so their stripped configs never bleed into your normal tmux — which is also why a base-terminaltmux ls(default socket) only shows the agent. - Managing it from the base terminal:
- Leave the frame from inside:
C-f d(detach, all kept) orC-f Q(quit the wrapper; the agent survives, reattach later). amux --frames— list active frames (no need to remember the socket).amux --frame-kill [session]— tear down a frame: both the wrapper and its left terminal (default: current dir's). The agent session keeps running.amux --kill [session]— kill the whole project: the agent session plus its frame and terminal (default: current dir's). Use this instead of a rawtmux kill-sessionso nothing is left orphaned.amux --sessionslists the exact session names.
- Leave the frame from inside:
Add a new [[agents]] block to ~/.agentmux/amux.toml:
[[agents]]
name = "myagent"
flag = "a"
cmd = "my-ai-cli"
colour = "green"colour is a curated palette name (run amux --colours to preview them) or a 256
code like "colour82"/"82" — agentmux derives the active and inactive tab shades
from it automatically. Run amux --colours pick myagent to choose one interactively
and get a paste-ready line. For full manual control, skip colour and set the raw
tmux styles instead (see Optional per-agent fields).
The new agent appears in the prefix m cycle immediately (no reload needed).
| Field | Default | Effect |
|---|---|---|
flag |
— | Single-letter shorthand for amux -<flag> |
dirs |
— | List of directories; a bare amux run inside one (or any subdirectory) auto-selects this agent. ~ → $HOME; longest match wins. See Directory-based agent selection |
label |
name | Short display name used in tmux tab labels (e.g. label = "pers" for a name = "personal" agent) |
colour |
— | Tab colour as a curated palette name or 256 code ("green", "colour82", "82"); the active/inactive shades are auto-derived. Preview with amux --colours. Ignored if the raw pair below is set |
colour_inactive / colour_active |
— | Escape hatch: full raw tmux styles for total control (e.g. "fg=black,bg=colour56" / "fg=black,bg=colour93,bold"). Set both as a pair — they override colour, and setting only one is a misconfiguration (agentmux warns) |
keep_alive |
false | Appends ; exec $SHELL so the tab stays open after the agent exits |
reattach |
false | Uses reattach-to-user-namespace (macOS clipboard fix); requires keep_alive = true |
resume |
— | Resume program shown by amux --log for this agent's windows. Overrides just the executable of the recorded resume command (e.g. resume = "claude-work" → claude-work --resume <id>); omit to use the program the adapter recorded |
amux and friends launch any CLI from amux.toml. The richer integration — tab-state emojis and the AI summary status lines — runs through a per-agent adapter that lives at scripts/<agent>/status.sh. The shipped Claude Code adapter (scripts/claude/) is the reference implementation.
An adapter is a thin shim that:
-
Exports three env vars and execs the shared core (
scripts/tmux-status.sh):Env var Purpose AGENTMUX_AGENT_NAMELabel used in tab and temp-file names (e.g. claude,gemini)AGENTMUX_CTX_BINPath to a transcript context extractor (see below) AGENTMUX_DIGEST_BINPath to a transcript digest builder (see below) -
Parses whatever hook payload its agent sends and re-exports:
Env var Purpose AGENTMUX_HOOK_PROMPTLatest user prompt (working state only) AGENTMUX_HOOK_TRANSCRIPTFilesystem path to the agent's session transcript (working state only)
The shared core is hook-schema-agnostic — it consumes only those env vars, never stdin. Hook-payload parsing belongs in the adapter because every agent has a different schema.
Two optional overrides let an adapter swap in custom helpers; defaults work for everyone:
| Env var | Default | Purpose |
|---|---|---|
AGENTMUX_TAB_LABEL_BIN |
~/.agentmux/scripts/tab_label.sh |
Resolves the tab-label suffix from @window-agent. Override to render labels differently. |
AGENTMUX_SUMMARISE_BIN |
~/.agentmux/scripts/summarise.sh |
LLM-summary entry point. Override to point at a different summariser. |
Contracts for ctx.sh and digest.sh (both read the transcript path as $1):
ctx.sh <transcript> <max_msgs> [percap] [head|tail]— prints prose-only turns joined by/; used to derive the stable subject and to anchor recent activity. Filters out tool noise and pasted dumps.digest.sh <transcript> [start_line] [char_budget]— prints a chronological digest (prose + mutating tool one-liners) for the done/now/next summariser. Drops oldest prose first when over budget; keeps tool lines.
Both must print nothing and exit 0 on any error — the shared core treats them as cosmetic.
To add e.g. a Gemini CLI adapter: create scripts/gemini/{status,ctx,digest}.sh following the Claude versions, wire your agent's hook system to call ~/.agentmux/scripts/gemini/status.sh <state> with state ∈ start|working|notify|permission|done, and install.sh will pick the directory up automatically.
Keep custom adapters outside the shipped tree. ~/.agentmux/ is a git clone and amux --update runs git pull --ff-only. Because adapters are referenced by absolute path — the hook command plus the AGENTMUX_CTX_BIN/AGENTMUX_DIGEST_BIN (and other AGENTMUX_*_BIN) overrides — they can live anywhere on your filesystem. Put your own under e.g. ~/.agentmux-local/<agent>/ and point your hook command and env-var overrides at those paths. Avoid authoring an adapter under ~/.agentmux/scripts/<name>/ using a name agentmux might later ship: if a future release adds a tracked scripts/<name>/, amux --update will refuse to proceed rather than overwrite your file, blocking updates until you relocate it.
amux works in bash, zsh, and fish. All of the launcher logic lives in a single standalone bash executable — bin/amux, the one source of truth — and each interactive shell just sources a thin wrapper that defines the amux command and wires tab-completion. The launcher runs as a subprocess, so bash must be installed, but it does not have to be your interactive shell.
| Shell | amux command |
Tab-completion | Integration file | Source from |
|---|---|---|---|---|
| bash | ✓ | — | shell/agentmux.sh |
~/.bashrc |
| zsh | ✓ | ✓ | shell/agentmux.sh |
~/.zshrc |
| fish | ✓ | ✓ | shell/agentmux.fish |
~/.config/fish/config.fish |
Where completion is wired, it's backed by amux --complete — which prints the agent names and -<flag> shortcuts, one per line — and offered for the first argument only. (bash gets the command without completion; nothing stops you adding a complete script for it.)
To support a shell that isn't listed (e.g. nushell, elvish, xonsh):
-
Create
shell/agentmux.<shell>— a thin wrapper, in that shell's own syntax, that:- resolves the launcher path (default
~/.agentmux/bin/amux, overridable via theAGENTMUX_BINenv var) and defines anamuxcommand forwarding all arguments to it; - optionally registers a first-argument completion populated from
<launcher> --complete.
shell/agentmux.sh(bash/zsh) andshell/agentmux.fish(fish) are the reference implementations — both are only a few lines. - resolves the launcher path (default
-
Update both installers so the file ships and gets wired:
install.sh(it's carried by the clone; add its source line to the printed instructions) and.claude/commands/agentmux/install.md(detection + wiring). They must stay in sync. -
Source
~/.agentmux/shell/agentmux.<shell>from your shell's startup config.
No other agentmux code needs to change — bin/amux and every scripts/ helper are shell-agnostic subprocesses.
The claude/status.sh hook drives an emoji on the tmux tab label reflecting Claude's current state:
| Emoji | State |
|---|---|
| 🤖 | Session started |
| ⚡ | Working |
| 🔐 | Awaiting permission |
| 📣 | Waiting for input |
| ✅ | Done |
Setup: the clone includes scripts/claude/; wire the hooks in ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "~/.agentmux/scripts/claude/status.sh start" }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "~/.agentmux/scripts/claude/status.sh working" }] }],
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "~/.agentmux/scripts/claude/status.sh working" }] }],
"Notification": [{ "hooks": [{ "type": "command", "command": "~/.agentmux/scripts/claude/status.sh notify" }] }],
"PermissionRequest": [{ "hooks": [{ "type": "command", "command": "~/.agentmux/scripts/claude/status.sh permission --notify 'Claude is waiting for permission'" }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "~/.agentmux/scripts/claude/status.sh done --notify 'Claude has finished working'" }] }]
}
}The --notify flag triggers a macOS notification via osascript. Remove it if you don't want desktop alerts.
The 3-row status bar shows a rolling done / now / next summary of the active session. agentmux.conf already wires status-format[1-3] to summary_rows.sh; you just need a local LLM endpoint and the Claude Code hooks above.
Requirements:
- A local OpenAI-compatible LLM endpoint with a small non-reasoning instruct model loaded (e.g. LM Studio at
localhost:1234withqwen2.5-14b-instruct, or Ollama atlocalhost:11434) agentmux.confsourced in~/.tmux.conf- Claude Code hooks wired (above) — the
workinghook triggers the summariser
The coloured status bar and 3 extra summary rows only appear in amux sessions (@autoagent=1). Plain tmux sessions are left unstyled with a single status line.
Pipeline (runs detached on every working hook):
claude/ctx.sh— extracts recent prose turns from the Claude Code transcriptclaude/digest.sh— compacts the session into a chronological digest (prose + mutating tool actions)summarise.sh(stand mode) — sends the digest to a local OpenAI-compatible endpoint; receives"<subject>. done: …; now: …; next: …"- Result written to
/tmp/agentmux-status-<pane_key>.txt summary_rows.sh(called by tmuxstatus-format[1-3]) splits that into three display rows
Override the endpoint or model with environment variables:
export AGENTMUX_LLM_URL=http://localhost:1234/v1/chat/completions
export AGENTMUX_LLM_MODEL=qwen2.5-14b-instruct
export AGENTMUX_LLM_TIMEOUT=20 # secondsNon-Claude agents: any agent can participate by writing to /tmp/agentmux-status-<pane_key>.txt directly (where pane_key=$(echo $TMUX_PANE | tr -d '%')). The format is a single line: <subject>. done: <text>; now: <text>; next: <text> — any of the done/now/next labels may be omitted.
Each session gets a status-bar colour seeded from a stable hash of its name, so the same project usually lands on the same colour with no config. The summary rows use a matching shade of the same hue, and colours update automatically on attach. The colour is frozen for a session's lifetime — it never moves while the session lives, regardless of what else starts or stops.
Because two names can hash to the same slot, a newcomer that collides de-dups onto the next free slot. Which one wins is launch-order-dependent, so two colliding projects can swap colours between runs. To make a project's colour fixed, pin it:
[amux.dirs."~/Developer/github.com/you/myproject"]
session_colour = "blue"A pinned colour is frozen for that directory and removed from the auto-assign pool entirely — no other project is ever assigned it, even when the pinned project isn't running. Run amux --colours for the bar colour names (this is the session bar palette, distinct from an agent's tab colour).

