Maestro CLI

One binary, one socket. maestro talks to the running app over its control socket — the same verbs drive the UI, your scripts, and the agents themselves. Everything you can click, an agent can run.

Concepts

Every maestro-managed terminal runs inside a session (a tab in the app). Sessions form a tree — a session can have child sessions — and windows hold session trees. The env var $MAESTRO_SESSION_ID holds the current session's id; "this session"/"this tab" means that id.

Get the CLI

Sessions spawned by Maestro already have a maestro shim on their PATH. For a global command in your own shells, symlink the launcher out of the app bundle:

ln -sf /Applications/Maestro.app/Contents/Resources/bin/maestro /usr/local/bin/maestro

Targeting

<target> accepts a session-id prefix (e.g. 3f2a) or a unique title substring — but the id is the only stable key. Titles change as an agent works, so a title match that resolves today can resolve to the wrong tab (or nothing) a minute later. Except for a one-shot command you type by hand, resolve a title to its id once with maestro id and target by that id from then on:

SID=$(maestro id "api tests")   # resolve once
maestro send "$SID" "npm test"  # target by id thereafter
maestro wait "$SID" --until idle

maestro open prints the new session's id — capture that rather than guessing a title. Use "$MAESTRO_SESSION_ID" for the current session, and maestro list to see every id and title. The examples below all target by id for that reason.

Sessions

maestro list [<target>] [--full] [--limit <n>] [--archived | --closed]

Live sessions as JSON (id, title, kind, status, cwd, parentId, windowId). Pass a <target> to narrow it to one session. --full widens each row with the session detail: job and state labels, work context (branch, dirty, projectRoot, statusReason, timestamps), the attached agent (kind, origin, pid, configHome, agentSessionId), model, accountId, contextTokens, links, and lifecycle flags. --limit <n> caps the rows returned. --archived lists archived (shelved) sessions instead; --closed lists recently-closed sessions (tombstones) instead, newest first.

maestro list | jq '.[] | {id, title, status}'
maestro list "$SID" --full            # everything known about one session

maestro open --kind <claude|codex|opencode|shell> [--child|--sibling] [--cwd <dir>] [--parent <id>] [--title <t>] [--prompt "<task>"] [--model <id|@user>]

Open a new session and print its id. By default it inherits the cwd of the tab the command ran in — pass --cwd to override, or run from outside maestro to start in your home directory.

  • --kind claude, codex, opencode, or shell (default shell)
  • --child / --sibling place relative to the FOCUSED session (parent and cwd resolved server-side)
  • --parent <id> nests under a specific session
  • --cwd <dir> working directory
  • --title <t> tab title
  • --prompt "<task>" seeds an agent (claude/codex/opencode) with its first task at spawn — one command, no follow-up send.
  • --model <id|@user> agent model for this session — an id, @user to match your own CLI default, or "" for the agent's native default
SID=$(maestro open --kind claude --child --prompt "run the api tests and fix failures")

maestro focus <target> | --back | --forward

Focus a session (bring its tab to front), or move back/forward through this window's focus history.

maestro focus "$SID"
maestro focus --back

maestro send <target> "<text>" [--no-enter] [--force]

Type text into a session's terminal and press Enter to submit it — one call. Pass --no-enter to type without submitting, for staging input that the user (or a later key) will complete.

If the target's agent composer already holds text the user was typing, the message is parked rather than delivered — merging the two would submit their unfinished prompt as part of yours. The reply carries {delivered:false, parked:true, queued:n}, and it goes out when the user submits their own prompt or clicks “send now”. --force jumps the queue instead of waiting behind it, stashing the draft into the agent's own kill ring so the message lands alone. When a draft can't be stashed and given back — the agent has no kill ring, or the draft spans more rows than it returns intact — even a forced send parks.

maestro send "$SID" "npm test"
maestro send "$SID" "npm test" --no-enter   # stage it, don't run it
maestro send "$SID" "stop and revert" --force

maestro parked <target> [--flush | --drop]

Inspect or release messages parked for a session. With no flag it returns the queue as JSON metadata (from, at, bytes) oldest first — never the message text. --flush delivers the whole queue in arrival order right now; --drop discards it.

A --flush answers {flushed:0, held:"draft-too-tall"|"no-kill-ring"} when the draft in the composer can't be stashed and handed back — releasing would submit that draft along with the messages, so they stay queued and go out once the composer is empty. Parked messages are never delivered on a timer and do not survive an app restart.

maestro parked "$SID"            # what's waiting, and since when
maestro parked "$SID" --flush    # let it all through now

maestro send-key <target> <enter|escape|ctrl-c|tab>

Send a key press to a session.

maestro send-key "$SID" ctrl-c

maestro archive <target> [--instant] [--stash] [--force] [--cascade]

Archive a session and its subtree — put it to sleep in closed history; restore via unarchive. This is what closing a tab does by default, respecting the close countdown unless --instant.

  • --cascade archives the whole subtree, children first
  • --instant skips the close countdown
  • --stash sends it to the visible Stash shelf instead of closed history
  • --force bypasses the busy/dirty guard
maestro archive "$MAESTRO_SESSION_ID" --cascade --force

maestro delete <target> [--force]

Permanently delete a session and its subtree (stops it if running). Recoverable from Recently-Closed (⌘P #) unless recovery is off. --force bypasses the busy/dirty guard.

maestro delete "$SID" --force

Orchestration

maestro id <target>

Print a session's full stable id (uuid). Titles change as an agent works and aren't a stable key — resolve a title/prefix to its id once, then target later commands by that id.

SID=$(maestro id "api tests")
maestro focus "$SID"

maestro wait <target> --until <status> | --until-not <status> [--timeout <s>]

Block until a session's status matches (--until) or stops matching (--until-not) a status — working, idle, done, compacting — then return JSON {matched, timedOut, finalStatus, waitedMs}. If the session is removed, archived, or exits while waiting, it returns immediately with {matched:false, gone:true} rather than hanging. --timeout is in seconds; 0 waits forever; the default is 300s.

maestro wait "$SID" --until idle --timeout 120

maestro compact <target> [instructions...] [--then-send "<text>"]

Trailing words are passed through to /compact as its instructions, so you can steer what the summary keeps. Submit /compact to a session, wait (event-driven) for the compaction to finish, then optionally submit a follow-up — one deterministic call instead of sending /compact and polling for it to settle. Returns JSON {compacted, timedOut, sentFollowup, finalStatus, waitedMs}. When the target is the calling session itself, the sequence runs detached and the call returns {detached:true, target, willSend} immediately, since a session can't wait on its own turn ending.

maestro compact "$MAESTRO_SESSION_ID" --then-send "continue where you left off"

maestro status-set <target> <status> [--reason <text>]

Manually override a session's status (e.g. working, review, idle). Mostly for scripting and testing.

maestro status-set "$SID" review --reason "needs review"

Layout & windows

maestro split <target> [--down]

Split a tab into panes (tmux-style): spawns a shell pane beside <target> (below with --down).

maestro split "$MAESTRO_SESSION_ID" --down

maestro panel <target> [--url <url>] [--off] [--close-view <id>] [--reopen-view]

Open the session's side panel — by default a live web preview (the dev-server URL is auto-detected, or set one with --url). The panel is tabbed and holds web previews and code-editor views side by side.

  • --off hides the panel
  • --close-view <id> closes a specific panel tab (a code-editor#<path> or web-preview instance id)
  • --reopen-view reopens the last pane tab closed for the session (LIFO)
maestro panel "$SID" --url http://localhost:5173

maestro open-file <target> <path>[:line] [--with <viewId>]

Open a file in the session's side-panel code editor, optionally at a line. --with <viewId> opens it in a specific editor view instead — an extension-contributed panel view, or code-editor for the default text editor.

maestro open-file "$SID" src/billing.ts:42

maestro web-clear

Wipe the web preview's browsing data — cookies, logins, localStorage, and cache. The preview shares one persistent browser profile across every session, window, and project, so this clears it everywhere and signs you out of every previewed site.

maestro web-clear

maestro web-login <url> | --done | --forget [domain]

Sign in to a site for the side panel using a real Chrome. Google blocks sign-in inside the embedded browser, so this opens a maestro-owned Chrome window, mirrors that site's cookies into the panel while you sign in, and stops when you finish — click “I've signed in” in the panel dialog, or just close the Chrome window. Only the target site's own cookies are copied, never your identity provider's.

Starting a url this way needs confirmation in the focused maestro window: that browser carries your live sign-in session, so the app asks before pointing it anywhere, and refuses when no maestro window is focused. --done finishes the running handoff from the terminal, needing no confirmation since the handoff was already approved. --forget <domain> wipes just that site's mirrored cookies; --forget with no domain wipes the sign-in profile and every cookie in the panel's shared browsing profile.

maestro web-login https://app.example.com
maestro web-login --done

maestro move <target> [up|down] [--before <id>] [--after <id>] [--child-of <id>] [--promote] [--to-window <id>] [--new-window] [--project <dir>]

Reorder or re-parent a session, or move it to another/new window.

  • --promote lifts it to root level
  • --child-of <id> re-parents under another session
  • --to-window <id> / --new-window moves it across windows
  • --project <dir> regroups it under a project (clears the manual pin)
maestro move "$MAESTRO_SESSION_ID" --new-window

maestro move-project <dir> [up|down] [--before <dir>] [--after <dir>] [--first] [--last] [--window <id>]

Reorder a project group in the sidebar (per window, custom sort). Dirs resolve to their project root; defaults to the focused window.

maestro move-project ~/dev/api --first

maestro duplicate-window [--window <id>]

Open a new window viewing the same workspace (same sessions, independent focus). Defaults to the focused window.

maestro duplicate-window

Lifecycle

maestro rename <target> "<title>" | --clear

Set a session's custom title. --clear (or an empty title) reverts to the auto-derived one. A name maestro generated itself (shell, or the attached agent's kind) is refused as a pin, so a placeholder can never block the real derived title from landing — the reply reports pinned so you can tell which happened.

maestro rename "$MAESTRO_SESSION_ID" "api tests"

maestro reveal <target> <finder|terminal|editor>

Open the session's cwd in Finder, a new Terminal, or your editor.

maestro reveal "$SID" editor

maestro popup <target>

Open a compact popup window showing a live duplicate of a session's terminal — the same window that auto-appears when a session needs input.

maestro popup "$SID"

maestro reopen [<target>]

Reopen a closed session (see list --closed). <target> matches a closed session by id-prefix or title; omit it to reopen the most-recently-closed one.

maestro reopen

maestro stash <target>  ·  maestro unstash <target>

stash shelves a session and its subtree — same as archive, but it stays visible in the sidebar Stashed section. unstash restores it; agents resume lazily on focus.

maestro stash "$SID"
maestro unstash "$SID"

maestro unarchive <target>

Restore an archived session (and its subtree) to the sidebar; agents resume lazily on focus. Find archived ids with maestro list --archived.

maestro unarchive "$SID"

maestro promote <target>

Promote a live orchestrator run, or a stashed session, into a normal permanent tab — detaches it from its run and clears the orchestrator flag so it becomes a regular session.

maestro promote "$SID"

Cards & metadata

maestro color <target> "<#hex>" | --clear

Tint a session's card with a custom color. Quote the hex — an unquoted # starts a shell comment. --clear reverts to the theme's default card background.

maestro color "$SID" "#3fb87f"

maestro card <target> [--compact | --normal | --toggle]

Switch a session's sidebar card between the compact one-line row and the full card (no flag toggles). Persists per session; a compact project group still overrides it while collapsed.

maestro card "$SID" --compact

maestro snooze <target> --for <5m|15m|1h|3h|tomorrow|…> | --clear

Quiet a session now and tuck it into the collapsed Snoozed section until the duration elapses, then mark it unread and notify so it draws attention again. --for accepts a compound duration (e.g. 90m, 1h30m, 2d) or tomorrow (next 9am). --clear unsnoozes it immediately. Focusing a snoozed session does not wake it.

maestro snooze "$SID" --for 1h30m

maestro job-set <target> <name> | --clear

Label what a session is working on. The name is free text — call it whatever the work actually is — and shows as a pill on the card, coloured by a stable hash of the name, so the same job always looks the same. Nothing is inferred: jobs and states are only ever set by you (or an agent on your behalf). --clear removes the label.

maestro job-set "$SID" code
maestro job-set "$SID" release

maestro state-set <target> <text> | --clear

Override the state label shown after a session's job pill — free text for the phase happening right now (e.g. plan review, blocked). --clear removes it.

maestro state-set "$MAESTRO_SESSION_ID" "code review"

maestro pill-color <target> "<#hex>" | --clear

Set a per-session colour for the job/state pill, overriding the name-derived default. Quote the hex — an unquoted # starts a shell comment. --clear reverts to the automatic colour.

maestro pill-color "$SID" "#ffffff"

Agents & accounts

maestro swap <target> --resume <agentSessionId> | [--to claude|codex|opencode|kimi] [--model <m>] [--handoff <file>] [--account <id>|user|auto] [--force] | --handoff-auto [--include "<text>"]

Replace the agent running in a tab in place (same tab id/position).

  • --resume <id> continues an existing claude/codex/opencode conversation; otherwise a fresh agent boots
  • --to picks the provider, --model the model, --handoff <file> seeds its first prompt
  • --account <id> swaps the tab onto a different account of the same provider (see maestro accounts list); user swaps back to your personal login; auto picks by the configured strategy
  • --handoff-auto has the outgoing agent write its own handoff document, archives it with grace, and opens the successor at the same tab placement — --include "<text>" adds context to the handoff prompt (not combinable with --resume/--handoff)
  • --force bypasses the busy guard

The outgoing conversation is tombstoned — reopen or ⌘Z restores it.

maestro swap "$SID" --to codex --model gpt-5
maestro swap "$SID" --handoff-auto --to claude --include "focus on the retry logic"

maestro accounts <list | add | reauth | remove | rename | color | strategy | use | usage | refresh | capacity | cancel-login>

Manage claude/codex/kimi login accounts.

  • list every account with nickname, login state, limit state
  • add [--nickname <n>] [--provider <claude|codex>] opens a login session and watches for the completed /login (default provider: claude)
  • reauth <id> re-authenticates an existing account in place — opens a login session against its own home, no new account created
  • remove <id> [--force] [--migrate] deletes an account (--force if a session still uses it)
  • rename <id> "<nickname>" · color <id> <hex> set the nickname/swatch
  • strategy <useEvenly|mostAvailable5h|mostAvailable7d|sequential|rotate|ask> how a new agent session picks an account
  • use <id> sets the active account
  • usage [--refresh] each account's usage window · refresh [id] re-polls usage and renews a near-expiry token
  • capacity [--provider <claude|codex|kimi>] headroom across the fleet, for deciding whether there's room to start more work
  • cancel-login <sessionId> abandons an in-flight login session
maestro accounts list
maestro accounts strategy mostAvailable5h

Config & lookup

maestro settings <list [group] | get <key> | set <key> <value>>

Read or change maestro settings. list writes the full settings catalog (keys, labels, descriptions, current values) to a file and prints its path — read that file to find the right key, then get/set it. Agent access is gated by agentAwareness.settingsAccess.

maestro settings list
maestro settings set terminal.fontSize 14

maestro commands [--search <q>]

List every maestro command and its current keyboard shortcut(s). Writes the catalog (id, title, category, description, shortcuts) to a file and prints its path — same pattern as settings list. --search narrows by id/title/category substring.

maestro commands --search split

maestro keybind <command> <chord> | --add <chord> | --remove <chord> | --unbind | --reset | --list

Rebind an app command's keyboard shortcut: a bare chord replaces, --add adds a second, --remove drops one, --unbind removes all, --reset reverts to default, --list prints the effective bindings. Gated by agentAwareness.settingsAccess.

maestro keybind session.split "cmd+d"

maestro docs [<query>]

Answer "how do I / what does X do" questions about Maestro itself. With a query it returns the matching how-to topic(s); with none it prints an index of topics plus the other lookup verbs.

maestro docs "how do I restore a closed tab"

maestro help  ·  maestro launch

help prints the built-in usage doc — every verb, every flag, always current. launch — or bare maestro with no verb — launches the app, or focuses it if it's already running.

maestro help

Analytics & extensions

maestro view <analytics|settings|accounts> [--window | --overlay]

Open a full view — analytics, settings, or accounts — as its own window (default) or as an in-window overlay (--overlay).

maestro view analytics

maestro timeline [--from <ms>] [--to <ms>] [--stats]

Query the activity timeline for a time range (epoch-ms). --stats also returns per-day rollups: KPIs, model split, concurrency, the bottleneck breakdown, and leverage/parallelism.

maestro timeline --from 1721000000000 --stats

maestro ext <list | install <path> | uninstall <id> | reload>

Manage extensions. list shows every installed extension with its contributions and any load errors; install <path> copies a local extension folder into the user extensions dir; uninstall <id> removes it (built-ins can't be uninstalled); reload re-scans by relaunching the app. Extensions run as trusted code with full filesystem access — only install ones you trust.

maestro ext list

Maintenance

maestro ps

Per-session CPU and memory as JSON, one row per non-archived session, sorted by CPU descending: id, title, cpu, rssMb, adopted, floor, measured. cpu is a percent summed per core like top312 means about 3.1 cores.

It reports honestly rather than guessing. measured:false means the session couldn't be sampled — cpu/rssMb are null, the row sorts last, and a reason says why: dormant (asleep, nothing running), exited, no-readings, or unsampled. On a measured row cpu is still null after only one sample, since the figure is a delta between two. rssMb sums the whole process tree and so double-counts shared pages — treat it as an over-estimate. adopted is non-null when part of the figure comes from a build daemon outside the session's own tree (a Bazel server reparented to PID 1) that this session is waiting on, split evenly when several sessions share one. floor is true when the number is a lower bound, because the session churned through processes that started and finished between samples.

maestro ps | jq '.[] | select(.measured) | {title, cpu, rssMb}'

maestro render-mark [<target>] [--note <text>]

Drop a timestamped marker into the logs the instant you see a terminal-rendering glitch (garbled or scrambled glyphs), so the surrounding renderer diagnostics can be read backwards from it. Run it from another terminal without touching the affected window. Set logging.level to debug to capture the surrounding detail.

maestro render-mark --note "garble after scrolling back"

maestro bug-report "<what happened>"  ·  maestro report --type bug|feedback --text "<message>"

bug-report spins up a run that reads logs, state, and version, then shows a paste-ready handoff in a copy popup. report does the same without the interactive field — --type bug runs the log investigation, --type feedback (the default) builds a lightweight description + environment report with no investigation.

maestro report --type feedback --text "would love vertical split panes"

maestro reap [--session <id> | --worktree <path>] [--dry-run]

Kill leaked child processes (test runners, dev servers) left behind by a session or worktree. --session <id> reaps that session's process tree; --worktree <path> reaps maestro-marked processes still running under that path; with neither, it sweeps every leaked maestro-marked orphan. --dry-run reports what would be killed without killing it.

maestro reap --dry-run

Examples

# spawn a claude session under the focused one, task assigned at spawn — keep its id
SID=$(maestro open --kind claude --child --title "api tests" --prompt "run the api tests and fix failures")

# block until it finishes, then read what it's doing
maestro wait "$SID" --until idle --timeout 600
maestro list | jq --arg s "$SID" '.[] | select(.id == $s)'

# type a command into it and submit (Enter is sent by default)
maestro send "$SID" "npm test"

# already have a tab but only know its title? resolve once, then use the id
SID=$(maestro id "api tests")

# swap this tab's agent for codex, keeping the tab in place
maestro swap "$MAESTRO_SESSION_ID" --to codex

# compact this session, then keep going
maestro compact "$MAESTRO_SESSION_ID" --then-send "continue"

# pop this session out into its own window
maestro move "$MAESTRO_SESSION_ID" --new-window

# archive this session and its whole subtree
maestro archive "$MAESTRO_SESSION_ID" --cascade --force