agentlytics
Comprehensive analytics dashboard for AI coding agents โ Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigraviโฆ
๐ฎ Track all your AI coding agents (Claude Code, Codex, Cursor, ...) in tmux and jump to the one that needs you
git clone https://github.com/epilande/ccmux.gitepilande/ccmuxTrack all your AI coding agents (Claude Code, Codex, Cursor, ...) in tmux and jump to the one that needs you
When running multiple AI coding agent sessions across tmux panes, it's hard to keep track of which session is idle, which is waiting for permission, and which pane to switch to. ccmux solves this with a background daemon that monitors session activity and an interactive TUI that shows live session states at a glance.
It works with your existing tmux workflow. You don't change how you launch or run your agents; ccmux discovers what's already running in your panes, so as long as you're in tmux with a supported agent, it just works.
Built-in support for: Claude Code, Codex, Cursor, OpenCode, Pi, Antigravity, Copilot, Gemini CLI, plus custom agent definitions via config.
agents with a live list in the previewccmux invoke for scripted one-shot agent turnsbrew install epilande/tap/ccmux ccmux setup
Requires Bun.
git clone https://github.com/epilande/ccmux.git cd ccmux bun install bun link ccmux setup
ccmux setup installs agent hooks for authoritative session matching. ccmux works without it, but it's recommended; see Session Matching with Hooks. Bare ccmux setup only configures agents whose executable is found on PATH; use ccmux setup --agent <name> to install for a specific agent even if it isn't detected.
ccmux
Tip
Bind a tmux key so you can pop ccmux open from anywhere (add to ~/.tmux.conf):
# Prefix + C-p: open ccmux in a centered popup bind-key C-p display-popup -E -w 80% -h 75% "ccmux" # Or skip the prefix entirely (Alt+p from any pane) bind-key -n M-p display-popup -E -w 80% -h 75% "ccmux"
The picker exits after you select a session, so the popup closes itself and drops you straight into that pane. (display-popup requires tmux 3.2+.)
| Command | Description |
|---|---|
ccmux |
Launch interactive TUI picker (default) |
ccmux picker |
Launch TUI with options (--preview, --icons <style>) |
ccmux picker --persistent |
Dashboard mode (stay open after switching sessions) |
ccmux spawn [agent] |
Spawn a new agent session in a tmux pane |
ccmux invoke [agent] "prompt" |
Run a single agent turn and write the response to stdout (docs) |
ccmux invoke list |
List active and recently-finished invocations (-j for JSON) |
ccmux invoke cancel <id> |
Cancel a running invocation by id (idempotent) |
ccmux invoke result <id> |
Print an invocation's full captured output (subprocess agents only) |
ccmux show |
List all active sessions |
ccmux show --json |
Output sessions as JSON |
ccmux status |
Show daemon and session overview |
ccmux switch <id> |
Switch tmux client to a session's pane |
ccmux review [id] |
Review a session's diff with hunk (defaults to cwd) |
ccmux kill <id> |
Kill a session's process |
ccmux restart <id> |
Kill and resume a session |
ccmux send <id> <text> |
Send text to a session's tmux pane (multiline pastes as one message; --no-enter skips submit) |
ccmux screen [id] |
Capture pane content |
ccmux screen --grep <pattern> |
Search across all session panes |
ccmux dismiss <id> |
Remove a session from tracking |
ccmux worktree prune |
Remove worktrees whose work is finished (--dry-run, --state, --repo <path>) |
ccmux daemon start|stop|restart|status |
Manage the background daemon |
ccmux config set <key> <value> |
Set a preference |
ccmux config get <key> |
Get a single preference value |
ccmux config list |
List all preferences |
ccmux config themes |
List built-in themes (marks the active one) |
ccmux setup |
Install hooks for every supported agent found on PATH (Claude + Codex + Cursor + OpenCode + Pi + omp + Antigravity + Copilot) |
ccmux setup --agent <name> |
Limit install/uninstall/status to specific agent(s); forces install even if not found on PATH |
ccmux setup --status |
Report install state without writing anything |
ccmux setup --uninstall |
Remove hooks (preserves user-owned hook entries) |
ccmux debug |
Diagnose session tracking discrepancies |
ccmux notify [message] |
Send a notification via the configured backend (bare: test message + diagnostics) |
ccmux sidebar |
Launch narrow sidebar TUI (no preview/footer) |
ccmux sidebar --toggle |
Smart toggle: spawn/kill sidebars in every window across all tmux sessions |
The daemon starts automatically the first time you run a ccmux command (picker, show, invoke, etc.). It runs on 127.0.0.1:2269 and provides both a REST API and SSE event stream.
Press P to split the picker and preview the highlighted session's live pane content side by side. Press Tab to focus the preview and act in place: your keystrokes go straight to that agent's pane, so you can approve a permission, answer a question, or type a follow-up without ever leaving ccmux.
When the session has agents running, an Agents section lists each one with its runtime. Finished agents drop off the list.
hunk is a terminal diff reviewer. With hunk on your PATH, press d in the picker to review the selected session's working-tree diff without leaving ccmux: the picker suspends, hunk diff --watch takes over the pane in the session's repository root, and the picker resumes when hunk exits. The same action is available from the right-click context menu. If the working tree has no changes, ccmux reports that instead of opening an empty review.
To send review feedback back to the agent:
The offer relies on hunk's session JSON commands (hunk session list / session comment list). With an older hunk the review itself still works; the offer just doesn't appear.
The reviewHandback preference controls what happens when hunk exits:
confirm (default) asks before sending the prompt.auto sends and submits the prompt immediately without a dialog.fill pastes the prompt into the agent's composer without submitting it. The text remains there until you jump to the session and submit or edit it; a later send or invoke may find it prepended.The review also runs from the CLI:
ccmux review # Review the current directory's repository ccmux review <id> # Review a session's repository by id
Install hunk with brew install hunk. The d footer hint and help entry appear only when hunk is detected on PATH at launch.
A compact, always-visible session list that lives alongside your working panes. No preview panel, no footer, just status icons and project names.
ccmux sidebar --toggle # Toggle sidebars in all tmux windows ccmux sidebar --toggle --width 40 # Custom width (default: 30) ccmux sidebar --toggle --position right # Right side (default: left) ccmux sidebar --resize --width 30 # Snap every existing sidebar pane to <width>
The smart toggle fills gaps when some windows are missing sidebars, and kills all sidebars when every window already has one. New windows automatically get a sidebar, and sidebars snap back to their configured width when a window is resized.
Configure defaults so --toggle uses your preferred layout:
ccmux config set sidebar.width 40 ccmux config set sidebar.position right
Suggested tmux keybinding (add to ~/.tmux.conf):
bind-key S run-shell "ccmux sidebar --toggle"
Desktop notifications on waiting/finished transitions, disabled by default. When a session needs permission, or has a plan waiting for approval, the banner carries Approve / Deny buttons; permission, plan, question, and "finished" notifications also carry an inline Reply field, so you can answer, redirect, or send the next instruction without switching to its pane. Focusing a session's pane clears its notification.
| Permission โ Approve / Deny | Question โ inline Reply |
|---|---|
![]() |
![]() |
ccmux config set notifications.enabled true ccmux notify # sends a test notification and prints setup diagnostics
Actionable Approve/Deny buttons work for Claude Code, OpenCode, Codex, Cursor, Gemini CLI, Antigravity, Copilot, and oh-my-pi; Pi has no tool-approval pause, so it never raises a waiting notification. Inline Reply on waiting-state notifications (permission, plan, question) is Claude Code only; Reply on finished notifications works for every built-in agent. A reply the agent's composer would misread as a command (e.g. leading ! or /) is refused instead of typed, and the text comes back in a follow-up notification so it isn't lost. Approve/Deny work on macOS and Linux; inline reply needs a notification server that advertises it (always on macOS, varies on Linux).
For OpenCode, one server can host several sessions folded into a single row, so when more than one is waiting at once the buttons are withheld (the keystroke could land on the wrong session's dialog) and the notification is delivered informational-only.
macOS: the buttons, ccmux's own name and icon, per-session grouping, and retraction come from a helper app Homebrew installs alongside ccmux, so brew install epilande/tap/ccmux for the full experience. Source installs fall back to osascript (posts as Script Editor, silenced by Focus / Do Not Disturb, no buttons or reply). macOS never shows a permission dialog for a CLI-launched app, so grant it once by hand: run ccmux notify and follow the printed steps (open the settings deep link, find ccmux, enable Allow notifications, set Alert Style to Persistent), then re-run ccmux notify to confirm.
Linux: dbus grouping, click-to-jump, and Approve/Deny are native (no extra binary); inline reply appears only when the server advertises it. A headless daemon (SSH, systemd) needs DBUS_SESSION_BUS_ADDRESS, plus DISPLAY for the notify-send fallback.
Configure further with ccmux config set notifications.<key> <value>, or edit ~/.config/ccmux/ccmux.json directly:
{
"notifications": {
"enabled": true, // default false (opt-in)
"events": ["waiting", "finished"], // default both
"sound": "Glass", // false (default) | true (platform default sound) | macOS sound name
"delayMs": 1000, // debounce for "finished" only; "waiting" always fires immediately
"backend": "auto", // "auto" | "ccmux-notifier" | "osascript" | "notify-send" | "dbus" | "osc" | "command"
"command": "ntfy publish agents \"$CCMUX_TITLE: $CCMUX_BODY\"", // used when backend = "command"
},
}
backend: "auto" picks ccmux-notifier (else osascript) on macOS, and D-Bus (else notify-send) on Linux. command runs your own shell command with CCMUX_* env set (EVENT, SESSION_ID, AGENT, PROJECT, BRANCH, TITLE, SUBTITLE, BODY, PANE), for ntfy, Pushover, and the like. CCMUX_BODY is the complete text (the event line plus any context), so a script reading only it still gets something meaningful; CCMUX_SUBTITLE is the bare event line on its own for structured consumers.
The osc backend delivers notifications through the terminal stream instead of a desktop API, for daemons running on a remote box; see Remote / SSH.
Note
Approve/Deny only send the mapped keystroke to that session's pane (for Claude, the same key you'd press yourself). Approve on a plan picks "manually approve edits" (edits stay gated), never Claude's auto-accept mode. Reply on a permission or plan notification denies the pending tool/plan and sends your text as the next message (it cancels the prompt first, then types). If the session moved on since the notification fired, the press sends nothing and you get a fresh "state changed" notification instead; dismissing a notification never approves anything.
The keystrokes behind the buttons come from a per-agent notificationActions map, overridable per Custom Agents.
Press / to filter the list as you type. ccmux searches several sources at once and highlights why each row matched:
ccmx still finds ccmux.Prompts come from the daemon's in-memory index, which keeps the most recent prompts per session and is tail-bounded after a daemon restart (only recent prompts are re-read from disk). Transcript search closes that gap: it reads each session's transcript file on demand and covers the full session history, including assistant replies (Claude and Codex).
Three toggles control what gets scanned: searchPaneContent, searchPaneLines, and searchTranscript (see Configuration).
Launch new agent sessions directly from the CLI:
ccmux spawn # Spawn claude (default) in a new tmux window
ccmux spawn codex # Spawn a specific agent
ccmux spawn --split # Split current pane instead of new window
ccmux spawn --split h # Split left/right ('v' is the stacked default)
ccmux spawn --target %12 # Split (or place the window next to) a specific pane
ccmux spawn --detach # Don't switch to the new pane
ccmux spawn --cwd ~/proj # Set working directory
ccmux spawn --resume <id> # Resume an existing session
ccmux spawn --fork <id> # Branch an existing session into a new one
ccmux spawn --prompt "fix the tests" # Send an initial prompt
ccmux spawn --worktree --prompt "fix flicker" # Spawn into a git worktree (name derived from the prompt)
ccmux spawn --worktree fix-flicker # Spawn into a named worktree, creating it if needed
ccmux spawn --worktree --base develop --prompt "fix flicker" # Branch the new worktree from develop
Split directions use tmux's own vocabulary: h puts the new pane beside the
old one, v stacks it below. Run inside tmux, ccmux spawn uses the pane you
ran it from, so the new pane or window lands in your session rather than
wherever the daemon happens to consider "current". A new window is appended at
the end of your session, which leaves every existing window index alone; pass
--target <pane-id> to insert one directly after that pane's window instead
(tmux renumbers the windows after it), or --target none to let tmux place it.
Targeting a pane in a different tmux session creates the pane there but does
not move you to it, so pair that with --detach
(see #75).
--prompt starts the agent interactively with the prompt already submitted.
It is supported for the agents whose interactive-with-prompt invocation ccmux
has verified; for anything else (including custom agents) ccmux refuses the
spawn rather than guessing a flag, and you can teach it the right shape with
promptCommand in your agent config.
--worktree [name] spawns the agent into a git worktree at
<main>/.claude/worktrees/<name>, creating it first if it doesn't exist yet.
An explicit name is create-or-open: spawning into the same name again reuses
that worktree rather than failing. Without a name, ccmux derives one from
--prompt's opening words; a derived name that collides with an existing
worktree gets a numeric suffix (-2, -3, ...) instead of reusing it, since
two different prompts landing in the same worktree would silently merge
unrelated work. --base <ref> sets what the new branch is cut from,
defaulting to the main checkout's current branch.
Sometimes the interesting question is "what if it had gone the other way". Fork starts a second session that continues an existing conversation's history, in a pane beside the original, and leaves the original running and untouched. The agent is heading down path A; fork it and try path B side by side. (A source with no pane of its own gets a new window instead.)
F in the picker, or Fork in a session's context menu, does it,
and places the new pane beside the source's own. ccmux spawn --fork <session-id> forks the same way but places the result like every other
ccmux spawn, relative to the pane you run it from. Either way the new pane
is tracked like any other session, with its own row, state and id.
The fork always starts in the source's directory. Claude looks a resumed
session up under the project directory for the current working directory, so
a fork somewhere else would find no conversation at all; --cwd pointing
elsewhere is refused rather than silently opening an empty shell.
Fork needs two things, and the picker hides the action when either is missing:
the agent has to declare how it forks (forkCommand), and ccmux has to know
which conversation the pane holds. For most agents that knowledge comes from
hooks, so run ccmux setup if the action isn't offered.
Today Claude Code is the only agent that ships a fork command, because it
is the only one whose behavior when resuming a still-running session has been
verified. Adding another is one config line once you have checked it yourself
(see docs/agent-adapters.md).
Agents create git worktrees faster than anyone cleans them up, and the ones where work actually happened are the ones that stick around. After a branch is merged (and auto-deleted on GitHub), three leftovers stay on your machine: the worktree directory, the local branch, and often a tmux pane with a finished agent in it.
W in the picker (or Prune Worktrees on a group header, or ccmux worktree prune) lists the worktrees whose work is finished, and why each one is removable:
| Reason | Meaning |
|---|---|
PR merged |
GitHub says the branch's PR was merged (survives squash/rebase merges) |
merged locally |
The branch tip is an ancestor of the default branch |
upstream gone |
The branch had an upstream and it's gone after a fetch --prune |
PR closed |
The PR was closed without merging; the branch is kept |
Removing a worktree deletes its directory, attempts to delete the local branch, closes the leftover pane once its agent has exited, prunes git's metadata, and drops the directory's entry from ~/.claude.json. Branch deletion follows the evidence rather than the reason: a merged PR is force-deleted (git branch -D, since a squash merge leaves the tip unmerged by git's definition), merged locally and upstream gone use the safe git branch -d and report a refusal if git says the branch still holds unmerged work, and PR closed keeps the branch entirely.
Safety rules, in short: a worktree with any bound session, working, idle, or waiting, is never offered, a branch still sitting on a base's tip is never classified as merged, nothing is pre-selected, dirty worktrees (uncommitted or untracked changes) need their own D opt-in on top of being selected and are re-checked immediately before deletion, a worktree.symlinkDirectories symlink does not count as dirty (it is setup, not your work), gitignored files that would be deleted are listed before you confirm, and the main checkout is never a candidate.
Each directory is renamed to a .ccmux-trash-<name>-<timestamp> sibling before being deleted, so the path frees immediately and the contents survive for the length of the run. If ccmux is interrupted mid-run, look for that directory next to where the worktree was: mv .ccmux-trash-<name>-<timestamp> <name> restores it, and git worktree repair <name> re-links it to the repo.
ccmux worktree prune # Interactive confirm list ccmux worktree prune --dry-run # Show what would go, change nothing ccmux worktree prune --state # Also drop agent state entries (see below) ccmux worktree prune --repo ~/p # Limit to one repository
--state removes ~/.claude.json entries for any recorded directory that does not exist right now, not only former worktrees, so an ordinary repo you have not checked out will be dropped too. Entries whose parent directory is also missing are skipped, which keeps an unmounted external drive or a disconnected network share from taking every project on it with them. The removed paths are printed, and the file is copied to ~/.claude.json.ccmux-backup-<timestamp>-<pid> first (the newest three are kept).
There is no --yes and no automatic mode; removals are always confirmed interactively. --dry-run changes nothing on disk, though it still runs git fetch --prune per repo, which is what makes the upstream gone signal visible and does update remote-tracking refs.
ccmux invoke runs a single agent turn and writes the response to stdout, so you can use real agents in shell pipelines and scripts. See docs/invoke.md for the full reference.
ccmux invoke claude "say hi in one word" echo "what is 2 + 2" | ccmux invoke claude git diff main | ccmux invoke claude "Review this diff"
Claude runs interactively in a dedicated tmux session and returns clean text parsed from the transcript JSONL. Codex, Cursor, OpenCode, Pi, oh-my-pi, Antigravity, Copilot, and Gemini run as non-interactive subprocesses (codex exec -o, cursor-agent --print, opencode run --format json, pi -p, omp -p, agy -p, copilot -p --allow-all-tools, gemini -p) and return the agent's clean response text.
For orchestration, name an invocation with --id <id>, then use ccmux invoke list, ccmux invoke cancel <id>, and ccmux invoke result <id> to watch, cancel, or read its full captured output by that id. See docs/invoke.md for the fire-and-poll reference.
This repo ships a dispatch Agent Skill that teaches your coding agent to orchestrate other agents through ccmux invoke (firing, fan-out, joining, cancelling, and reading worker output). For Claude Code it installs as a plugin (this repo doubles as a plugin marketplace):
/plugin marketplace add epilande/ccmux
/plugin install ccmux@ccmux
Other skills-capable agents (Codex, Cursor, OpenCode, and others) can use the same skill by copying it into their skills directory. The skill is additive glue for the ccmux CLI, which must be installed and on your PATH. See plugins/ccmux/README.md for details.
| Action | Key | Description |
|---|---|---|
| Navigate | j / k or โ / โ | Move through session list |
| Jump to first/last | gg / G | Go to top / bottom |
| Jump to session | 1โ9 | Switch directly to session N |
| Switch to session | Enter | Switch tmux to the selected pane |
| New session | n | Open the new-session dialog (agent, placement, prompt, worktree; directory derived from the selected row) |
| Search | / | Enter fuzzy search mode |
| Toggle preview | P | Show/hide the preview panel |
| Scroll preview | Ctrl+D / Ctrl+U | Half-page scroll in preview |
| Resize preview | Alt+H / Alt+L | Increase/decrease preview width |
| Focus preview | Tab | Send keys directly to tmux pane |
| Restart session | r | Kill and resume the selected session |
| Reconnect | R | Reconnect to the daemon SSE stream |
| Kill session | x | Kill the selected session's process |
| Kill all | X | Kill all tracked sessions |
| Fork session | F | Branch the conversation into a pane beside it, leaving the original running |
| Prune worktrees | W | Open the prune list for finished worktrees (multi-select, confirmation) |
| Review and hand back | d | Review with hunk, then offer to send notes to the agent (requires hunk on PATH) |
| Collapse/expand | h / l or Space | Toggle group collapsed state |
| Move group | J / K | Reorder group down / up (persisted) |
| Move group top/bottom | < / > | Pin group to top / bottom |
| Collapse/expand all | zM / zR or - / = | Collapse or expand all groups |
| Hide idle | f | Toggle hiding idle sessions |
| Cycle prompt | p | Prompt display: inline โ own row โ off |
| Cycle group-by | b | Cycle through group-by modes |
| Help | ? | Show keyboard shortcuts overlay |
| Quit | q / Esc | Exit the picker |
Opened with n, or from the right-click menu on a session row or a group header. Every field has a default, so n Enter spawns straight away.
| Action | Key |
|---|---|
| Next / prev field | Tab / Shift+Tab |
| Move within a field | j / k or โ / โ |
| Pick by number | 1โ9 |
| Where it runs | 1 This checkout / 2 New worktree |
| Spawn | Enter |
| Cancel | Esc |
Movement and number keys apply to the focused field, so 2 picks the second agent on the Agent field and the second placement on the Placement field. In the Prompt field every key is text, so โ / โ (or Ctrl+P / Ctrl+N) move between fields there instead.
Where picks between this checkout and a new git worktree. The worktree option carries the name it would create, derived from the prompt and updated as you type, and its branch is cut from the main checkout's current branch. Choosing a different base ref is CLI-only (ccmux spawn --worktree --base <ref>).
The working directory is derived, not typed: a session row uses that session's directory, a group header uses the group's, and no selection falls back to where the picker was launched. The picker jumps to the new pane, and a one-shot picker then closes while a --persistent board stays open; the sidebar spawns into the window's main area without stealing focus.
| Action | Key |
|---|---|
| Navigate results | โ / โ or Ctrl+N / Ctrl+P |
| Select | Enter |
| Cancel | Esc |
When preview is focused (Tab), keystrokes are forwarded to the tmux pane. These keys still work:
| Action | Key |
|---|---|
| Navigate sessions | Ctrl+N / Ctrl+P |
| Resize preview | Alt+H / Alt+L |
| Scroll preview | Ctrl+D / Ctrl+U |
| Exit focus | Tab / Esc |
Preferences are stored in ~/.config/ccmux/ccmux.json and can be managed with:
ccmux config set <key> <value> ccmux config get <key> ccmux config list
| Key | Values | Default | Description |
|---|---|---|---|
iconStyle |
dot, emoji, nerdfont, none |
dot |
Status icon style |
theme |
catppuccin-*, tokyo-night*, dracula, gruvbox-*, nord, rose-pine* |
catppuccin-mocha |
TUI color theme (resolved at launch; see Theme) |
showPreview |
true, false |
false |
Show preview panel on launch |
previewWidth |
20โ80 |
40 |
Preview panel width (percentage) |
command |
any non-blank string | claude |
CLI command used for session restart |
groupBy |
project, cwd, session, window, none |
project |
How sessions are grouped in the TUI |
promptDisplay |
inline, row2, off |
inline |
Prompt display: inline on row 1, its own row, or hidden |
backgroundAgents |
true, false |
true |
Show Claude background agents as rows (daemon restart required) |
additionalClaudeConfigDirs |
array of paths | [] |
Additional Claude config dirs to watch (daemon restart required; see Multiple Claude Config Dirs) |
searchPaneContent |
true, false |
true |
Include captured pane content in TUI search |
searchPaneLines |
10โ500 |
100 |
Lines of pane content scanned in TUI search |
searchTranscript |
true, false |
true |
Search live Claude/Codex transcripts (full history + assistant text) via the daemon |
persistent |
true, false |
false |
Keep picker open after switching sessions (dashboard mode) |
reviewHandback |
confirm, auto, fill |
confirm |
After a hunk review, confirm delivery, send immediately, or fill the agent composer without submitting |
sidebar.width |
10โ80 |
30 |
Sidebar pane width in columns |
sidebar.position |
left, right |
left |
Which side of the window to place the sidebar |
For how these search knobs interact, see Search Mode.
Each session item has up to two rows (row1, row2), and each row has a left and right side. Each side is a comma-separated list of field entries. An entry is either <field> (use the field's default mode) or <field>:<mode> (override the mode).
ccmux config set columns.row1.left "index,status:icon,project" ccmux config set columns.row1.right "agent:short,pane,time" ccmux config set columns.row2.left "prompt" ccmux config set columns.row2.right "branch"
Pass an empty string to clear a side: ccmux config set columns.row2.left "".
| Field | Modes | Default mode | Description |
|---|---|---|---|
index |
โ | โ | Row number (1โ9) |
status |
icon/short/full |
icon |
Status badge style |
project |
dirname/full |
dirname |
Project path (basename or full); a worktree renders as <repo>/<worktree> in both modes |
agent |
short/full |
full |
Agent name (2-char code or full label) |
version |
โ | โ | Agent version |
pane |
โ | โ | Tmux pane target (session:window.pane) |
time |
โ | โ | Relative time since last input |
prompt |
โ | โ | Last user prompt (truncated) |
cwd |
โ | โ | Working directory |
branch |
โ | โ | Git branch, suffixed + in a worktree |
pr |
short/full |
full |
Open PRs for the branch (#25/PR #25) |
The project cell reads path:branch, and a session running in a git worktree marks it twice: the branch gains a trailing + (also on the standalone branch column), and the path is replaced by <repo>/<worktree> โ ccmux/parking rather than the worktrees/parking the directory happens to spell, so worktrees of different repos stay distinguishable. Both survive the dirname mode, since they are identity rather than path context; when the cell is too narrow for both, the repo yields before the worktree's own name does.
Defaults: row1.left is index, status, project (status badge widens iconโshortโfull as the terminal grows). row1.right cascades by breakpoint: just pane below xs, then agent:short, pane at xs, agent:short, pane, time at sm, and agent:full, version, pane, time at md+. The prompt and pr cells are configured on row2, but promptDisplay (default inline, cycled live by p) controls how they render: inline flattens them onto row1 so each session stays a single line, row2 gives the prompt its own line with pr at the right edge, and off hides both. Sessions with no prompt stay single-line in inline mode; in row2 mode the second line still appears when another row-2 field (such as an open PR) has data.
Sidebar defaults differ to fit the narrow rail: row1 is status, project with pr:short, agent:short on the right (PR stays visible even with the prompt hidden), and row2 is prompt / time (a lone time never earns the row; it rides along when some other field has data). The 30-col rail has no room to inline, so the sidebar always uses the two-row layout (inline behaves like row2). Override these under the sidebar.columns key in ~/.config/ccmux/ccmux.json (e.g. "sidebar": { "columns": { "row2": { "left": ["pane"] } } } to bring the pane target back).
The CLI's comma-separated form sets one mode per entry. To vary the layout by terminal width (responsive cascade), edit ~/.config/ccmux/ccmux.json directly and use the default/xs/sm/md/lg keys on either a row side (whole array) or an entry's mode.
Named breakpoints control when responsive column layouts activate. A breakpoint value applies from that terminal width upward until a larger breakpoint overrides it.
| Name | Default width |
|---|---|
xs |
40 |
sm |
60 |
md |
80 |
lg |
100 |
ccmux config set breakpoints.sm 55 ccmux config set breakpoints.lg 120
The TUI ships 14 built-in palettes across six families, resolved once at launch (no in-TUI toggle).
ccmux config themes # list built-ins, mark the active one ccmux config set theme tokyo-night # switch theme
| Theme | Background |
|---|---|
catppuccin-mocha |
dark (default) |
catppuccin-macchiato |
dark |
catppuccin-frappe |
dark |
catppuccin-latte |
light |
tokyo-night |
dark |
tokyo-night-storm |
dark |
tokyo-night-day |
light |
dracula |
dark |
gruvbox-dark |
dark |
gruvbox-light |
light |
nord |
dark |
rose-pine |
dark |
rose-pine-moon |
dark |
rose-pine-dawn |
light |
For per-key tweaks, set theme to an object in ~/.config/ccmux/ccmux.json: a built-in base plus colors (the 14 semantic keys) and/or ansi (the 16 terminal colors used to render the preview), deep-merged over the base.
{
"theme": {
"base": "catppuccin-mocha",
"colors": { "red": "#ff5555" },
"ansi": { "brightBlack": "#585b70" }
}
}
An unknown base name falls back to the default theme; an invalid hex value or unknown override key is dropped and the base value is kept. Each emits a warning. Run ccmux config themes to inspect any problems with the current config.
Note
ccmux paints no background fill, so theme colors sit on your terminal's own background. The light palettes (catppuccin-latte, tokyo-night-day, gruvbox-light, rose-pine-dawn) assume a light terminal; pair them with a light background. Every other palette assumes a dark one.
Claude Code writes session transcripts to $CLAUDE_CONFIG_DIR/projects (default ~/.claude/projects), so sessions from a second account (e.g. a personal login launched with CLAUDE_CONFIG_DIR=~/.claude-personal) land in a tree ccmux doesn't watch by default. List those dirs in additionalClaudeConfigDirs and a single daemon watches every <dir>/projects tree:
ccmux config set additionalClaudeConfigDirs '["~/.claude-personal"]' ccmux setup --agent claude # installs hooks into every configured dir ccmux daemon restart
~/.claude is always watched; entries are additional config dirs (~ paths supported), and a set CLAUDE_CONFIG_DIR environment variable is picked up automatically. Sessions are keyed by their globally unique session ID, so the same project opened under two accounts coexists without collision.
Note
If you add a dir later, re-run ccmux setup --agent claude. The daemon warns at startup about any configured dir still missing hooks.
For reliable session-to-pane mapping (especially with multiple sessions of the same agent in the same project), install hooks:
ccmux setup # Install hooks for every supported agent found on PATH ccmux setup --agent codex # Limit to a single agent (installs even if not on PATH) ccmux setup --status # Report install state without writing ccmux setup --uninstall # Remove hooks
Hooks write PID marker files under ~/.config/ccmux/session-pids/ whenever a session starts or begins its first invocation, a turn completes, or the agent asks the user to approve a tool. The daemon picks up the markers in real time via a filesystem watcher. See docs/architecture.md#hook-lifecycle for the full flow (marker writes, chokidar dispatch, per-agent correlation).
Gemini CLI is tracked through process detection and terminal pattern matching, so it needs no setup.
Uses Claude's native hooks in ~/.claude/settings.json with three scripts under ~/.claude/hooks/:
ccmux-session-start.sh: writes the marker on session create/resumeccmux-session-end.sh: removes the markerccmux-state-notify.sh: updates state on idle_prompt / permission_promptUses Codex's native hooks (~/.codex/hooks.json plus the codex hooks feature flag in ~/.codex/config.toml, which is [features] codex_hooks = true pre-0.124 and [features] hooks = true on 0.124+; ccmux recognizes either) with three scripts under ~/.codex/hooks/:
ccmux-session-start.sh: writes the marker when a Codex session startsccmux-stop.sh: refreshes the marker at the end of every turnccmux-permission-request.sh: marks the session as waiting_permission when the user is asked to approve a toolTool-approval detection (PermissionRequest) needs Codex >= 0.122.
Uses Cursor's native hooks (~/.cursor/hooks.json) with four scripts under ~/.cursor/hooks/:
ccmux-session-start.sh: writes the marker on fresh chat launchccmux-session-end.sh: unlinks the marker when the chat endsccmux-before-submit-prompt.sh: flips state to working and records the last prompt (1 KB cap)ccmux-stop.sh: refreshes state back to idle at turn completionRequires cursor-agent >= 2026.1.16 (when the hooks feature landed).
Uses OpenCode's plugin system rather than shell hooks. ccmux setup --agent opencode drops a single auto-discovered JS plugin at ~/.config/opencode/plugin/ccmux.js (honors $XDG_CONFIG_HOME). The plugin subscribes to OpenCode's in-process event bus and writes a marker for every session on the server:
session.created / session.updated: marker with directory + titlesession.status (busy/retry/idle): refreshes state to working or idlemessage.updated / message.part.updated: captures the user's last prompt (1 KB cap) into the marker (parity with Claude/Codex/Cursor)permission.asked / permission.replied: flips state to waiting_permission with the pending tool, clears back to working on replysession.deleted: unlinks the markerBecause one OpenCode server can host many sessions, the daemon folds all markers sharing a server PID into the single ccmux Session for the tmux pane that hosts the server. Status is worst-of (waiting > working > idle); cwd and nativeSessionId come from the newest-activity marker, while pendingTool and the attention indicator come from the newest-waiting marker.
Both use Pi's extension system rather than shell hooks (oh-my-pi, omp, is a hard fork of Pi that kept the extension API). ccmux setup --agent pi / --agent omp drops a single auto-discovered JS extension at ~/.pi/agent/extensions/ccmux.js or ~/.omp/agent/extensions/ccmux.js. The extension subscribes to the agent's lifecycle events and writes one marker per session:
session_start: marker with the session id, transcript path, and cwd (fired at launch, so the marker carries full identity immediately)before_agent_start: captures the user's last prompt (1 KB cap)agent_start / agent_end: flips state to working / idle (these bracket one full user prompt, so the row never flickers mid-response the way per-turn events would)session_shutdown: unlinks the markeromp additionally handles the in-place session swaps that change the session id (session_switch for /new and /resume, session_branch for /branch and fork): each reaps the old session's marker and seeds a fresh one for the new id, since omp mutates the session in place rather than emitting a shutdown/start pair.
Both run one session per process, so there's no server-style aggregation; the daemon correlates the marker's PID to its tmux pane via process ancestry and links nativeSessionId.
Approvals are where the two diverge. Unlike Pi, omp gates tool calls behind an Approve/Deny prompt, so its extension also tracks tool_approval_requested (flips to waiting_permission with the gated tool's name) and tool_approval_resolved (clears back to working once the last outstanding approval is answered; approve and deny both resume the loop). omp rows show a real waiting state and get actionable Approve/Deny buttons; Pi rows never raise one. omp emits these events only when an approval mode is configured; on its default yolo mode nothing is gated, and installing the ccmux extension does not change that either way.
Uses Antigravity's global named-hook config at ~/.gemini/config/hooks.json with two scripts under ~/.gemini/config/hooks/:
ccmux-preinvocation.sh: creates or refreshes the marker as working before each model invocationccmux-stop.sh: refreshes the marker as idle when the execution loop stopsAntigravity exposes no session-start hook, so a fresh idle session remains pane-tracked until its first prompt. ccmux deliberately does not install PreToolUse: in Antigravity v1.1.1, an empty {} response silently denies the tool call. Permission attention instead comes from the native permission dialog detected in pane content.
Drops one hooks file plus its marker script into Copilot's auto-discovered ~/.copilot/hooks/ dir (ccmux-copilot.json and ccmux-copilot.sh), registering observational events only:
sessionStart: writes the marker (working if the session launched with an initial prompt, else idle)userPromptSubmitted: flips the marker to workingnotification: flips to waiting when the payload is a permission or elicitation dialog (other notification types are ignored)agentStop: flips back to idlesessionEnd: removes the markerccmux deliberately does not install Copilot's permissionRequest hook: it is a deciding hook whose output can allow or deny the tool call. Permission attention is observed through notification instead. Copilot's events.jsonl is also tailed as a log source (it flushes in real time, including the mid-wait permission.requested), and its held-open session.db backs no-hooks native-id discovery.
Without hooks, the daemon does not use historical session IDs to claim pane ownership. It creates pane-scoped sessions from live process + tmux discovery, then attaches agent log metadata only when it can safely tie a log to the running process.
The built-in agents are the happy path: they ship with hook integration for authoritative session matching. If you run an agent ccmux doesn't support out of the box, you can teach it one in ~/.config/ccmux/ccmux.json. Custom agents fall back to process matching plus terminal pattern scanning (no hooks), so detection is less precise than a built-in, but it gets unsupported agents onto the board.
{
"agents": {
"myagent": {
"processMatch": "myagent",
"terminalRules": [
{
"matchAny": ["thinking...", "esc to interrupt"],
"status": "working"
},
{
"matchAll": ["approve?", "[y/n]"],
"status": "waiting",
"attentionType": "permission",
"pendingTool": "Command"
}
],
"resumeCommand": "myagent resume {id}",
"promptCommand": "{bin} '{prompt}'"
}
}
}
promptCommand is what ccmux spawn --prompt types into the new pane. It
must start an interactive session with the prompt submitted, not a
one-shot/print run. {prompt} is the prompt text and has to stay wrapped in
single quotes, because that is the quoting ccmux escapes for. ccmux reads the
template the way sh does and refuses it unless every {prompt} lands in a
real single-quoted context with the template's quotes balanced, so an unquoted
or double-quoted placeholder is rejected, and so is one whose single quotes sit
inside double quotes (sh -c "agent '{prompt}'"), where ' is just an
ordinary character and the escaping would do nothing. The optional {bin}
resolves to the agent's launcher, so a wrapper binary or executable override
survives.
You can also override built-in agent settings by using the agent's name as the key (e.g., "claude", "codex"). An override of notificationActions (the notification button/reply keystroke map) replaces the whole map, it is not merged key by key; it also controls the reply surfaces (replyOnQuestion, replyOnFinished, permissionReplyPrelude, the plan* keys, and the unsafeReplyPattern reply guard, written as a regex string like readyPattern), so any key you leave out is dropped rather than inherited from the built-in default. Copy across every key you still want when you override it. The one exception is unsafeReplyPattern: it is carried forward from the built-in as a safety default even when your override omits it, so a partial override can't accidentally re-enable unapproved shell execution through a reply. To disable it on purpose, set an explicit never-match pattern (e.g. "/(?!x)x/").
| Field | Required | Description |
|---|---|---|
processMatch |
Yes* | Regex to match the process executable |
commandPatterns |
No | Additional regex patterns to match full commands |
terminalRules |
No | Ordered terminal matching rules |
versionCommand |
No | Command to get agent version |
versionPatterns |
No | Regex patterns to extract version from output |
resumeCommand |
No | Command template for restarting ({id} placeholder) |
promptCommand |
No | Command template for spawn --prompt ({prompt} placeholder, single-quoted) |
forkCommand |
No | Command template for Fork / spawn --fork ({id} placeholder, optional {bin}) |
sessionFilePattern |
No | Regex to extract session ID from log filenames |
executable |
No | Command used to launch the agent (defaults to key) |
hooks |
No | { type } (built-in override only; internal) |
notificationActions |
No | Notification button/reply keystroke map (built-in override only; whole-map replace) |
* Required for new agents; optional when overriding built-in agents.
Invoke-related fields (invokeMode, errorRules, readyPattern) are documented in docs/invoke.md.
Each terminalRules entry must define exactly one matcher:
matchAny: matches when any string is present in the last 30 lines (case-insensitive)matchAll: matches only when every string is present in the last 30 lines (case-insensitive)Rules are evaluated top-to-bottom, and the first match wins. This lets you express broad "working" prompts and more specific multi-line waiting prompts without detector-specific logic.
ccmux tracks the sessions on the machine where it runs, so for a remote devbox, run everything there: install ccmux, tmux, and your agents on the remote host, run ccmux setup, and attach over SSH. Detection, hooks, the picker, and the sidebar all work at full fidelity because nothing crosses the SSH boundary; your terminal is just the window into it.
The one piece that doesn't follow automatically is desktop notifications: the remote daemon has no desktop to deliver to. The osc notification backend covers this by writing a notification escape sequence into the session's tmux pane, so it rides the terminal stream, SSH included, and renders as a banner in the emulator you're sitting in front of. Opt-in only (never picked by auto) and informational only: no buttons, reply, sound, or retraction, and paneless background sessions are skipped. Kitty clients get OSC 99, everything else OSC 9 (title: body); supported by Ghostty, iTerm2, and WezTerm (OSC 9) and Kitty (OSC 99), silently ignored by Apple Terminal and Alacritty.
# on the remote host ccmux config set notifications.enabled true ccmux config set notifications.backend osc tmux set -g allow-passthrough on # add to tmux.conf to persist ccmux notify # test: a banner should appear locally
ccmux has three layers: agents running in tmux panes, a background daemon that observes them, and clients (TUI + CLI utilities) that consume daemon state over HTTP/SSE.
The daemon merges three signals into one session state: log parsing for agents that write JSONL transcripts, terminal pattern matching for agents that don't, and PID marker files written by hook adapters for authoritative session-to-pane mapping. It exposes a local HTTP API with SSE streaming on port 2269. The TUI connects as an SSE client and renders state reactively using Solid.js via the @opentui/solid framework.
For deeper internals (status detection cascade, session-to-pane binding, hook event lifecycle, PR enrichment, background agents, code map), see docs/architecture.md. Per-agent hook quirks and the agent-owned files ccmux reads are in docs/agent-adapters.md.
| State | Meaning |
|---|---|
| idle | Waiting for user input |
| working | Processing (thinking, running tools, subagents) |
| waiting | Needs attention: permission, plan approval, or question |
The status machine derives state from JSONL log entries, tracks pending tool IDs for parallel tool calls, and checks process liveness to detect crashed sessions.
bun install # Install dependencies bun run dev # Run with --watch bun run typecheck # Type check bun test # Run tests bun run build # Bundle to dist/index.js (consumed by the launcher)
Set CCMUX_PERF=1 to enable performance instrumentation:
CCMUX_PERF=1 ccmux picker 2>/tmp/ccmux-perf.log
This outputs a startup waterfall and periodic runtime stats (FPS, memo recomputes, active timers) to stderr.
MIT
more like this
Comprehensive analytics dashboard for AI coding agents โ Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigraviโฆ
meine ๐ - A CLI file manager and system utility built with Textual. It combines intuitive command parsing with rich tโฆ
Skill Claude Code de veille tech francophone. Agrรจge les flux RSS (Journal du Hacker, Human Coders Newsโฆ) et produit unโฆ
search projects, people, and tags