neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
Fleet command for your Claude Code agents. Open source, self-hosted, made in the USA.
Developer toolsJavaScript · TapeGitHub @xxyjoelOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
BlueArch Mission Control — keyboard-first terminal TUI to monitor & run dozens of Claude Code agents: 10 live at once, every session saved and resumable.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Run a fleet of Claude Code agents. Know what every one of them costs.
A keyboard-first terminal TUI for real claude CLI agents —
10 live by default and up to 64, every conversation saved and brought back with one command,
with spend measured per session and hard limits that refuse to go past them.
No GUI. No Electron. No prefix keys.
Designed and built by BlueArch ,
a cloud governance and efficiency company.
We make the cost of running software visible and controllable. This is that, for AI development.Read the full README ↗This is not a mockup. Every session is a real claude CLI process in its own
pseudo-terminal. Tokens, context and git status are read from claude's own session
files and from git itself; cost is computed from those measured token counts against
a published per-model rate table, and a rate we have not verified is marked as an
estimate on the card rather than quietly presented as fact. Mission Control stores
nothing off your machine and reports nothing to us: no telemetry, no analytics, no
BlueArch account, and nothing listening on a port. btop × lazygit aesthetic, built on
Ink (React for terminals) with no build step.
Bring the whole fleet back. Close the terminal, reboot, come back tomorrow —
:resume-all restarts every session that was open, each one reattached to its own
conversation through claude --resume, in its own folder and branch, on the model it
was using, with the tokens and dollars it had already spent carried forward. Not empty
terminals in the right directories. The actual conversations.
See the bill while it runs. Every session shows its own token counts, context pressure and running cost, derived from the usage claude records for each turn and priced against a per-model rate table. Sub-agent and workflow fan-out is attributed back to the session that spawned it, so one agent spawning five still gives you one honest number.
Stop a runaway agent, don't just watch it. :cap <slot> <usd> refuses to send
another message once a session reaches its limit. :budget <usd> refuses new launches,
resumes, :clear / :restart and :cr once the fleet reaches its daily figure. Both act on the
next message or launch: a turn that is already running finishes, so the final total can
end up a little past the figure. These refuse work; they do not just report it afterwards.
Never hunt for the stuck one. Status comes from Claude Code's own lifecycle hooks, not from watching a pane go quiet, so a session blocked on a permission prompt reads as needing you rather than as idle. Answer it with one key from the grid.
Find the agents you forgot you were paying for. :bg lists the conversations
claude is running outside your fleet — the ones a force-closed terminal left behind,
still open, still holding a session id you cannot resume. Measured on one machine:
six of them, the oldest blocked for 9.8 days, together holding 1875 MB across 13
processes. Two keystrokes clears one.
Nothing leaves your machine. No analytics, no account with us, no service listening on a port. The tool ships five runtime dependencies and none of them is an HTTP client or an analytics SDK.
Mission Control isn't a generic multiplexer or a native app — it's a purpose-built TUI that understands Claude Code agents. It happily runs alongside tmux and over SSH.
Each row below describes what the three tools document about themselves; the Mission Control column is the one we can prove from this repository.
| tmux | cmux | Mission Control | |
|---|---|---|---|
| What it is | terminal multiplexer | native macOS GUI app | terminal TUI |
| Runs anywhere (SSH, any terminal) | ✅ | local macOS | ✅ |
| Purpose-built for Claude Code agents | ❌ | ✅ | ✅ |
| Per-agent cost / context / token tracking | ❌ | ❌ | ✅ |
Sub-agent fan-out awareness (⋔) | ❌ | ❌ | ✅ |
| Approval / permission routing | ❌ | partial | ✅ |
| Resume all active sessions | ❌ | ❌ | ✅ |
| Broadcast slash commands to all agents | ❌ | ❌ | ✅ |
| No prefix keys, zero config to start | ❌ | ✅ | ✅ |
| No GUI / no Electron | ✅ | native (no Electron) | ✅ |
| Open source & self-hosted | ✅ | ✅ | ✅ |
Requires Node 20+ and platform build tools for the PTY layer (macOS:
Xcode Command Line Tools; Linux: build-essential).
# Run it now, no install:
npx @bluearch/mission-control
# …or install the `mc` command globally:
npm install -g @bluearch/mission-control
mc
npm ≥ 11.19 (allow-scripts): newer npm blocks packages' install scripts until you allowlist them. mc self-heals at boot if its postinstall was skipped, but for a fully clean install allow the three that matter once:
npm config set allow-scripts=@bluearch/mission-control,node-pty,esbuild --location=user(mc fixes node-pty's spawn-helper permissions; node-pty builds its native PTY binding; esbuild fetches the binarytsxruns on.)
Installing from the repo also works (
npx github:xxyjoel/ba-mission-control) and tracksmaininstead of the released version. A Homebrew tap is planned.
Prefer to hack on it? See From source below and CONTRIBUTING.md.
Seven built-in palettes ship out of the box — BlueArch (default), Tokyo Night,
Gruvbox Dark, Catppuccin Mocha, Solarized Dark, Amber (CRT), and the
green-phosphor Matrix theme. Switch live with :theme <name> (e.g.
:theme matrix) or in Settings → Colors. Self-hosted means it's yours to
re-theme — palettes live in tui/lib/themes.js.
macOS (uses POSIX signals for pause/resume — Linux works too; Windows untested)
Node 20+
claude CLI on $PATH (Claude Code 2.x). Verify with claude --version.
A signed-in account — either ANTHROPIC_API_KEY, or a stored OAuth
session from claude auth login. Mission Control runs claude auth status on startup and prints the account it found:
[mc] claude: 2.1.142 (Claude Code)
[mc] auth · [email protected] · max plan · claude.ai
The same info shows as a chip in the top-right of the header strip and
can be re-probed at any time with :whoami (alias :auth). If the
banner says not signed in, quit and run claude auth login.
git on $PATH
A terminal that supports 24-bit color + Unicode box-drawing (modern macOS Terminal / iTerm2 / Alacritty / Ghostty / wezterm all qualify)
For contributors, or to run the latest main:
git clone https://github.com/xxyjoel/ba-mission-control.git
cd ba-mission-control
npm install
npm start
The TUI takes over the terminal. To leave, press q (or Ctrl-C).
After npm install, you can also invoke the bin directly:
./bin/mc.mjs
…or link the mc command globally:
npm link
mc
Scrolling back through output. In the Zoom view press Ctrl+F to enter
scroll mode, then w/s for a line, f/b for half a page, g for the top
and G to return to the live output. In the ! shell overlay use PageUp and
PageDown; typing anything returns you to the live output. Both views hold
your place while the session keeps printing, and both are limited by the
5,000 rows of history each session keeps — scroll past that and you are
clamped to the oldest row still held.
Configurable env vars:
| Var | Default | Effect |
|---|---|---|
CLAUDE_BIN | claude | Path to the claude CLI |
REPO_PARENTS | see server/repos.mjs | Colon-separated parent dirs scanned for the New Session repo picker. Recursive to depth 3. Overridden by a folder chosen in-app via :repos (which persists to settings). |
MC_MOCK | unset | Fixture name (e.g. approval-request) — when set, every launched session replays a JSONL fixture from server/fixtures/ instead of spawning a real claude subprocess. Use for deterministic Zoom UX iteration without API spend. |
MC_STATUS_DIR | ~/.local/state/claude-mc/status | Absolute path for the per-session hook status files. mc passes it to claude so the hook writes where mc reads. Used by the real-claude tests. |
MC_HEAP_LOG | unset | When set, logs memory (rss/heap) + per-structure counts every 60 s to ~/.local/state/claude-mc/heap/ for diagnosing long-uptime memory growth. Independent of this, kill -USR2 <mc pid> writes a heap snapshot on demand (owner-only, 0600). Off by default. |
Settings (theme, density, grid columns, max windows per pane, ctx
threshold, etc) persist to ~/.config/claude-mc/settings.json — open with
Esc in the TUI.
Card anatomy. Each grid tile is a pure-stats dashboard (fixed height, no
wrapping): title + status, model + branch + git, ctx bar, tok/min sparkline
(empty at zero throughput) with the claude subprocess's live CPU% + memory
right-aligned (4% 182M, one shared ps sample per fleet tick), a
triage row (▸ 5/7 ██████░░ <next action> — todo burndown from the
session's live TodoWrite list plus a status-driven next-action verb; blank
when there's nothing actionable — status and its duration already live in the
card's corners), the current item (↳ <in-progress todo>), session vitals
(small ●<score> health dot + turns / messages / uptime / time-in-state), and
a cost + tokens foot. It intentionally shows no session text — read the
running conversation by zooming (↵) or in the fleet log.
Which conversation a card is showing. The model row carries the first eight
characters of the session id — the same eight :bg and claude agents print —
so a card can be matched against them by eye. A yellow !N beside it means
claude is holding N other live conversations in the same folder. That is a
warning, not an error: your typing goes to the session the card names, and the
others cannot see it. Open :bg to look at them. No mark is drawn when the
count is zero, and none is drawn when claude's session list could not be read —
an absent mark never claims there is only one conversation.
The triage row answers the scan-10-cards question "does this need me, when, and
what next": check back (working), ready to review → (idle, plan complete),
needs a nudge → (idle, plan unfinished), needs input · answer to continue
(waiting) — colored by urgency.
Paging. The grid shows at most Max windows per pane cards at once
(Settings → LAYOUT, windowsPerPane, default 9 — a 3×3); when more sessions
are live, or the terminal is too short to fit them, the extra cards spill onto
additional panes instead of being clipped. [ / ] switch panes; the active
pane follows the focused card. ↑ / ↓ move by the columns the grid actually
draws.
Card width. A card is never narrower than 34 columns, the narrowest width where every row keeps its figures. When the Grid columns setting asks for more cards than fit, the grid draws fewer columns: a 120-column terminal draws 3 columns whether the setting is 3, 4 or 5, and an 80-column terminal draws 2. On a terminal narrower than 36 columns one card takes the full width. No row on a card ever wraps.
Each slot runs a real interactive claude in its own pseudo-terminal — the same
binary and the same interface you get in a normal shell — spawned with
--session-id, --model, --permission-mode and a status hook Mission Control
installs for that session. It does not drive claude through a wire protocol. It
reads what claude already writes: the session JSONL under ~/.claude/projects/
for tokens, cost and tool activity, and the hook's own status stream for whether
a session is working, idle, or blocked on a prompt.
A legacy path still exists behind FLEET_USE_PTY=0. It drives
claude --print --input-format stream-json --output-format stream-json over
stdin and stdout and maps those events onto the same UI state. It is kept as a
rollback and is not what you get by default. Zoom does not work on that path: it
shows "zoom is not supported for this session" instead. The table below
describes that path:
| stream-json event (legacy path only) | UI effect |
|---|---|
system.init | session attached; appended to tail |
stream_event.content_block_delta (text) | live activity line during a turn |
assistant (text) | activity line + log entry, usage → tokens, input_tokens → context |
assistant (tool_use) | log entry ▸ <tool>: <summary> |
user (tool_result) | log entry ← tool_result <preview> |
result | total_cost_usd → cost, status back to idle |
Verifiability — every action is observable:
tail -f over the whole fleet. Its height is exactly
the Fleet log lines setting (clamped only on terminals too short to fit).usage token counts claude records for each
assistant message, priced against the per-model rate table in tui/lib/models.js
and deduplicated by message id. They are an accurate estimate at published rates,
not a copy of your invoice. A model whose rate we have not verified inherits the
newest rate in its family and is marked ~ on the card, and an unknown model is
never priced at $0.ctx) tracks the main thread only: sub-agent (Task)
turns carry isSidechain and are excluded so the gauge never dips to a
sub-agent's smaller context mid-turn. in / out / cost are cumulative
session totals (sub-agent spend included) and reset on /clear alongside
ctx; they persist across a relaunch only through a proper save & quit.in counts fresh input only (input_tokens + cache_creation).
With prompt caching on, claude re-reads the whole context window from cache on
every assistant message, so cache_read_input_tokens re-counts the same
tokens each turn and, if summed into in, dwarfs it ~100× (e.g. 67M "in" on a
60k window). Those re-reads are broken out as cache (shown in Zoom,
billed at the 0.1× cache-read rate in cost). ctx still reflects the full
live window (fresh + cache read), which is the real size on the wire.Task / Workflow fan-out) are surfaced on the
card: when any are in flight the current-item row shows ⋔{n} (the count, or
the single agent's label), and Zoom lists each with its elapsed time. The
server pairs each tool_use with its returning tool_result to know what's
live. STUCK is suppressed while sub-agents run — they work on sidechains the
main thread can't see, so the parent's activity clock legitimately goes quiet.Each agent has exactly one status from this six-value enum (derived in server/deriveStatus.mjs):
| State | Meaning |
|---|---|
idle | attached, no current activity |
working | actively processing — streaming text, calling tools, thinking |
waiting | model delivered a prompt awaiting user approval (a.k.a. "needs input") |
paused | claude and every process it started frozen via SIGSTOP (user pressed pause). Paused always wins over the other statuses while it lasts. |
error | crashed or API failure — auto-restart may retry |
empty | slot is vacant, no agent assigned |
The fleet header (Header.jsx) shows live counts for work / wait / err
plus (when any session is live) how many are over the context threshold
(ctx≥600k 0/3 = zero of three sessions above 600k tokens of context), and
an aggregate NOMINAL / AWAITING / DEGRADED pill. Session uptime and UTC
clock sit early in the strip so they survive a narrow resize. When two or
more subscriptions are connected, the header says all sessions (count
alongside); each provider’s green ◆ active marker lives on its Aggregate
row, not as CC/CUR chips in the header.
The Aggregate line under the header is Claude-only (tokens, session/week
cost, 5h/7d plan %) when only Claude is connected. With multiple
subscriptions it splits into one Aggregate row per subscription — each
provider keeps its own meters (Claude’s rolling plan windows stay on the Claude
row; Cursor shows measured $ / tokens when usage sync is on, otherwise
sync off / $-.--). Metrics are never blended across providers whose
baselines do not match.
Two live slots may share a project path; transcripts and PTYs stay
isolated, but the working tree and git index are shared. Prefer
git worktree (or careful sequencing) for parallel writers — git commits
are the source of truth for history, not for concurrent mid-edit races.
Launching into a cwd already used by another live slot shows a soft warn
toast.
Not states (derived indicators):
STUCK Nm — a red chip on the card when an agent is working and nothing
real has happened for ≥ 5 minutes: no transcript line and no hook event
(agent.stuckMin). Claude redrawing its spinner does not count, so a tool
or thinking phase that runs longer than 5 minutes with no output also shows
STUCK. Never shown beside waiting (the session is waiting on you) or
paused, and not while sub-agents run. Not a status — the underlying state
is still working.ctx high / ctx full — derived from agent.context vs the
warn-band / threshold settings, not from status..project-health/history.jsonl and shows a small
●<score><trend> dot at the left of the card's vitals row (colored by
verdict: green HEALTHY / cyan STABLE / yellow / red), plus a health segment
on the zoom stats line. The dot is simply omitted until the project has logged
a scored turn; mc only reads the score, it doesn't compute it
(tui/lib/projectHealth.js). The untrusted verdict string is no longer
rendered on the card at all — only the numeric score — so it carries no
terminal-escape surface (zoom still shows the verdict word, humanize()d).Status accuracy — hook-driven source of truth. The session JSONL has no
permission-prompt event and marks end_turn mid-work, so a JSONL-only status
misreads: it shows working when the agent is actually blocked on a tool-approval
prompt, or idle mid-turn. mc fixes this by injecting Claude Code's own lifecycle
hooks into every spawned session (--settings → server/hooks/emit-status.mjs
writes PreToolUse / Notification / Stop events to
~/.local/state/claude-mc/status/<sid>.ndjson; server/statusHookTailer.mjs
tails it). PtyAgent.toJSON then derives status from those events:
Notification:permission_prompt → waiting (the connector is blind to
prompts — this is the "working when it's actually asking for input" fix);PreToolUse → working, held until Stop (covers the mid-turn end_turn
flash without terminal scraping);Stop / Notification:idle_prompt → idle, winning over a stale connector
working (compared against a JSONL-only clock, lastConnectorTs, so Claude's
constantly-repainting TUI can't keep a finished session pinned to working).detectApprovalPrompt is kept as an instant-INPUT fast-path while a tool is
outstanding (the permission_prompt hook is delayed ~10–20s). Sessions with no
hook events yet — the legacy FLEET_USE_PTY=0 Agent, or a PTY session before
its first event — fall back to the older terminal-scrape overlay
(detectWorking / detectApprovalPrompt in server/ptyAgent.mjs). Hooks are
auto-injected; nothing to configure. (Takes effect per session on next spawn —
a session already running when mc updated keeps its old status source until
relaunched.)
A new claude in a folder it has not been told to trust opens with a
folder-trust dialog. mc shows that slot as waiting and holds any queued
messages until you answer the dialog in zoom, so your first message is not
typed into the dialog.
Session summaries (/compact, /compact-restart) are manual,
user-invoked actions — they're not bound to a state transition. There is
no automatic stage-bound summary today.
| Keys | Action |
|---|---|
← ↑ ↓ → (or h j k l) | Move focus across the grid |
↵ | Zoom focused session — or open New Session if nothing is live |
1–9, 0 | Jump to slot 1–10 (slots 11+ via arrow nav or :goto <slot>) |
[ / ] | Switch to the previous / next pane — only when the grid pages (see LAYOUT → Max windows per pane) |
Esc | Open settings menu (or close current overlay) |
, | Settings menu |
B | Broadcast modal — types the message into each targeted session and submits it (no manual Enter per session) |
D | Fleet dashboard — sortable one-row-per-slot table for at-scale triage |
n / Ctrl+N | New session — appended below the last active card (fills a killed-slot hole only when there's no room to append) |
P / R | Pause (SIGSTOP) / Resume (SIGCONT) |
K | Kill — uppercase only (lowercase k stays vim-up and never kills); armed by first press (3s window); confirms on second K. :kill <slot> follows the same arm/confirm flow; :kill! <slot> bypasses. |
A | Approve — send a generic "continue" message to the focused session. Only accepted while the session is waiting for input (a warn toast explains otherwise) |
Shift+Tab | Cycle focused session's permission mode: plan → auto → acceptEdits |
! | Open shell overlay — a persistent $SHELL pane for aws sso login, git, kubectl, etc. |
? | Help |
/ | Filter — type a substring (matches name/branch/model/status); non-matches dim. Press again to clear. |
Every exit saves except [d]. q q (save & quit), Ctrl-C, closing
the terminal (SIGHUP) and SIGTERM all keep the live conversations and their
token/cost totals, so :resume-all can bring them back with claude --resume.
Only [d] quit, no save records just the open repo locations:
:resume-all then reopens those repos as fresh sessions (no history,
in/out/cost reset to 0). A crash that mc catches saves too; a kill -9 of mc
cannot save anything. Per-slot crash recovery during a running session still
resumes the conversation.
A slot is reported as resumed only when claude really reattached to the old
conversation. When the saved transcript is gone, the slot opens a fresh session
and the toast says slot N: no saved transcript — opened fresh. One session id
can be live in only one slot at a time.
| Command | What it does |
|---|---|
:resume-all | Restart the slots that were open when mc last closed. After a saving exit each is rehydrated via claude --resume (conversation + totals restored); after [d] each reopens fresh in its repo. The toast reports resuming N · M fresh. Killed/closed slots are excluded. On boot, a toast surfaces this if records exist. |
:resume <slot> [slot ...] | Restore specific slots — e.g. :resume 1 3 5 or :resume 1,3,5. Every slot must be a whole number from 1 to the fleet size; if any is not (:resume 99, :resume 1 99), mc shows the valid range and restores nothing. Bare :resume resumes (SIGCONT) the focused live session — the pair to :pause — or, when the focused slot is empty, restores that slot's saved record. |
:history [n] | View-only browse of the last N sessions for historical reference. Never bulk-restores (by design). |
:sessions (alias :ls) | List the sessions that were open at last close — the set :resume-all restores. |
:forget <slot> | Drop one slot from the :resume-all set. |
Configurable in Settings → GENERAL:
autoResumeOnStart, default off) —
when on, mc runs :resume-all implicitly at boot.sessionHistoryLimit, default 20) — how many
sessions the LITE history (:history) remembers.↵ on a live card opens the Zoom view. Claude's own "update available" banner
is lifted out of the body and shown as a discrete ⬆ update chip on the right
of the zoom header so it doesn't encroach on the conversation — toggle with
Hide claude update banner in zoom (hideClaudeUpdateBanner, default on) in
Settings → LAYOUT. Rows inside claude's composer are never hidden, so your own
typing can't be mistaken for the banner. Claude's own footer (the
permission-mode line) is never hidden either. Keys available there:
| Keys | Action |
|---|---|
/, @, ↑ / ↓, ⌥↵ | Claude's own slash commands, file mentions, prompt history and newline. mc passes them straight through. |
Ctrl+J / Shift+↵ | Newline without submitting (plain ↵ submits) |
Ctrl+F | Enter scroll mode — view scrollback without forwarding keys to claude (the embedded session owns the screen, so mc brackets a dedicated mode rather than fighting it for arrow keys) |
w / s (scroll mode) | Scroll one line back / forward through history |
f / b (scroll mode) | Scroll half a page up / down |
g / G (scroll mode) | Jump to the oldest / newest (live) line |
Esc (scroll mode) | Exit scroll mode (any other key also exits, returning input to claude) |
Ctrl+U | Expand / collapse the stats panel (defaults to a compact one-liner) |
Ctrl+K | Show / hide tool-call events in the log |
Ctrl+Q | Close the zoom view |
Esc, Ctrl+T, Ctrl+S, Shift+Tab | Forwarded to claude — its own cancel/back-out, todos, stash, and permission-mode cycle. mc no longer shadows these (mc's own keys are Ctrl+Q/F/K/U; claude uses those only for line editing). |
Session geometry is fixed, on purpose. Every session's claude runs at the
zoom body size — computed once from your terminal (tui/lib/zoomGeometry.js)
and applied at spawn — and only a real terminal resize changes it. Claude
reprints its entire frame on every resize and the pre-resize copy stays in mc's
scrollback, so a resize costs you one extra, differently-wrapped copy of the
conversation (measured: 1 copy → 2 after widening → 3 after widening again).
Zooming in, zooming out, a toast landing, and opening the stats or tasks panel
therefore resize nothing; the zoom pane renders the bottom slice of the
emulator instead, and Ctrl+F scroll reaches whatever the window skipped.
The zoom body is a real claude PTY, so slash commands typed there are
claude's own (/compact, /model, /clear, …) and are handled by claude
itself. mc no longer intercepts a client-side slash catalog in zoom — the
mc-side verbs live in the :cmd command bar (below), available from the grid.
!)Press ! from the grid or a focused card to open a persistent $SHELL pane — useful for aws sso login, git, kubectl, and any other shell chore that would otherwise force you out to a separate terminal.
! (from FleetView or a focused card)Ctrl+Q — returns you to the grid. Every other key (including Ctrl+K, Ctrl+U, Ctrl+J) forwards straight to the shell.aws sso device-flow are preserved across toggles. The shell is killed only on app shutdown.cd: when you open the overlay from a focused card, the shell automatically cds into that card's working directory so git and aws operate on the right repo. Opening from FleetView (no focused card) leaves the shell wherever it last was.The overlay chrome matches the Zoom modal: shell · <$SHELL> · <cwd> in the header, ⌃Q close · all other keys → shell in the footer.
| Command | Effect |
|---|---|
:theme <name> | Cycle palette (any substring match) |
:cols 3|4|5 | Change grid columns |
:perm <mode> | Set default permission mode (default, acceptEdits, bypassPermissions, plan) |
:model | Show the focused session's requested vs. resolved model |
:model <id> | Switch the focused session's model live. Restarts claude only when the new model differs from the one claude is running (picking auto again, or the model it already runs, changes nothing). |
:model default <id> | Set the fleet default model for new launches |
:model refresh | Programmatically probe the live model catalog — see Model catalog |
:kill [slot] | Kill focused (or specified) session |
:bg (alias :background) | List the sessions claude is running that are not in your fleet — id, age, state, project. These are created by claude when a conversation moves to the background, not by Mission Control, and they keep running until removed. X twice on a row deletes that conversation. The fleet row shows the count as bg N, flagging the oldest idle more than a day. |
:pause / :resume | SIGSTOP / SIGCONT the focused live session |
:approve (or :a) | Same as the A hotkey — only accepted while the session is waiting for input |
:resume <slot ...> | Rehydrate saved session(s) from disk via claude --resume (bare :resume with an empty focused slot restores that slot) |
:sessions | Show saved sessions (toast) |
:forget <slot> | Drop the saved session for a slot |
The static table in tui/lib/models.js is only the verified pricing
book — models are never hand-added to it. Each entry maps a friendly id
(opus-4.8, sonnet-4.6, …) to the CLI model name passed to
claude --model, plus display metadata (context window, per-MTok pricing,
colour). Every selector (Settings → GENERAL, the NewSession ←/→ cycler,
:model validation) reads the live catalog via modelIds(), so models
discovered at runtime are immediately selectable. The default for new
sessions is auto — the newest Opus in the live catalog, today opus-4.8
(1M-token context), so a newly released model becomes the default the moment it
is discovered. Pin an explicit id in Settings → GENERAL or with
:model default <id>.
mc syncs from two live sources on boot, in the background, so you rarely add a model by hand:
GET /v1/models — the endpoint the Anthropic
SDK's client.models.list() wraps). Free, complete, and diffed against
mc's catalog on every boot when an API credential is in the environment
(ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN): unknown models are
added, known models' real context/output limits are updated, and models
the API no longer serves are marked retired (kept for cost history).
Skipped silently on claude-CLI-only logins, where no env credential
exists.opus / sonnet / haiku
point at today (which the API can't say) and covers credential-less
installs. Each probe is a billed turn, so it fires only when
claude --version differs from the version stamped in
models-cache.json. That's how Opus 5 appeared with zero code changes
when v2.1.220 shipped. mc checks the version at boot, then again only
when an install changes the folder holding claude (a file-system watch,
no timer) — so upgrading while mc runs refreshes the list within a few
seconds. Failed probes don't stamp, so discovery retries next boot. A brand-new model family (its own alias) reaching
credential-less installs needs a one-string addition to KNOWN_ALIASES
in tui/lib/modelProbe.js — the API path needs nothing.The known gap. A new model family is invisible to both sources when you
sign in through the claude CLI subscription rather than an API key. Source 1
finds nothing without an env credential, and source 2 has no alias for the new
family. Claude Fable 5.1 hit exactly this, so fable-5.1 is hand-added to
tui/lib/models.js with limits from a live /v1/models call. Its pricing is
inherited from Fable 5 and flagged estimatedPricing — treat the cost column
for that model as an estimate. Remove the entry once discovery finds it.
Minimum claude version. A model can require a newer CLI than you have.
claude 2.1.220 rejects Fable 5.1 with 400 … version 2.1.251 or newer is required, and mc surfaces that as a failed session, not as a refusal before
launch. Upgrade the CLI the way you installed it — Homebrew users run
brew upgrade --cask claude-code, not claude update.
The default model is auto: new sessions launch on the newest Opus in
the live catalog, so a freshly discovered release becomes the default the
moment it lands — no pinned id anywhere. Pin an explicit model in Settings
(or :model default <id>) if you'd rather stay put.
Sandboxed runs (MC_CONFIG_DIR set — dev sandboxes, tests) skip boot
discovery so throwaway configs never trigger billed probes. To make a
sandbox behave like a real first-run install — e.g. when trying the
published package as a user would — set MC_SYNC_MODELS=1:
MC_SYNC_MODELS=1 MC_CONFIG_DIR=$(mktemp -d) mc
:model refresh)The claude CLI has no "list models" command, so mc learns what an alias
resolves to — and its real context window — by running a one-shot query
and reading the modelUsage block of the JSON result:
claude -p --model opus --output-format json 'hi'
# → "modelUsage": { "claude-opus-4-8": { "contextWindow": 1000000, … } }
:model refresh runs that probe for opus / sonnet / haiku
concurrently, then:
Each probe is a real, billed turn (~$0.10–0.15, ~2s), so probes run
only on manual :model refresh or a detected claude version change (above)
— never on a steady-state boot. The result is cached to
~/.config/claude-mc/models-cache.json (stamped with the CLI version) and
overlaid onto the static catalog offline on every boot. A discovered
model's pricing is inherited from its newest same-family sibling and
flagged estimatedPricing until a verified row lands in the pricing book.
If you set an incoming-webhook URL
via :slack <url>, then :feedback <message> and :request <message>
will POST a structured payload to that channel — auto-tagged with your
account email, current fleet state (live sessions, models, branches),
and plan-side usage (5h / 7d %). The webhook URL is stored in
~/.config/claude-mc/settings.json and never displayed in the UI;
Settings → FEEDBACK shows only whether one is configured.
The aggregate strip shows the same numbers Claude Code's /usage slash
command reports — read from ~/.claude/abtop-rate-limits.json:
plan 5h 1% ↻4h 50m · 7d 29% ↻2d 19h
These are plan-side percentages (the actual Anthropic quota), not
just what Mission Control has observed. The file is rewritten by every
claude invocation on the machine; we re-read it every 8 seconds. The
locally-tracked cost·week figure to the left of it is independent —
it's the rolling $ spend Mission Control itself has booked.
Inside modals, Esc closes and Tab cycles fields.
Errors and confirmations (launch failures, broadcasts, command results) appear as transient toasts above the status bar.
bin/
mc.mjs CLI entry — registers tsx JSX loader, boots tui/main.jsx
tui/
main.jsx Boot: constructs Fleet, renders <App/>, wires shutdown
App.jsx Top-level: hotkeys, focus, modal routing, fleet sub
Header.jsx Top status strip (+ connected subscription chips)
Aggregate.jsx Token/cost line (per-sub when multi-provider)
Card.jsx One agent tile
FleetLog.jsx Bottom pane — aggregated activity stream
StatusBar.jsx Vim-style status bar
modals/
Help.jsx Keymap reference
Broadcast.jsx Send one prompt to N sessions
NewSession.jsx Single-input launcher: fuzzy-match recents or type a path
Settings.jsx btop-style settings (tabs: GENERAL / LAYOUT / COLORS / ALERTS / SAFETY / NOTES)
Zoom.jsx Single-session view: header, stats/tools panels, PtyPane body
Dashboard.jsx Sortable one-row-per-slot table (D)
QuitConfirm.jsx Save / no-save quit choice
RepoPicker.jsx Filesystem browser to choose the repo scan folder (:repos)
zoom/
PtyPane.jsx Draws the slot's own terminal emulator; forwards keys to claude
zoomKeys.js The few keys zoom keeps for itself (Ctrl+Q/F/K/U/J)
lib/
themes.js 7 palettes (BlueArch / Tokyo Night / Gruvbox / Catppuccin / Solarized / Amber / Matrix)
format.js bar / sparkline / fmtK / fmtMoney / trunc / fmtClock
models.js Claude model metadata (label, maxCtx, kind, costs)
settings.js Schema + defaults + on-disk persistence
sessionStore.js Saved sessions for :resume / :resume-all
gridLayout.js Card grid geometry and paging
TextField.jsx Minimal single-line input (Ink has none built-in)
server/
fleet.mjs Slot manager (10 by default, up to 64) + pub-sub (EventEmitter)
ptyCore.mjs One slot's PTY: spawn, emulator, send queue, pause/kill, restart
ptyAgent.mjs claude on top of PtyCore: argv, hooks, tailers (the default)
deriveStatus.mjs The one function that turns a slot's signals into its status
sessionFileTailer.mjs Follows claude's session JSONL; jsonlConnector.mjs parses it
statusHookTailer.mjs Follows the hook status file (working / waiting / idle)
subagentUsageTailer.mjs Adds sub-agent token use to the parent session
hooks/emit-status.mjs The hook claude runs; appends one NDJSON line per event
claudeSessions.mjs Reads `claude agents --json` (which sessions claude has open)
providers/ Slot backends: Claude, and Cursor under providers/cursor/
agent.mjs Legacy stream-json wrapper (FLEET_USE_PTY=0 only)
mockAgent.mjs Fixture-driven Agent stand-in for UX iteration (MC_MOCK)
fixtures/ JSONL fixtures replayed by MockAgent
git.mjs branch / dirty / ahead-behind via `git`
repos.mjs Recursive repo scanner for the New Session picker
For iterating on Zoom UX without burning real claude sessions, set
MC_MOCK=<fixture> before launching. Every session opened in the TUI
will then replay the named fixture from server/fixtures/ instead of
spawning a real subprocess. Available fixtures:
| Fixture | Exercises |
|---|---|
quick-reply | text-only assistant turn |
tool-loop | assistant → tool_use → tool_result → assistant |
long-thinking | extended-thinking block + streamed answer |
approval-request | tool approval banner; pauses at waiting, resumes when you reply (a/r in zoom) |
Example:
MC_MOCK=approval-request npm start
Drop a new fixture by adding server/fixtures/<name>.jsonl. The
directive schema is documented at the top of server/mockAgent.mjs.
server/ is the data layer — it has no Express anymore. The TUI reads
fleet.snapshot() directly and subscribes to fleet.on('change', …).
Sessions default to acceptEdits — claude can read and edit files in the
session cwd without prompting, but still blocks unsafe bash. Valid values are
default, acceptEdits, bypassPermissions, plan. Configurable in the
Settings menu (Esc) under GENERAL → Default permission mode, or via the
command bar with :perm <mode>. The mode is swappable mid-session (which is
why it isn't part of the launcher anymore). bypassPermissions removes all
guardrails — only use it in trusted, scratch directories.
Each running session is mirrored to ~/.config/claude-mc/sessions.json
along with the claude session UUID. To pick up where you left off:
:resume <slot> rehydrates in place.:sessions. Drop a record with :forget <slot>.Under the hood we call claude --resume <session-id> against the same
working directory. claude restores the prior transcript from its own
on-disk session store.
Built on top of the reliability layer below.
D or :dash)A single-screen table — one row per live agent — for triage when you have more than a handful of slots running. Columns: slot, name, model, status pill, ctx %, tok/min, $ session, age, activity. Sortable:
S cycles the sort column (slot → status → ctx → tpm → cost → age)R reverses direction↑ ↓ moves the highlight (the focused slot tracks)↵ zooms the highlighted slotD or esc closesThe header also surfaces today's fleet spend and the configured budget when one is set — so "how much have I spent today?" is one keystroke from any view.
:cap and :budgetTwo tiers of cost protection, both off by default (set the values to opt in):
:cap <slot> <usd> refuses further messages to that
slot once its costSession reaches the cap. This covers sends from the
grid, broadcast, approve and typing in zoom (Enter and pastes are refused;
a respawn after :model or :perm keeps the cap). <slot> can be any
live slot up to the fleet size, not just 1–10. :cap default <usd>
persists a fleet-wide default for every new launch; it does not change a
slot you capped by hand. Raise the cap with the same command to continue
(:cap 3 10 bumps slot 3 to $10). A hand-set cap stays until that slot is
relaunched (:cap <slot> 0 turns it off but still counts as hand-set).:budget <usd> refuses to start sessions once today's
fleet-wide spend reaches the budget: New Session, :template, :resume,
:resume-all, auto-resume at boot, :clear / :restart (refused before
the kill) and :cr. Sessions already running keep running, and a running
session that restarts itself is not blocked: crash auto-restart, and the
restart that :model or :perm does. Use :budget 0 to disable. :budget alone
shows today's spend.Both limits act on the next message or launch. A turn that is already running finishes, and a message queued before the cap was reached is still sent, so the final total can end a little past the figure.
Both also have form-editable rows under the SAFETY section of the
settings menu (, or esc).
:template <name>Pre-configured bundles of N session launches with model / permission /
prompt baked in. Bundled defaults at first launch (auto-written to
~/.config/claude-mc/templates.json):
review — 3 sessions (Opus architecture + 2 Sonnets) reviewing
the same repo in plan mode.explore — 2-session parallel exploration: Opus deep + Sonnet
fast.spec-then-implement — 2 sessions: Opus writes a spec in plan
mode; Sonnet implements in acceptEdits.Usage:
:template # list available templates
:template review # launch into next N empty slots, using
# focused agent's cwd (or process.cwd)
:template review ~/my-other-repo
Edit ~/.config/claude-mc/templates.json to add your own.
@file mentions in zoomZoom is claude's own interface, so claude's @ file mentions work there as
they do in a plain terminal: type @ and part of a filename to get claude's
picker. mc does not add its own.
These guard against the failure modes that hit hardest at fleet scale.
Quiet at idle (battery-friendly). An idle fleet coalesces all of its
background polling into a single shared wakeup (stretching to 3s when
nothing is working), gates the invisible command-bar caret, and detects
transcript rotation with one directory stat instead of a per-file sweep.
At idle the screen is redrawn only when something on it changes (the
clocks show HH:MM until an agent is working), sessions.json is written
only when a record changes, and claude agents is asked every 120 s plus
once after a launch, kill or exit. Measured with node scripts/measure-idle.mjs (mc plus every process it starts, 240 s, stub
agents, one machine): 0 agents 0.96% → 0.23% of one core and 929 → 12
bytes/s of terminal output; 64 idle agents 1.15% → 0.55%. Renders,
statuses and keystroke latency are unchanged while agents work.
Status truth, regression-proofed. A card blocked on a question
(AskUserQuestion / plan approval) reads INPUT for the entire wait —
the hook channel goes silent while an ask is pending, and the pending
prompt now outranks the last "tool running" signal however stale it gets.
Every real status incident is checked in as a recording under
tests/fixtures/status-corpus/ and replayed through the live pipeline
(tests/status.replay.test.mjs) with virtualized time, so a fixed lie
stays fixed.
Press-K-twice to kill. A first press arms the kill action for 3
seconds and shows a warning toast; the second K confirms and SIGTERMs
claude and every process it started, then SIGKILLs anything still alive
after 1.5 s. Eliminates the most painful misfire (accidental loss of
work). The :kill command bar entry follows the same arm-then-confirm
flow; :kill! (bang) bypasses for explicit batch use.
Auto-restart on transient errors. When a claude subprocess exits
unprompted with a non-zero code, the slot retries up to 3 times with
widening backoff (2s → 5s → 15s), restarting via --resume <session-id>
so the in-progress conversation is preserved. The retry counter resets on
the next successful init event, so a recovered slot is eligible again
on its next independent failure. After 3 failed restarts the slot enters
the errored state with K clears slot hint.
Pause stops the whole tree. P / :pause sends SIGSTOP to claude and
to every process it started (including Bash-tool shells that run in their
own session), and resume sends SIGCONT to all of them. If mc dies hard
while a slot is paused, the next mc start ends the frozen claude it left
behind (only when the pid, start time and parent all still match).
--resume ("currently running as a background agent") until the orphan is
stopped via claude agents. mc's card surfaces this state with the
remediation; prefer q → save (which now cannot stall) over killing the
terminal.default mode. The A hotkey sends a generic
"yes, continue" message that unblocks most prompts, but for tight
control prefer acceptEdits or bypassPermissions, or answer the prompt
in zoom.~/.config/claude-mc/settings.json.~/.config/claude-mc/costs-week.json. The week
rotates automatically every Monday 00:00 UTC.~/.config/claude-mc/sessions.json so the next launch can --resume.npm install
npm start
On first launch the grid is empty — you'll see the header, the aggregate
strip, and a "no sessions running" hint. Press n (or Ctrl+N) to open
the New Session picker. As you launch sessions, cards appear and
autosize to fill the row; the grid wraps to multiple rows once you pass
settings.gridCols columns. The bottom fleet log streams events from
every live session.
One path input. As you type, the dropdown below it blends two sources:
:repos./ or ~
(or contains a /), child directories of the deepest existing parent
are appended. Hidden dirs, node_modules, dist, and build are
skipped.Hotkeys:
| Key | Action |
|---|---|
↑ / ↓ | move the highlight through suggestions |
↵ | launch the highlighted suggestion — or, if nothing is highlighted, the typed path as-is (must already exist) |
← / → | cycle the model |
Ctrl+B | open the filesystem browser (familiar cd/ls-style nav); ↵ inside the picker launches the highlighted folder |
esc | cancel |
Inside the filesystem browser (Ctrl+B): ↑/↓ or k/j move the
highlight, →/l descends into the highlighted folder, ←/h goes
up a level, . picks the folder you're currently in, ↵ launches the
highlighted folder immediately. esc returns to the path input.
Intentionally absent: mode toggle, create-new (mkdir + git init), resume banner, branch input, permission picker, initial prompt. The launcher does one thing: pick a repo and launch. Branch follows the repo's default; permission mode and prompt are post-launch concerns and changeable from inside the running session.
Without claude on $PATH the New Session launch will still succeed at the
fleet level, but the spawned process will exit immediately — you'll see the
error in the per-session tail.
npm test # run the suite once
npm run test:watch # rerun on file change
npm test runs scripts/run-tests.mjs, which executes every
tests/**/*.test.* in its own process. In headless CI (CI=true) it skips
the real-terminal suites — the node-pty recipes under tests/recipes/ and
the *.realparser.test.* ink-keypress-parser tests — because the GitHub runner
has no usable TTY/PTY (node-pty emits no output; ink mis-parses control bytes).
Those run normally for you locally and on every push (the pre-push hook runs
npm test with CI unset). Set MC_RUN_PTY=1 to force them on anywhere.
The suite never touches your real home. The runner gives each test file
its own temporary HOME, MC_CONFIG_DIR, XDG_STATE_HOME and
XDG_CONFIG_HOME, deletes them afterwards, and sets CLAUDE_BIN=/usr/bin/false
so no test can start the real claude (tests/runner.hermetic.test.mjs checks
this). Run single files through the runner too:
node scripts/run-tests.mjs <test files…>. A bare
node --test does not get this protection.
Tests against the real claude are opt-in. They are skipped unless you set
MC_RUN_REAL=1:
MC_RUN_REAL=1 node scripts/run-tests.mjs tests/real/claude.real.test.mjs
They take about a minute and cost money: a few haiku turns and one sonnet
turn on your own claude login. claude itself still writes to your real home
while they run (a folder-trust entry in ~/.claude.json, lines in
~/.claude/history.jsonl, and whatever your own hooks and MCP servers write).
The suite uses Node's built-in node:test runner plus
ink-testing-library for component rendering. Coverage includes:
tests/detectPrompt.test.mjs — structured-prompt classification
(numbered, checkbox, lettered Option A/B/C, binary fallback, priority
rules, the screenshot regression).tests/RepoPicker.test.jsx — filesystem browser lists real subdirs
(filtering dotfiles / node_modules / files), ↵ picks a child, .
picks the current folder, esc cancels.tests/NewSession.test.jsx — single-input launcher: substring
filtering, ↵ launches the highlighted suggestion, ↵ on a typed real
path launches it, ←/→ cycles the model, esc closes, nonexistent
paths show an error and don't launch.tests/TextField.test.jsx — plain ↵ submits; ⌥↵ (ESC+CR) and
Ctrl+J insert a newline; the meta/shift Return paths are checked
before the plain-return submit branch so macOS users don't
accidentally submit half-typed messages.tests/zoom/zoomKeys.realparser.test.jsx — each zoom key (Ctrl+Q/F/K/U/J)
is driven as raw bytes through Ink's real key parser.tests/zoom/ptyPane.scrollHold.test.jsx — scroll mode holds its place
while the session keeps printing.tests/agent.costCap.test.mjs — the legacy agent refuses a send once the
session's cost has reached its cap, and cap 0 means no cap.tests/zoom/ptyPane.guardedInput.test.jsx — on the default PTY path,
Enter and pastes typed in zoom are refused once the cap is reached, and the
cap still holds after a :model / :perm respawn.tests/App.dailyBudget.test.jsx — over the daily budget, :template,
:resume, :resume-all (including its delayed launches), boot
auto-resume, :clear / :restart and :cr launch nothing.tests/sessionSave.sighup.test.mjs — closing the terminal keeps the full
resumable record.tests/costStore.twoInstances.test.mjs — two running copies of mc cannot
corrupt each other's spend ledger.tests/status.replay.test.mjs — every status misreport ever filed is
checked in as a recording and replayed through the live pipeline, so a
status bug that is fixed stays fixed.tests/recipes/zoom-text-input.recipes.test.jsx,
tests/recipes/newsession.recipes.test.jsx,
tests/recipes/repopicker.recipes.test.jsx,
tests/recipes/pty.recipes.test.jsx — declarative recipe coverage for
each surface (see the section below).For new feature coverage, prefer the declarative recipe runner in
tests/lib/recipe.js over hand-writing render / stdin / assert
boilerplate. A recipe is a JSON-shaped list of steps that drive a
rendered Ink component and assert on the frame at each step. Each step
can type characters, press a key, tick for timers, assert on
expectFrame / expectNotFrame substrings, or check
expectCallback arg arrays. On failure the runner throws with the
failing step's label, the assertion, and the last frame — so you can
diff actual UI against intent.
Example, from tests/recipes/dashboard.recipes.test.jsx:
import { runRecipe } from '../lib/recipe.js';
import Dashboard from '../../tui/modals/Dashboard.jsx';
test('recipe: dashboard renders one row per LIVE agent (empties filtered)', async () => {
await runRecipe({
component: Dashboard,
props: dashProps(), // three live agents + one empty slot
steps: [
{ expectFrame: [/FLEET DASHBOARD/, /3 live/, /alpha/, /beta/, /gamma/] },
// The empty slot 4 must NOT appear in the body.
{ expectNotFrame: [/EMP|\[4\]/] },
],
});
});
Common fixtures (makeAgent, chatTail, the active theme) live in
tests/lib/fixtures.js so recipes don't have to redefine boilerplate.
The intent: adding "press X, expect Y" coverage should feel like
writing a JSON document, not a React component.
tests/lib/recipe.js — in-process runner (Ink + ink-testing-library).tests/lib/fixtures.js — agent / tail / theme helpers.tests/recipes/*.recipes.test.jsx — one file per UI surface.The in-process runner is fast but renders Ink in a fake stdout — it can't
catch terminal-specific bugs (e.g. the macOS Option+Return split-read
where ESC and CR arrive in two reads instead of one). The
runRecipePty backend in tests/lib/recipe-pty.js spawns a real
subprocess inside a pseudo-terminal (node-pty) and pipes its output
through a headless xterm.js Terminal so escape sequences are processed
exactly the way iTerm/Terminal.app would process them. Same step DSL
(type / press / tick / expectFrame / expectNotFrame),
different backend.
import { runRecipePty } from '../lib/recipe-pty.js';
test('counter app: + increments', () => runRecipePty({
command: process.execPath,
args: ['tests/lib/pty-fixtures/counter-app.mjs'],
bootDelayMs: 600,
steps: [
{ expectFrame: [/counter:/, /\b0\b/] },
{ type: '++' },
{ tick: 80 },
{ expectFrame: [/counter:[^\n]*\b2\b/] },
{ press: 'q', expectExit: 1500 },
],
}));
Use the in-process runner for fast component tests and the PTY runner
for "does this work when wrapped by a real terminal." See
tests/recipes/pty.recipes.test.jsx for working examples.
node-pty ships a prebuilt spawn-helper whose executable bit is
sometimes dropped by npm install. A postinstall script
(scripts/fix-node-pty.mjs) restores it automatically — if you see
posix_spawnp failed after a fresh install, run npm install again or
re-run the script manually.
tests/humanize.test.mjs — ANSI strip, path collapse, UUID shorten,
JSON collapse, idempotency.tests/MockAgent.replay.test.mjs — every shipped fixture replays
cleanly; approval fixture pauses at waiting and resumes on send().When iterating on UI without API spend, prefer:
MC_MOCK=approval-request npm start
…to exercise the structured-prompt + APPROVE? marker flow against the
canned fixture instead of a real claude session.
Mission Control is designed and built by BlueArch, a cloud governance and efficiency company.
Our work is about the same problem in two places. In the cloud it is idle capacity, oversized instances and spend nobody attributed. In AI development it is agents left running in a closed terminal, fan-out whose cost lands on no session, and a bill that arrives a month after the decision that caused it.
Mission Control answers that for Claude Code: measure the spend per session while it happens, attribute the sub-agents back to the run that spawned them, surface the sessions you forgot were open, and let you set a figure the fleet is not allowed to pass. Governance you can act on at the moment it matters, not a report afterwards.
<sessionId>/subagents/agent-*.jsonl) the main tailer
never reads, so their tokens, cost, and tok/min used to be invisible — a
fan-out session read near-zero throughput. A dedicated sidechain tailer folds
that usage into the parent's in / cache / out / cost / tok-min (not
ctx — sidechains keep their own window). On resume, pre-existing sub-agent
files are primed at EOF so historical spend isn't double-counted.git calls
in the session's cwd when the slot starts or restarts (the legacy
FLEET_USE_PTY=0 path also re-reads it after each turn). When git fails or
times out the card shows ? instead of a count, never a clean ●. A folder
that is not a git repo shows no git chips./: | Command bar (see below) |
q | Quit — opens a confirm: [q] save & quit, so q q saves (s, y and Enter also save, unlisted) vs [d] quit, no save vs [n]/Esc cancel |
Ctrl-C | Quit immediately and save, the same as q q (see below) |
:repos | Open the folder picker to choose where repos are scanned. The chosen folder replaces the built-in defaults. :repos clear resets to defaults. |
:update | Report claude version drift: the on-disk claude --version vs the version each live session actually launched on (running processes keep their old binary until restarted). Drifted slots get a fleet-log line; converge by quit+relaunch. |
:whoami (or :auth) | Re-probe claude auth status and surface email + subscription |
:usage | Re-read plan-side rate-limit telemetry (5h and 7d quota %) |
:transcript (or :tx, :log) | Print the full path of the focused session's claude transcript file, or say none is on disk yet |
:where | Print the config dir and the focused session's transcript path |
:remember "<note>" (or :rem) | Append a dated note to <cwd>/.mc/MEMORY.md. Off by default — needs .mc/MEMORY.md auto-inject (plugin_projectMemory) turned on in Settings → PLUGINS. With it on, a new session in that folder also gets the file prepended to its first message. Settings saved before this default changed may already hold true. |
:memory (or :mem) | Show the focused repo's .mc/MEMORY.md in the session tail |
:note <text> (or :n) | Inject a local annotation into the focused session's chat log (not sent to claude) |
:slack <url> | Set the Slack incoming-webhook URL. :slack clear removes it. |
:feedback <msg> | Send feedback to Slack (includes auth + fleet + plan-usage context) |
:request <msg> | Send customer request to Slack |
:quit | Exit |
One mc per config dir. A second mc started on the same config dir opens read-only, so two copies cannot overwrite each other's saved sessions or spend.
Quit can't stall. q → save SIGTERMs every child, gives them a 1.5s
grace, SIGKILLs any straggler, and exits — a claude wedged on a permission
prompt can no longer hold mc's shutdown hostage (previously the quit hung
until a force-close, which orphaned that child into claude's daemon).
Background-agent claim detection. If a session can't resume because
claude's daemon holds it as a background agent (the usual leftover of a
force-closed terminal), the card says exactly that — with the fix
(claude agents → stop the orphan, then :resume the slot) — instead of
silently burning restart attempts on a failure that can't succeed.
Stuck-detection. When a slot is working but has had no transcript
line and no hook event in the last 5 minutes (screen redraws do not count),
the card shows a red STUCK Nm chip and a one-shot toast fires (slot N · stuck 5m · no events while working). Re-arms automatically when the
slot starts emitting again.
Context-pressure toasts. Crossing 80% / 90% of the model's max
context fires a yellow / red toast (slot N · context 80% · consider /compact). Each crossing is fired exactly once; the trigger re-arms
when the slot drops back under the threshold (post-compaction).
Title-row context chip. When a slot's context passes warnPct of the
ctxThreshold setting (defaults: 85% of 150k tokens), the status pill on the
card title gets a · 89% chip in the matching urgency color — yellow near the
threshold, red past it. Both values are in Settings → GENERAL. Pairs with the
toast but stays visible at-a-glance.
Per-session status stream on disk. Every lifecycle event the status hook emits for a session is appended as NDJSON to:
$XDG_STATE_HOME/claude-mc/status/<sessionId>.ndjson
(defaults to ~/.local/state/claude-mc/status/). It survives process
restarts, slot reassignments and reboots, and it is what to grep when a card
showed something you did not expect. The conversation itself is not copied
here — claude keeps that in its own session JSONL under ~/.claude/projects/,
and :transcript prints the path to it for the focused slot.
The legacy FLEET_USE_PTY=0 path additionally writes a combined
inbound/outbound transcript to .../claude-mc/sessions/<sessionId>.jsonl,
disabled with MC_NO_TRANSCRIPT=1. The default PTY path does not write it.
On both paths :transcript prints claude's own session file.