maestro-cli) for sending messages to agents, browsing sessions, running playbooks, managing settings, and controlling resources from the command line, cron jobs, or CI/CD pipelines. It runs on the Electron runtime that ships inside Maestro, so it needs no separate Node.js.
Installation
The CLI is bundled with Maestro as a JavaScript file,maestro-cli.js, in the app’s resources folder. It must run under the app’s own executable with ELECTRON_RUN_AS_NODE=1 set, not under a system node: its SQLite module (used by Cue, maestro-cli host, and cue activity) is built for the app’s Electron and does not load in a system Node.js, so there host start reports Cue as failed and cue activity cannot read cue.db.
The simplest way is to let the app write the wrapper: Settings → General → Maestro CLI → Install / Update CLI. It writes ~/.local/bin/maestro-cli (maestro-cli.cmd on Windows) in exactly that form and adds ~/.local/bin to your PATH. Run it again after moving or updating the app.
To write the wrapper by hand, use the line for your install:
/tmp/.mount_*) that disappears when it exits, so neither a hand-written wrapper nor Install / Update CLI run from the AppImage can point into it. Extract it once to a directory that stays, and point the wrapper there:
%LOCALAPPDATA%\Programs\Maestro when installed for the current user (the default), or in %ProgramFiles%\Maestro when installed for all users; you may also have picked another folder. Set $MaestroDir to yours, then run in PowerShell (no Administrator needed):
%USERPROFILE%\.local\bin to your user PATH. The portable build (Maestro-Portable-*.exe) unpacks itself to a temporary folder on each launch and deletes it on exit, so it cannot back a wrapper; use the installer build for the CLI.
To run a single command without a wrapper, use the same form directly:
Usage
Global Flags and Exit Codes
Two flags work on every command:
Commands exit with a standardized code so scripts and CI can branch on the failure class:
Almost every verb that needs the desktop app reports its absence the same way: the message
Maestro desktop app is not running or not reachable, exit code 3, and with --json (or on verbs that always print JSON) the code MAESTRO_NOT_RUNNING:
steer-auto-run, quick-chat, window, maestro-run and plugin list|update print the same message but exit 1 with no code, and the computer-history writes exit 1 with the message wrapped in Could not reach the Maestro app (...). In a script, treat exit 1 from these as possibly meaning the app is not running.
Who Moves the View (--background / --focus)
Focus belongs to whoever is at the keyboard. An agent may create a surface; it should not decide you ought to be looking at it. Every verb that can move the Maestro view or raise a notice therefore accepts --background, which means exactly two things: the active agent does not change, and the active tab inside any agent does not change. The surface is still created and still addressable - it lands in the tab bar the way a browser opens a background tab.
Maestro’s own system prompt tells agents to pass --background by default and to drop it only when you asked to be shown something. What follows describes what each verb does when the flag is absent, which is unchanged.
--focus is the opposite ask, and it ships on every one of these verbs even where it only names the current default. If both are passed, --focus wins.
The flag is additive: one verb’s default changed. An unflagged call behaves exactly as it always has, except refresh-auto-run, which is now background by default because its old focusing default only ever pulled you away from the agent you were looking at.
focus-agent, send --tab, open, and open-graph exist to move the view - you named that intent - so they take no placement flag. The graph in particular is a full-window overlay whose only effect is being looked at, so a background one would do nothing at all.
switch-mode is the one verb here that creates nothing: changing an agent’s rendered surface is its entire effect, so there is no background surface to leave behind. --background there means “skip it rather than move me”, and it only refuses when the target is the agent already on screen. Switching an off-screen agent changes no pixels and goes through normally.
dispatch is two verbs wearing one name. With --new-tab it creates a tab you will address by the id it prints, so it is background by default. Without it, it writes into an existing conversation and selects that agent, as it always has. create-worktree --message uses the same write path, and carries whatever placement you asked create-worktree for - so --background no longer creates the agent quietly and then yanks you onto it one message later.
refresh-files accepts --background and ignores it, because it never moved the view or said anything in the first place: the Files panel it refreshes is only drawn for the agent already on screen. That is deliberate rather than an oversight. The advice given to agents is “pass --background unless the user asked to be taken there”, and an unknown option is a hard error - so one verb that refused the flag would turn a good habit into a failed command.
Errors ignore placement. A command that fails still raises its toast with --background set: the flag decides where a surface goes, not whether you get to hear that something broke.
Sending Messages to Agents
Send a message to an agent and receive a structured JSON response. Supports creating new sessions or resuming existing ones for multi-turn conversations.outcome says how the turn ended:
For providers that stream JSON lines (everything except Claude Code), a captured answer outranks a bare non-zero exit, but not a specific provider error: a turn whose stderr says the login expired fails even if some text was produced. Claude Code keeps requiring a clean exit. A process killed by a signal nobody requested is always
crashed.
The first Ctrl+C stops the agent gracefully (SIGTERM, then SIGKILL after 5 seconds if it ignores it) and exits with code 130; a second Ctrl+C exits immediately. Other failures exit with 1. Over an SSH remote this stops the local ssh client, and the remote process is not guaranteed to receive the hangup.
On failure, success is false and an error field is included:
Every
send writes an entry to the agent’s History, the same file the desktop History panel reads, whether or not the desktop app is running (an open app picks it up live). Who started the send decides the kind of entry. A send Maestro Cue started (a command action that runs maestro-cli send, or an agent Cue woke that runs it) is a CUE entry, and the turn it runs is marked MAESTRO_QUERY_SOURCE=cue. A send run from another agent’s shell is an AGENT entry whose summary opens with Sent by <agent>:. Both carry a pass/fail indicator. A send from your own shell or a script is a USER entry. A successful turn is summarized by a short synopsis turn resumed on the same provider session at the cheapest model and effort, unless that would shrink the context window, in which case the agent’s own model is kept. If the synopsis reports nothing to record, fails, or prints a provider error, the first line of the response is used instead. After a --read-only send the synopsis turn is read-only too. Failed and interrupted turns are recorded without a synopsis turn. A History write that fails prints a warning on stderr and never fails the send; the JSON on stdout is unchanged either way. Use --no-synopsis to skip the extra turn, or --no-history to record nothing.
For desktop-handoff workflows (route the message through a desktop tab, return an addressable tab id, etc.) use maestro-cli dispatch instead.
Error codes: AGENT_NOT_FOUND, AGENT_UNSUPPORTED, <AGENT>_NOT_FOUND (e.g. CLAUDE_CODE_NOT_FOUND, CODEX_NOT_FOUND), MAESTRO_NOT_RUNNING, COMMAND_FAILED.
Supported agent types: any installed provider - claude-code, codex, opencode, factory-droid, copilot-cli, hermes, pi, qwen3-coder, omp.
Messages that start with a dash
Messages whose first character is a Unicode dash (U+2014 em dash or U+2013 en dash), a double hyphen (--), or an ASCII hyphen/minus (-) collide with option parsing and will be rejected as unknown flags. Use the standard -- end-of-options separator so the message is passed verbatim:
-- is treated as positional, so any flags you need must come before the separator. For send that’s -s, -r, -t (-t is the boolean focus flag here); for dispatch it’s -t/--tab, --new-tab, -f.
Dispatching to a Desktop Tab
dispatch hands a prompt to an agent in the running Maestro desktop app and returns the tab/session id, so callers can address the same tab on follow-up calls without holding a persistent channel. Use this for orchestration use cases (Cue pipelines, external bots, multi-step automations).
sessionId and tabId are the same value, duplicated so polling consumers can use either name:
Error codes:
INVALID_OPTIONS, AGENT_NOT_FOUND, FORCE_NOT_ALLOWED, MAESTRO_NOT_RUNNING, SESSION_NOT_FOUND, DISPATCH_REFUSED, NEW_TAB_NO_ID, COMMAND_FAILED. DISPATCH_REFUSED fires when the desktop declines the prompt, most often because the target tab is mid-turn (use --queue to wait for it). NEW_TAB_NO_ID fires when the desktop app acknowledges --new-tab without returning a tab id, leaving callers nothing to chain follow-up dispatches against. Requires the Maestro desktop app to be running.
Dispatch with Callback (--notify-on-complete)
dispatch is fire-and-forget: it returns a tab id and exits, and nothing tells the caller when that delegated run finished. --notify-on-complete closes that loop. When the dispatch finishes, Maestro starts a real turn in the calling agent’s live tab carrying the result plus a handle to the full output, so an orchestrator can run build -> review -> fix -> verify without a human relaying “it’s done, carry on”.
Semantics
- Correlated. The callback is bound to the
(target agent, target tab)pair established at dispatch time, and only arms once the dispatched process actually starts. Other tabs of the same agent, and a predecessor turn a--queued dispatch is waiting behind, never trigger it. This is why an explicit--new-tabor--tabis required. - Fires exactly once, on final completion. A second exit is a no-op.
- Auto Run aware. If the dispatched prompt starts an Auto Run, the callback waits for the whole batch, not for task 1. A 6-task run wakes the caller once.
- Retry aware. If the provider refuses the turn for quota or availability and Agent Resilience holds it for a resend, the callback waits for the resend and reports how that went. It reports
failedonly if the retry is stopped or given up, and the callback timeout still applies while it waits. - Carries a handle, not just a slice. The inlined result is capped at 5000 characters (same as Cue’s
{{CUE_SOURCE_OUTPUT}});{{DISPATCH_TARGET_ID}}and{{DISPATCH_TAB_ID}}let the caller read the untruncated transcript withmaestro-cli session show <tabId>. - Busy-safe delivery. The wake-up turn goes through the same execution queue
dispatch --queueuses, so a busy caller gets it on its next idle turn instead of having it dropped or interleaved. - Lands where it was asked for. The wake-up goes to the tab that ran
dispatch(or--callback-tab). When there is no such tab, because the dispatch came from a Cue or bot run, from your own shell, or the tab was closed meanwhile, it opens a new background tab in the caller agent. It never lands in whatever tab happens to be active. - Self-cleaning. Entries live in a main-process registry, never in
cue.yaml. They expire on timeout and are dropped when the dispatch is rejected. They do not survive a desktop restart. - Non-blocking. The dispatching turn ends normally; the callback opens a new turn later.
--callback-prompt):
{{DISPATCH_CALLBACK_ID}}, {{DISPATCH_TARGET_ID}}, {{DISPATCH_TARGET_NAME}}, {{DISPATCH_TAB_ID}}, {{DISPATCH_STATUS}} (completed | failed | timeout | cancelled), {{DISPATCH_EXIT_CODE}}, {{DISPATCH_DURATION}}, {{DISPATCH_OUTPUT}}, {{DISPATCH_OUTPUT_TRUNCATED}}, {{DISPATCH_TASKS_COMPLETED}}, {{DISPATCH_TASKS_TOTAL}}, {{DISPATCH_PROMPT}}.
[!NOTE]Additional error cases: a callback agent that cannot be resolved ({{DISPATCH_OUTPUT}}is another agent’s output landing in your agent’s prompt - the same trust model as Cue’s{{CUE_SOURCE_OUTPUT}}and cross-agent consults. The default wrapper fences it and labels it as untrusted data. Keep that fencing if you supply your own--callback-prompt.
AGENT_NOT_FOUND), a callback that targets the dispatch tab itself, a tab that already has an armed callback (CALLBACK_ALREADY_ARMED), and callback flags passed without --notify-on-complete (all INVALID_OPTIONS).
Listing Sessions
Browse an agent’s session history, sorted most recent to oldest. Supports pagination with limit/skip and keyword search.
JSON output includes full session metadata:
claude-code agents.
Session Inspection
Inspect open AI tabs across the running Maestro desktop app and read their conversation history. Pairdispatch --new-tab (writes, returns a tabId) with session show <tabId> (reads, supports --since and --tail) to build a stateless poll loop without owning a persistent connection - used by Maestro-Discord and Cue follow-ups.
Both verbs talk to the running desktop over the same WebSocket as dispatch. There is no on-disk fallback: if the app is not running, the CLI reports MAESTRO_NOT_RUNNING and exits 3.
List Open Tabs
Flatten every open AI tab across every Maestro agent into addressable entries:state (busy / idle), star (★ if starred), tabId, agent name + id, tab name, createdAt (relative). One tab per line so the output pipes cleanly into grep, awk, etc.
JSON envelope:
tabIds with jq: maestro-cli session list --json | jq '.sessions[].tabId'.
Show Conversation History
Print a tab’s conversation log, with optional cursor (--since) and cap (--tail) filters applied desktop-side so the wire payload stays small even on long conversations.
JSON shape:
role is a coarse classification (user | assistant | system | tool | thinking | error | unknown) so conversational consumers can branch on intent; the raw source is preserved alongside for callers that need to discriminate further. ISO timestamps are emitted verbatim so a messages[-1].timestamp from one call can be fed directly back into --since on the next.
Error codes: MISSING_TAB_ID, TAB_NOT_FOUND, INVALID_OPTION, MAESTRO_NOT_RUNNING, COMMAND_FAILED. All errors are emitted as { "success": false, "error": "...", "code": "..." } with exit code 1, except MAESTRO_NOT_RUNNING, which exits 3.
Pasted Images (image list / image save)
An image pasted into a Maestro chat reaches the agent as pixels in its context: the agent can see the screenshot, but it has no path to the file, so “save that to the repo” used to be a right-click only you could perform. These two verbs give the agent the same reach.
image list prints one image per line: index, handle, age, agent, tab, and the first line of the message it came in on.
Details worth knowing:
- Scope it with
-a/-t. With neither, the scope is every agent, so “the newest image” is whatever was pasted most recently anywhere in the fleet. An agent saving its own conversation’s screenshot should pass its own agent id. - The target is an index, a handle, or
latest(the default). A handle is the leading hex of the image’s content hash, which is stable, so it keeps addressing the same picture as newer images push the indexes down. - The extension follows the bytes, not the name you asked for. Saving a JPEG as
shot.pngwritesshot.jpg, because an extension that lies about the encoding is a file every downstream decoder rejects. The path actually written is what the command prints. - Nothing is overwritten by accident. A generated name that collides gets a
-2,-3suffix; an explicit--outputthat already exists fails until you pass--force. - With
--all,--outputis always a folder and is created if it does not exist, even when the scope happens to hold a single image. - The Files panel refreshes itself. After writing,
image savenudges the tree for whichever agents own the written paths, so the image appears without waiting for the panel’s timed refresh (--jsonreports them asrefreshedAgents). The nudge is best-effort: the bytes are already on disk, so a closed desktop is not a failed save. - Reads come from disk, not the running app, so these work with the desktop closed. The cost is the renderer’s two-second persistence debounce: an image pasted this instant may not be on disk yet, and
image savesays so rather than guessing.
AGENT_NOT_FOUND, NO_IMAGES, IMAGE_NOT_FOUND, AMBIGUOUS_IMAGE, IMAGE_MISSING, FILE_EXISTS, WRITE_FAILED, INVALID_USAGE.
Creating, Updating, and Removing Agents
Create, mutate, or delete agents directly from the command line. Requires the Maestro desktop app to be running.update-agent mutates an existing agent in place, writing the same live desktop Session the Edit Agent modal edits (not the per-agent config store that settings agent set writes). Read the current values back with maestro-cli show agent <id> --json.
The group update reuses the same write path as drag-and-drop in the Left Bar. The cwd update moves the agent as a whole: the working directory, the project root the Files panel and Edit dialog read, and an Auto Run folder that lives inside the old directory all follow the new path (an Auto Run folder elsewhere is left where you put it). Provider conversations stored under the old path may not resume from the new one. Stop the agent before changing its cwd or SSH config; the underlying PTY’s working directory and spawn target are fixed at launch time, so the renderer refuses those updates while the agent is busy or its process is alive and surfaces the reason on stderr. The remaining settings (nudge, messages, model, effort, env, token source, etc.) are spawn-time values and apply on the next launch, so they are accepted even while the agent is running.
For text fields, passing an empty string (for example --nudge "") clears the field. --env replaces the environment map with the provided pairs; --clear-env empties it. --context-window 0 (or none) clears the context-window override. --token-source only carries meaning for Claude Code agents: api uses claude --print (per-token API credit), tui drives the maestro-p TUI (Max-plan quota), and dynamic starts on the TUI and falls back to API when a usage window hits its limit. The tui and dynamic modes need the maestro-p helper on PATH; it is bundled locally, but for SSH remotes it must be installed on the remote host. See Provider Notes. --codex-transport only carries meaning for Codex agents: exec (the default) runs codex exec, and app-server is an experimental opt-in that lets a running chat turn be steered, on this machine or an SSH remote (which needs a codex there that has app-server). Image, Auto Run, and Cue turns keep using codex exec either way.
--provider on its own is a plain switch, the same as Edit Agent with Convert memory and session history unchecked: tabs keep their transcripts, each starts a fresh conversation on the new provider, and switching back resumes the old one. Provider-specific settings are parked per provider and restored on the way back. JSON output reports converted: false and any notices (queued messages whose model or effort could not carry across).
--convert opts into the desktop’s native conversation translation flow; every translation option below implies it. Review it with --dry-run --json, take the returned plan.id, then apply that ID with --migration-plan and --accept-translation-limitations if you accept the reported limits (--force is an alias). Acceptance does not bypass a blocked plan. --migration supports plan, apply, status, recover, and rollback; --translation-resource selects an individual user resource and is repeatable. Tabs, transcripts, drafts, queued messages, and file previews are preserved. This changes the agent’s active provider. Inspect the reported state and exit status before reporting completion; a deferred or failed switch exits nonzero. Provider translation requires a matching desktop build and is refused by a headless host without changing agent state. See Provider Notes for supported versions, live evidence, and remaining limitations.
The flag table below covers
create-agent:
duplicate-agent <agent> copies what the Left Bar’s Duplicate… copies, through the same rules the New Agent modal prefills from: provider, working directory, group, additional directories, nudge and new session messages, custom path, args, env vars, model, effort, context window and provider path, Adaptive Mode, Agent Resilience, Codex automatic resets and transport, and the SSH remote. It does not copy tabs, history, the Auto Run folder (the copy gets <cwd>/.maestro/playbooks), or SSH history-sync flags. A name already in use is refused, as in the modal; sharing the source’s directory prints a warning. The Pianola manager cannot be duplicated. Read the copy back with show-agent <new-id>.
Creating, Updating, and Removing Groups
Manage Left Bar groups from the command line, including their appearance and nesting, so a bootstrap script or CI job can reproduce a whole workspace layout without anyone clicking through the desktop UI. Requires the Maestro desktop app to be running. Use a group ID withcreate-agent -g or update-agent --group to place agents into it, and update-agent --group none to move an agent back out.
remove-group refuses a non-empty group unless you pass --force, so you don’t accidentally scatter a populated group. Group IDs support partial-ID resolution.
Group appearance
--emoji and --icon are mutually exclusive: a group shows one or the other. --color combines with either, and it is also fine on its own.
Built-in icon IDs: folder, briefcase, rocket, code, star, heart, lightbulb, target, calendar, book, layers, shield, wrench, palette, archive, zap. Icons contributed by a plugin use their namespaced ID (my-plugin/my-pack/my-icon) and round-trip unchanged.
Colors are #RRGGBB hex values, normalized to uppercase before they are stored, or a plugin’s namespaced color ID.
Notes on behavior:
- Nothing half-applies. Every flag is validated before the command talks to the desktop, so an invalid icon or color leaves the group exactly as it was.
- Clearing is explicit. Passing
--iconnever silently drops an emoji you set earlier, and vice versa; use the--clear-*flags to remove a value.--clear-emojirestores the default folder emoji.--clear-parentpromotes a nested group to the top level. - Writes are verified. After the desktop reports success, the CLI reads the group back from storage and confirms it matches what you asked for. If a desktop app older than your CLI accepted the command and ignored the icon or color, the command fails with a version-mismatch message instead of reporting a success that did not happen.
- Icon and color survive the Groups+ feature gate. Turning Groups+ off falls back to the legacy emoji presentation but does not discard stored icon and color; turning it back on restores them.
- Group nesting is one level deep: a root group can hold child groups, but a child group cannot hold its own children.
maestro-cli list groups --json reports icon, color, parentGroupId, and collapsed alongside the existing fields, so a script can read back exactly what it set.
There is no reorder verb: the Left Bar sorts groups and agents by name, and a drag only changes membership. Move an agent with update-agent --group (its worktree children travel with it), and a group with update-group --parent / --clear-parent.
create-group flags:
update-group flags:
remove-group flags:
Moving Agents and Groups Between Windows (window)
The CLI half of the Left Bar’s Move to Window menus. move-agent carries one agent with its worktrees; move-group carries every agent in a group (and its subgroups when Groups+ nesting is on), gathering a group that is split across windows into one. The primary window answers through the same code path as a click, so the same rule applies: a move that would leave the main window with no agents is refused.
--to takes new, main, a window number (as shown by window list and the Left Bar badge), a window id, or a custom window name. Agent and group IDs accept the usual partial-ID resolution. Moving onto the window that already holds everything succeeds and moves nothing.
Creating Worktree Agents
Branch a new agent off an existing parent agent into its own git worktree, without an Auto Run playbook. This mirrors the desktop “create worktree” flow: the parent agent must already exist in the running app, the desktop creates the worktree on disk and a child session linked to the parent, then hands back the new agent’s ID.show agent <id> --json reports it as worktreeBasePath), so it appears in the Left Bar like one made from the desktop. This is the only supported way for an agent to create a worktree: a bare git worktree add produces a checkout the desktop never learns about.
The optional --message is delivered to the new agent as a plain prompt (not an Auto Run loop) on the same connection, addressed by the ID the desktop just returned. Both --agent and --branch support the usual partial-ID resolution.
Driving the Workspace (Focus, Mode, Tabs)
Steer the desktop UI itself: focus an agent, flip an agent between AI and terminal mode, and manage an agent’s AI tabs. These mirror clicking around the app and require the desktop app to be running.tab reopen prints the restored tab’s new ID; without -a it acts on the agent selected in the desktop, and an empty stack or a bad --index / --tab exits 1 with a message. The closed-tab stack is kept across restarts, capped at 25 entries per agent.
Message Pins
tab pins is the CLI half of the pin button on a message and the pin panel (see Pinned Messages). Every verb answers with the tab’s pins after it ran, so --json output is enough to verify a change. Only user messages and agent replies can be pinned. add takes any transcript entry ID (exact or a unique prefix; a later chunk of a streamed reply pins the whole reply) or --last user|agent, and pinning a message twice reports alreadyPinned instead of failing. A pin listed as loaded: false points at a message older than what the tab has read back. remove and rename take a pin ID (exact or a unique prefix). show selects the agent and the tab, then opens the panel; it refuses a tab with no pins.
tab compact prints the compacted tab’s ID and how much smaller its context is; it refuses a tab too small to compact. tab merge sends into the target agent’s active tab unless --target-tab names another (another tab of the same agent works too); with --background the context waits on that tab and sends when it is next opened, which the output says. tab transfer prints the new tab’s ID in the target agent; a busy target queues the context behind its running turn. A compaction can take minutes, so the CLI waits up to 10 minutes; if any of the three runs out of time it exits 5 and says the operation may still be running, rather than reporting a failure.
tab new, tab reopen, tab compact, tab merge, tab transfer, and switch-mode take --background / --focus; see Who Moves the View. switch-mode --background refuses when the target is the agent already on screen, since the mode change is the view change there, and tells you to re-run with --focus if you meant it anyway.
Find tab IDs with maestro-cli session list. tab new returns the new tab’s ID (printed, or in the JSON payload with --json). Every verb that takes a <tab-id> also accepts the literal active, which resolves to the tab the agent currently has selected - -a <agent-id> says whose, and without it the CLI uses the agent the desktop has focused.
An agent running inside Maestro gets its own tab ID in its system prompt (the Tab ID line under Session Information, from the {{TAB_ID}} variable), so you can just tell it “close this tab” or “rename this tab to Docs” and it will act on the right one. Every other entry in session list is a different live conversation, so agents are instructed never to guess a tab ID from that list.
Bookmark, unread, star, and save-to-history are explicit set operations rather than toggles, so re-running a script lands on the same state either way. tab thinking is the one exception, and only in its cycle form, which advances one step the way clicking the chip does. Read the current values back with maestro-cli tab show <tab-id>, maestro-cli show agent <id> --json (field bookmarked), or maestro-cli session list --json (fields starred, thinking, readOnly, model, effort, saveToHistory, enterToSend, active).
model, effort, and enter-to-send are per-tab overrides: inherit clears the override so the tab follows the agent’s model/effort or the global enterToSendAI setting again. That is not the same as false - tab enter-to-send <tab-id> false pins the tab to Cmd+Enter even when the global default is Enter. Agent-wide defaults still live on maestro-cli update-agent <id> --model/--effort. bookmark also has a flag form, maestro-cli update-agent <id> --bookmark true, for when you are already changing other agent settings in the same call.
Stopping a Running Turn (interrupt)
Stop a turn another agent cannot get out of, the same as clicking the red Stop pill. It requires the desktop app to be running.
--tab, every busy tab of the agent stops; --tab leaves the agent’s other tabs running.
The verb is safe to re-run. When nothing is running it prints Nothing to stop and exits 0 (wasBusy: false in JSON). A failed stop exits 1, and a desktop that never answered exits 5. The JSON payload is { success, agentId, tabId, wasBusy, interrupted, stoppedTabIds, exited }. exited: true means the stopped processes are gone, so maestro-cli tab show <tab-id> (the State row) and maestro-cli session list --json (field state) already read idle; when it is false, the CLI prints the tab show command to check again.
An agent cannot interrupt itself from inside its own turn: the verb refuses the caller’s own agent, or --tab naming the caller’s own tab. A sibling tab of the same agent is allowed.
Live Mode (live)
Read or switch Live mode, the LIVE pill in the Left Bar that opens the web interface to other devices on the network, and Remote Control, the Cloudflare tunnel that opens it to the internet. It requires the desktop app to be running.
live on and live off run the same main-process switch as the pill, so every open window’s pill follows. They are explicit set verbs, safe to re-run: when Live is already in the requested state nothing restarts, the access token stays the same, and the command prints Live mode was already on (changed: false in JSON). live off also stops Remote Control (the tunnel), as the pill does; live on starts it only with --tunnel.
Remote Control needs cloudflared on your PATH (macOS: brew install cloudflared), and refuses while Live is off, because the only server running then is the local one maestro-cli itself talks to. It does not restart the server, so live tunnel on simply waits for the tunnel (up to 45 seconds) and prints its public URL. The LIVE panel’s Remote Control row follows whatever the CLI does.
Switching Live replaces the server the CLI talks to, so the desktop answers first and then switches. The CLI reconnects and polls live status until the switch finishes (up to 20 seconds), then prints the result. A failed switch exits 1 with the desktop’s error, which live status also shows as Last error until the next success. The JSON payload is { success, changed, live, url, remoteControl, remoteControlUrl, remoteControlError?, revision, lastError? }; live status --json has the same fields without changed.
[!WARNING]
live on opens the web interface to every device that can reach this machine on the network, with no confirmation, the same as clicking the pill. Anyone holding the URL (it carries the access token) can drive your agents.
Listing Resources
Running Playbooks
Goal-Driven Auto Run
Spec-Driven runs (theplaybook command above) work through a checklist of documents. Goal-Driven runs instead pursue a single free-text objective: each iteration spawns a fresh agent that makes one increment of progress, reports how far along it is, and exits, repeating until the goal is reached, a deadlock is declared, the iteration limit is hit, or progress stalls.
The run writes an immediate “started” history entry (recording the goal and exit criteria), one entry per iteration, and a final summary with the stop reason and final progress. Goal-Driven runs honor the same per-agent SSH remote and model/effort/args overrides as
playbook, and refuse to start if the agent is already busy in the desktop app or another CLI instance.
JSON event stream: goal_start, goal_iteration_start, goal_iteration_complete (carries progress, rationale, complete, deadlock), and goal_complete (carries success, exitReason, finalProgress, iterations).
Visible runs (--visible)
By default goal-run is headless: the CLI spawns the agent itself, so the
run is real but only observable through the CLI stream and history entries. It
never appears in the agent’s Auto Run surface.
--visible hands the run to the running desktop app instead. It then
behaves exactly like a run started from the Auto Run modal’s Go button:
it shows up in the Auto Run panel, maestro-cli stop-auto-run -a <agent-id>
stops it, and maestro-cli session list reports it. The CLI returns as soon as
the desktop confirms the run has started rather than streaming iterations, and
prints a maestro://session/<agent-id>/tab/<tab-id> deep link that reopens it.
Use it when an orchestrating agent needs to launch a worker the user can watch;
use the headless default for CI and scripting.
--visible fails closed. If the desktop app is not reachable it reports
MAESTRO_NOT_RUNNING and exits 3 rather than quietly running headlessly, because a silent
fallback is invisible in exactly the surface you were pointing at. Other stable
failure codes: AGENT_BUSY (the agent already has an Auto Run going - pass
--wait to queue instead), AUTO_RUN_DISABLED, SESSION_NOT_FOUND,
LAUNCH_TIMEOUT, and UNSUPPORTED_COMMAND (the desktop app is an older build
than the CLI - rebuild and restart it).
With --json, a successful launch emits a single visible_launch event:
--new-tab / --tab: the run surfaces on whichever AI tab is active, and that
is the tab the returned deep link addresses.
Running Documents Without a Playbook (run-doc)
run-doc runs one or more Auto Run .md documents directly, without saving a playbook first. Like playbook, it runs headlessly - it spawns the target agent itself and streams events, so it works whether or not the Maestro desktop window is open. This is the reliable way to execute a document an agent just wrote.
run-doc accepts the same execution flags as playbook (--dry-run, --no-history, --json, --debug, --verbose, --no-synopsis, --wait, --model, --effort, --ignore-model-hints) plus --prompt, --loop, --max-loops, and --reset-on-completion. When no --prompt is given it uses the default Auto Run prompt.
Per-run model override
All four Auto Run entry points -playbook, run-doc, goal-run, and
auto-run - accept --model <model> and --effort <effort>. Both are
run-scoped: they apply to every agent spawn the run makes (including the
per-task synopsis and goal-handoff spawns) and take precedence over the agent’s
configured model, but nothing is written back to the agent. When the run ends,
the agent is exactly as it was.
playbook, run-doc, and auto-run also take --ignore-model-hints. It skips
every MAESTRO:MODEL marker in the run’s documents, so each task runs at
--model / --effort (or the agent’s configured default when those are
omitted) instead of the tier the document asked for. goal-run has no
documents, so it has no such flag.
- Omitting the flags keeps the existing behavior exactly: the run uses the agent’s configured model and effort.
- Valid values are provider-specific (for example
sonnet/opusfor Claude Code). The CLI passes the value through rather than validating it against the provider’s model list. --effortonly does something on providers that expose a reasoning-effort setting; it is ignored elsewhere.- The headless commands (
playbook,run-doc,goal-run) print aModel: <value> (this run only)line in human-readable (non---json) output so you can confirm which model the run used. JSONL output is unchanged.auto-runhands the run to the desktop app, so confirm that one in the desktop UI instead. - The desktop Auto Run launch modal has the same two pickers, both defaulting to “Use agent default”. See Auto Run.
playbookvsrun-docvsauto-run --launch: useplaybook <id>for a saved playbook andrun-doc <docs>for raw documents - both run headlessly with no desktop dependency.auto-run --launchinstead hands the run to the running desktop app (needed only when you want the run to appear and be controlled in the desktop UI).
--agentaccepts a name: the-a, --agentflag on these commands resolves an agent by ID (full or partial) or by display name. This lets a group-chat participant target itself with--agent "<its name>".
Prompt Customization
The CLI uses the same core system prompts as the desktop app. When you customize prompts via Settings → Maestro Prompts, those customizations are stored incore-prompts-customizations.json in the Maestro data directory and are automatically picked up by the CLI during playbook runs.
The prompts most relevant to CLI playbook execution are:
To customize these prompts, either use the desktop app’s Maestro Prompts tab or edit the JSON file directly:
Reading Prompts (prompts list / prompts get)
The CLI exposes Maestro’s prompt registry directly so other agents can self-fetch reference material on demand. Parent prompts can use the {{REF:name}} directive (see Prompt Customization → Include Directives) to expand into a one-line pointer; the agent then runs prompts get to retrieve the full content.
prompts get returns the same content the desktop app would deliver, so customizations made via Settings → Maestro Prompts are reflected immediately. Bundled include fragments use a leading underscore in their id (e.g., _maestro-cli, _history-format); standalone prompts do not.
Managing Settings
View and modify any Maestro configuration setting directly from the CLI. Changes take effect immediately in the running desktop app - no restart required.
Categories: appearance, editor, shell, notifications, updates, logging, web, ssh, file-indexing, context, document-graph, stats, accessibility, integrations, onboarding, advanced, internal.
Theme and Encore Features
Ergonomic, validated wrappers over the underlying settings, for the customizations users most often ask for by voice. Unlikesettings set (which writes the settings file), these route through the running desktop app, so the change applies live. The app must be running.
directorNotes, usageStats, symphony, maestroCue, pianola, quickChat, and on macOS maestroRun. Friendly aliases are accepted (for example cue for maestroCue, quick-chat for quickChat). Group Chat is a core feature with no Encore flag, so group-chat is not an alias for symphony.
autopilot, auto-pilot, and pilot remain aliases for enabling Pianola. They do not grant automatic repair permission. encore list --json includes pianola.mode (disabled, supervision, or automatic_repairs), the saved autoFixEnabled permission, and automaticMergeAvailable: false.
Automatic repairs are a separate opt-in in Settings > Plugins > Pianola > Settings > Automatic repairs, or via maestro-cli settings set pianolaAutoFixEnabled true. The setting defaults to false and is re-read before repair actions. Set it to false to revoke permission. Repair dispatches remain bounded; repair workers can change files within their existing access. Enabling repairs does not authorize merging. The current merge adapter records skipped and never executes a merge.
Legacy hand-edited encoreFeatures.autopilot values no longer grant repair permission. Review the separate setting and opt in explicitly if repairs are wanted; no migration silently enables them.
Gloss levels, least to most: off (the shipped flat look), sheen, strong, max. Gloss only adds highlights and shadows to the sidebars, headers, tab bar and composer, so it changes no theme color and leaves text exactly as legible. It has no effect on light themes.
MaestroRun (macOS)
Set up MaestroRun, the signed launcher that holds one Full Disk Access grant for your scheduled scripts. Each verb takes the same path as the matching button on the MaestroRun tile in Settings. The app must be running, and the plugin must be on (maestro-cli encore enable maestroRun).
grant cannot switch the permission on: macOS has no prompt for Full Disk Access, so you flip the switch in System Settings and status reports ready once you have. These verbs set MaestroRun up. To run a script, use the launcher itself, at the path status prints.
Dataview
Run a note’s Dataview queries and get the same result File Preview renders.this is the note, and the query runs against the index of the vault that holds it, in the running app (local or over SSH). The CLI indexes nothing itself, so the app must be running.
--block <n> counts dataview fences only, from 1. A note’s agent is the agent whose folder holds it; pass --agent <id> to choose one, and to name the SSH remote for index status and index rebuild. Tables fit the terminal width; --json prints the full result, with links as objects and dates as ISO strings. A dataviewjs block prints DataviewJS renders in the desktop app only and exits 2; a bad --block or a note no agent owns also exits 2, and a query error exits 1.
trust, untrust, and trusted edit and list the vaults whose dataviewjs blocks run without asking, the same list as Trusted Vaults in the Dataview settings and Always trust this vault on a block. The CLI never runs a script. A root is resolved against the current folder (~ works) and stored without a trailing separator; it must be the vault root as the app names it (the folder that holds .obsidian/, or the agent’s folder). untrust also ends that vault’s Run once grants.
Quick Chat
Drive the Quick Chat window, the floating chat summoned by a system-wide hotkey. Each verb takes the same path as the matching button in the window. The app must be running.Custom Theme Palette
set-theme only switches between built-in themes. The theme command group manages the user-configurable Custom theme palette (the same two settings the in-app Custom Theme Builder edits: customThemeColors and customThemeBaseId). Activate the result with set-theme custom.
theme show and theme export read the on-disk settings store directly (no running app required); theme import and theme set apply live through the running desktop app. Imports are validated the same way as the in-app importer: every required color key must be present and every value must be a valid CSS color.
Managing Agent Configuration
Each agent (Claude Code, Codex, OpenCode, Factory Droid) can have its own configuration for custom paths, CLI arguments, environment variables, and model overrides.settings agent set writes the per-agent-type configuration store (defaults applied to newly created agents and headless CLI spawns). To change the settings of a specific existing agent shown in the Left Bar - its nudge message, model, env vars, Claude token source, and so on - use update-agent instead, which writes that agent’s live desktop Session.settings agent list and settings agent get also accept an agent ID (or a unique prefix) in place of a provider type. They then read that agent’s own settings, the same fields update-agent writes and show-agent reports: codexTransport, tokenSource, customModel, nudgeMessage, and so on. set and reset refuse an agent ID and point you at update-agent.
Common agent config keys:
Codex configuration notices, such as an ignored setting, appear as
Codex warning:
diagnostics in the desktop transcript even with Thinking off. Headless CLI runs
print them to stderr. These notices do not stop the turn or become part of the
assistant’s answer; actual turn failures still use normal error handling.
Settings and agent config changes made via the CLI are automatically detected by the running Maestro desktop app. The app watches for file changes and reloads immediately - it’s as if you toggled the setting in the Settings modal yourself.
Managing SSH Remotes
Create, list, update, and remove SSH remote configurations. These commands read and write directly to the Maestro settings file - no running desktop app required.create-ssh-remote flags:
update-ssh-remote takes the same connection flags plus the ones below. --env
and --ssh-option MERGE into what the remote already has, so a single flag does
not silently drop the rest; pair them with the matching --clear-* flag to
replace the set outright. Passing an empty string to -u or -k clears it.
A command-line
-o outranks ~/.ssh/config, so --ssh-option is the only way
to change one of Maestro’s connection defaults (ConnectTimeout, BatchMode,
and friends). list ssh-remotes --json and update-ssh-remote --json both
report resolvedSshOptions, the full merged set ssh actually receives, which
is what answers “did my ConnectTimeout take effect?”. See
SSH Remote Execution for the reserved keys and the
merge rules.
SSH remote changes made via the CLI are detected by the running Maestro desktop app through file watching, just like settings changes.
Partial IDs
All commands that accept an agent ID, group ID, or SSH remote ID support partial matching. You only need to type enough characters to uniquely identify the resource:JSON Output
By default, commands output human-readable formatted text. Use--json for machine-parseable output:
complete event carrying "stopped":true. The interrupted task is recorded as “interrupted” rather than failed, and the process exits with code 130. A second Ctrl+C exits immediately. maestro-cli playbook, maestro-cli run-doc and maestro-cli goal-run all behave this way; a goal run ends with exitReason: "stopped-by-user".
send command always outputs JSON (no --json flag needed).
Desktop Integration
Commands for interacting with the running Maestro desktop app. These are especially useful for AI agents to trigger UI updates after creating or modifying files.Open a Maestro Surface (Modal or Dashboard)
Bring up one of Maestro’s modals or dashboards in the running app, optionally on a specific tab. This is how an agent answers “where do I see X?” by showing you rather than describing a menu path.
Surfaces are addressed by id or alias (
usage, stats, and dashboard all reach the Usage Dashboard). A --tab value matches either the tab id (scheduled) or its label ("Scheduled Tasks").
On success the command also prints how to reach that surface by hand:
Open a File
Open a file as a preview tab in the Maestro desktop app. Without--agent, the owning agent is auto-detected by which agent’s working directory the file lives in (longest-prefix match, most-recently-active wins on ties), and a path outside every agent’s directory is an error. Pass --agent <id> to name the agent yourself, which is also how you open a file that lives nowhere near a project.
A folder has no preview, so
open-file <folder> reveals it in the agent’s Files panel instead: the Right Bar opens on the Files tab, every ancestor and the folder itself expand, and its row is selected. Clicking a folder link in the chat does the same. --background and --no-switch expand and select without moving the view. A folder outside the agent’s file tree cannot be revealed there.
This is also the verb that plays media. A local audio or video file does not become a tab at all: it goes to the floating media player, so open-file ~/Music/track.mp3 --agent <id> is how you or an agent starts playback without handing the file to your system’s default player. There is no separate play command. --background has nothing to suppress in that case, since playback is audible either way, and a file on an SSH remote has no stream to play, so it falls back to the ordinary binary-file path.
--no-switch and --background are different asks, and this is the one command that offers both. --no-switch keeps the Left Bar selection where it is but still activates the new tab inside the target agent - so if you were already on that agent, your view still changes. --background changes nothing rendered anywhere. Passing both is fine; --background is strictly stronger and wins. If --no-switch is what you reached for, --background is probably what you meant.
Audio and video never open as a tab: they go to the floating media player, and a plain open-file starts playing. --queue puts the file in the player without pressing play. If nothing is loaded, the file loads paused and the player appears; if something is already loaded, the file lines up behind it and nothing is interrupted. A minimized player comes back either way. It never switches agents, since the player is app-wide. To hand someone a playlist to start themselves, queue each file in order:
--queue on a file that is not audio or video is an error.
Pass an http:// or https:// URL instead of a path to stream it in the player. This covers progressive MP4 (H.264 video, AAC audio) and plain audio files such as MP3 or M4A. Adaptive and live formats (HLS .m3u8, DASH, RTSP, RTMP) are not supported. A URL never becomes a tab and never switches agents; it is filed under --agent, or the agent that owns your shell’s current directory. --queue works the same as for a file:
Open a Browser Tab
Open a URL as a browser tab in the Maestro desktop app. Onlyhttp(s) URLs are accepted; scheme-less inputs like localhost:3000 or example.com:8080 are auto-prefixed with https://.
By default this switches the UI to the target agent and makes the new tab visible. Pass --background to create the tab without moving the user: the active agent is left alone and whatever tab they were looking at stays on screen. Either way the command prints the new tab’s ID, which is the handle for close-browser.
Agents doing research should always pass
--background and then close-browser when finished. A
foreground tab pulls the window away from whatever the user is doing, potentially mid-keystroke.Close a Browser Tab
Close a browser tab by the ID thatopen-browser returned. The owning agent is resolved from the tab ID, so no --agent is needed. Exits non-zero if no such tab exists, so cleanup scripts can tell a real close from a no-op.
Open a Terminal Tab
Open a fresh terminal tab in the Maestro desktop app. The working directory must resolve inside the target agent’scwd; paths outside it are rejected.
--command is stored as the tab’s startup command, the same field the tab’s right-click “Startup Command…” menu writes. The command runs as soon as the shell finishes loading its rc files, and it runs again if the tab is restarted or the app is reopened. That is what you want for npm run dev; for a one-shot command that should not come back, close the tab when it finishes, or use send-terminal instead.
The command prints the new tab’s ID. Keep it: it is the handle for send-terminal --tab.
Run a Command in an Existing Terminal Tab
open-terminal makes a new terminal. send-terminal types into one that is already open, which is what you want to drive a shell the user is watching.
Notes:
- A tab ID is matched across every agent, so an ID from
open-terminalworks without--agent. A tab name is matched only within the target agent, because names collide (three projects can each have a “Dev server”). - With no
--tab, the agent’s active terminal receives the command. If several terminals are open and none is active, the command fails rather than guessing. - The terminal must have a running shell. A tab that has never been displayed has no shell yet: open it with
open-terminal --commandinstead, or select it in the app first. - Text is typed into the shell verbatim. If something is already half-typed at the prompt, your command lands on the end of it.
Read a Terminal Tab’s Output
send-terminal types into a shell; read-terminal reads back what it printed. Without it the terminal is write-only from a script’s point of view: open-terminal --command "npm run dev" is the right way to run a long-lived process, but nothing could observe whether it came up.
--json returns { tabId, name, cwd, state, busy, totalLines, lines: [...] }. busy is the useful one for automation: it distinguishes “the command finished and this is its final output” from “it is still running and there is more to come”. totalLines is the size of the buffer before --tail truncation, so you can tell a complete read from a partial one.
Notes:
- Output is plain text. The scrollback comes from the terminal emulator, which has already interpreted the escape sequences, so there are no colour codes to strip.
- Reads are bounded on purpose. A
tail -ftab can hold an enormous buffer, and the default cap keeps it from swamping the caller; reads are capped app-side regardless of--tail. - The tab needs a live buffer. Terminals stay mounted once their agent has been on screen, but a tab belonging to an agent never visited since launch has nothing to read yet - select the agent once, then read.
- Tab resolution matches
send-terminalexactly: an ID matches across every agent, a name only within the target agent.
List Open Terminal Tabs
Terminal tabs live in the desktop app, so this asks the running app rather than reading from disk.state | active-marker | tabId | agent | name | cwd, with the startup command appended when the tab has one. * marks the agent’s active terminal (the one send-terminal writes to by default).
Refresh the File Tree
Refresh the file tree sidebar after creating multiple files or making significant filesystem changes:--background is accepted and ignored, so an agent that passes it on every command does not get a usage error here.
Refresh Auto Run Documents
Refresh the Auto Run document list after creating or modifying auto-run documents:--background), this never moves the view: an agent already on screen is refreshed silently, and an off-screen one is left alone, since its documents are re-read the moment you switch to it anyway. --focus switches to the target agent and flashes the document count on screen. This is the one verb whose default is background, because a focusing refresh only ever moved you when you were looking at a different agent.
Notifications
Surface notifications in the running desktop app from any script, hook, or agent. Two delivery modes are available, both built on the same five-color design language so they feel unified:- Toast - persistent notification that lands in the toast queue (top-right). Auto-dismisses by default. Use this when you want the user to see a result they may want to act on later, when an OS notification should also fire, or when the message benefits from being clickable to jump to a specific agent. Toasts can be made sticky with
--dismissibleso they require an explicit click to dismiss - use this for messages the user must acknowledge. - Center Flash - momentary, single-slot center-screen confirmation that auto-dismisses (default 1.5s, max 5s). Use this for “I did the thing” feedback for a user-initiated action - clipboard acks, quick status nudges, brief success notes. Only one flash is visible at a time; firing a new one replaces the active one.
Color palette (shared by both)
Both commands accept--color, one of five canonical values:
Pick
theme when you don’t have an opinion - the flash/toast will visually match whatever theme the user is running.
Toasts
The body-click hierarchy is: the
--open-* flags (mutually exclusive with each other) > --agent (+ optional --tab). A click on the body can therefore land on an AI tab, a File Preview tab, a terminal tab, an in-app browser tab, or the system browser. When the target tab has since been closed, the click still switches to the agent and says what was missing. --action-url is independent - it renders a separate inline link button and does not affect the body click.
Center Flash
Caps and dismissibility
External (CLI/web) callers are capped to 5 seconds for Center Flash and 60 seconds for Toast. The cap exists so external scripts can’t stick a permanent overlay on the user. The only way to leave a notification on screen indefinitely is--dismissible on a toast - there is no equivalent for Center Flash (it is, by design, momentary).
Both commands support --json for scripting. Toasts respect the user’s notification settings (audio feedback, OS desktop notifications) configured in the app.
Configuring Auto-Run
Set up and optionally launch an auto-run session with one or more markdown documents. Documents must be.md files containing - [ ] checkbox tasks.
--model and --effort are run-scoped: they apply to every task spawn in
this auto-run and are never written back to the agent. The agent’s interactive
tabs keep using its configured default, and the override disappears when the run
ends. Omit them to use the agent default. They are honored in worktree mode too,
without changing the child worktree session’s own configured model. See
Per-run model override for the full picture.
Every document must sit inside the agent’s Auto Run folder. A saved playbook
stores each one relative to that folder, so --save-as and --launch both
refuse a document outside it and name the file. When a saved playbook names a
document that no longer exists, maestro-cli playbook fails with
DOCUMENT_NOT_FOUND and lists the missing paths.
Worktree mode reuses the desktop app’s Auto Run pipeline: the app creates the
worktree (or reuses an existing one on the same repo), checks out the requested
branch, dispatches the agent inside the worktree, and - when --create-pr is
set - runs gh pr create once the batch completes. See
Git Worktrees for more on worktree behavior.
Controlling a Running Auto Run
Once an Auto Run is going, these commands stop it or recover it from an error pause - the counterpart to launching withauto-run --launch. They require the desktop app to be running.
reset-auto-run-tasks is relative to the agent’s Auto Run folder; absolute paths and .. traversal are rejected.
Steering a Running Auto Run
A steering note changes a run’s direction without stopping it: the note parks until the run dispatches its next task, then opens that task’s prompt. These are the CLI half of the Steer Auto Run button in the Thought Stream. See Steering a Run in Flight for what the agent is told to do with a note.send is refused when no Auto Run is active on the agent, or when the run belongs to another Maestro client: there is no next task to hand the note to. A note longer than 4000 characters is refused rather than cut to fit. list keeps working after the run ends, since the reply to a run’s last note arrives as it finishes; the notes are cleared when the next run starts. send --background queues the note without the on-screen “Steering note queued” flash.
Checking Status
Check if the Maestro desktop app, or a headless host, is running and reachable:maestro-cli host start.
Diagnosing Problems (doctor)
When a command isn’t working, doctor runs a checklist covering the most common causes in one shot: the desktop app reachable, the running build’s version vs. this CLI’s, whether the running app understands newer commands, and whether configured SSH remotes are well-formed.
Shell Completions
Generate a completion script for your shell and source it:maestro-cli reference (Markdown or --format json); docs/cli-reference.md is generated from it via npm run gen:cli-reference.
Headless Host (maestro-cli host)
maestro-cli host runs Maestro’s runtime with no desktop app: on a server, in a container, or on a laptop where the app is closed. The host owns the data directory the way the desktop does and serves the same WebSocket bridge, so maestro-cli commands attach to it exactly as they attach to the app. It runs:
- Agents: create, update, rename, move between groups, and remove them.
- Chat turns sent with
maestro-cli dispatch(including--new-taband--queue). A message to a busy agent waits its turn in the agent’s queue instead of being turned away. - Auto Run:
maestro-cli auto-run --launchand goal-driven runs, withauto-run stop,resume,skip, andabort. A run keeps going after the terminal that started it closes. - Group chats and consults between agents.
- Maestro Cue, when Cue is on in the shared settings and no other engine holds the Cue lock. This is the only way to run Cue subscriptions with
action: autorunwithout the desktop:maestro-cli cue engine starthas no runtime to run an Auto Run in and fails those runs.
host start
Starts the host as a detached process and returns once it is listening. The host keeps running after the shell that started it exits.
start reports the pid, the port, the log file, and where Cue stands. host status --json reports preventingSleep: true while a macOS host holds the Mac awake. A Cue that did not start does not fail the start: the host still serves turns and Auto Runs, and the output says Cue is not running and why.
Signals. SIGTERM or SIGINT (systemctl stop, systemctl restart, Ctrl+C on --foreground) drains the host: it waits up to --stop-grace seconds for chat turns, Auto Runs, group chat rounds, consults, and Cue runs to finish, then stops. A second signal stops it at once. New work is not turned away during the wait, so work that starts in it is waited for too, up to the same deadline. Keep the grace below your service manager’s own stop timeout (systemd sends SIGKILL after 90 seconds by default), with a few seconds to spare: Cue runs still going when the grace runs out get up to about 7 more seconds to exit after they are stopped.
Cue drains the way cue engine start does. On the first signal every trigger is disarmed at once, so no subscription fires while the host waits, and events already accepted by Cue remain queued instead of run. Cue runs in flight get the same --stop-grace; the ones still running when it ends are stopped and recorded as stopped. Queued events and fan-ins still waiting on sources are saved and pick up on the next start of whichever Cue engine serves the data directory. A second signal stops every Cue run and kills its processes at once, and still saves the queue. host stop and a host that lost its data directory drain Cue the same way with no wait. Switching Cue off in the settings while the host runs is not a shutdown: it stops the runs and clears the queue, as on the desktop.
host status
Reports the host’s pid, uptime, version, the holder of the data directory’s lock, how many clients are attached, the work in flight, and the Cue state. If another Cue engine takes the Cue lock over while the host runs (the host was suspended long enough for its lock to go stale, say), the host stops its Cue runs and gives Cue up, and the Cue state names the new holder (not running here: desktop pid 1234 holds the Cue lock) instead of running. The host does not take Cue back on its own: switching Cue off and on again in the settings, or restarting the host, tries again. With no host it exits with code 3 and names whoever holds the lock, if anyone does (a TUI hosting in its own process, say).
host endpoint
Prints the endpoint of the host serving the data directory, for Federation over SSH: a laptop runs maestro-cli host endpoint --json on the remote through the SSH remote it already has, and SSH is the authentication. With --json it prints {running, port, token, cliSecret, version, pid}, or {running: false} with exit code 3 when no host is running. Without --json it prints the pid, port and version only: the token and the CLI secret never reach a terminal or a log. On macOS and Linux it refuses, and prints neither, when the discovery file (cli-server.json) can be read by any user but its owner. The verb is listed in --help only while Federation is on.
host stop
Asks the host to stop and waits for its process to exit. With work in flight (a chat turn, an Auto Run, a group chat round, a consult, or a Cue run) it refuses, names the work, and exits 1, unless --wait or --force says otherwise.
remote-host status and remote-host start
Check, and start, the maestro-cli host on an SSH remote, for Federation over SSH. Both verbs belong to the Federation Encore Feature and are refused while it is off. Both refuse an SSH remote that is not opted in (update-ssh-remote <id> --federation on): Maestro never dials a remote without that flag. The <ssh-remote> argument is the remote’s name or its ID (a unique prefix works).
remote-host status makes one SSH call and reports whether Maestro is installed on the remote (with the fix when it is not), whether a host is running there (its pid, its port on the remote’s loopback and its version), and the version skew against this CLI: none, remote-older, remote-newer or unknown. It exits 0 with a running host and 3 when no host runs or Maestro is not installed. remote-host start runs maestro-cli host start on the remote, as the SSH user, when no host is running there, then reports status the same way. It does nothing when a host already runs. The SSH calls run one after the other, never at the same time. Neither verb prints the host’s token or CLI secret, with or without --json. A timeout exits 5, and a remote CLI too old to know host endpoint exits 4.
Agents on a Peer
A peer is an SSH remote that is opted in to Federation and runs amaestro-cli host. Its peer name is the remote’s name, or the remote’s ID when two remotes share a name. list agents --peer <name> reads the agents from that host over the SSH forward and prints them with the peer name beside each row. Each ID line reads peer:agent-id, the form other verbs take as a target. list agents --all-peers prints the local agents, then the agents on every enabled SSH remote opted in to Federation. It visits the peers one after another, never at the same time. With --json each row has a peer field: the peer name, or null for a local agent in an --all-peers list.
The host is the only record of its agents: Maestro reads them fresh every time and never writes them to the local agent store. A peer that cannot be reached is an error that names the fix (maestro-cli remote-host start <peer> when no host runs). --all-peers lists the peers it reaches first, then reports each peer it could not reach on stderr and exits with that failure’s code (3 when a host is down). Both flags belong to the Federation Encore Feature, are left out of --help while it is off, and are refused if passed then. Neither combines with --group, which filters local agents.
These verbs take a peer:agent target (an agent ID, ID prefix, or name after the first colon) and act on the host’s agent. Each prints what it prints for a local agent, plus a peer field in its JSON:
send, interrupt, and the tab verbs run the same shared action as the desktop’s mirrored agent (its composer, Stop, and the tab controls beside its tab strip), so a script and the desktop reach the host the same way. Nothing is queued or retried on this machine. A peer that is down, or a forward that is dead, exits non-zero with a PEER_* code (PEER_HOST_DOWN exits 3). If the forward drops while send waits, the turn keeps running on the host: send exits 3 with PEER_CONNECTION_LOST, the tab ID, and the command that shows the tab (maestro-cli show agent <peer>:<agent>). With Federation off, a target with a colon is an ordinary local agent ID or name.
Lift an SSH Agent Onto Its Host
maestro-cli lift <agent> moves an SSH remote agent onto the maestro-cli host on its remote. The provider already ran on that box as the SSH user, so its session files are already there. The host gets an agent with the agent’s remote working directory (not its local one), provider, model, effort and arguments, plus every visible tab with its transcript and the same provider session ID, so the next turn resumes the conversation. Queued messages move with it. After the lift, the local agent keeps every tab and transcript, but it is a pointer: list agents shows moved to peer:agent-id, and every send to it is refused with that target (AGENT_LIFTED).
Env vars: every non-secret custom env var moves. Secret ones stay here unless you pass --with-env. The variable that selects the provider’s config or session directory (CLAUDE_CONFIG_DIR, CODEX_HOME, COPILOT_HOME) always moves, and the host refuses the agent when that directory does not exist there, because the provider would start a new session instead of resuming.
Before it acts, lift prints what moves and what stays. --json gives the same plan, and --dry-run stops after it. It refuses, and moves nothing, when:
The host’s copy is made first. If the local agent cannot then be marked as moved, the host’s copy is removed again, so the agent never runs in two places. Hidden consult tabs, queued slash commands, and images in this machine’s image store stay here. The verb belongs to the Federation Encore Feature.
--dry-run prints, with a choice to send secret env vars, before anything moves. Afterwards the agent’s row shows on <peer>: click it to open the agent on its host.
One Host Per Data Directory
A data directory has one owner at a time: a lock file (maestro-runtime.lock) in the directory says who. host start refuses a directory the desktop app or another host already holds, and says which. The desktop app will not open on a directory a headless host holds: it shows a dialog naming the host and quits, because loading its stores on top of the host’s would overwrite the host’s writes. Stop the host with maestro-cli host stop, then open Maestro.
The data directory is resolved the way the desktop app resolves it (under ~/Library/Application Support on macOS, $XDG_CONFIG_HOME or ~/.config on Linux, %APPDATA% on Windows). --data-dir <path> or the MAESTRO_USER_DATA environment variable points the host somewhere else, and every other maestro-cli command honors the same variable, so set it in the environment of the commands that should reach that host. A directory that settings sync through a custom sync path is refused: the lock cannot see another machine writing there.
The host’s log is logs/maestro-host.log inside the data directory. When start fails before the host is ready, it prints the last lines of that log.
What the Host Does Not Do
The host refuses these in so many words, and nothing is started when it does:
Commands that drive the desktop’s window, such as opening a file or a browser tab, focusing an agent or tab, or raising a toast, fail with “The headless Maestro host (
maestro-cli host) does not support the ’…’ command”.
Installing on a Machine With No Desktop
The host is the CLI bundle, run with Node.js. On a machine without the Maestro app:- Node.js 22 or newer.
- The CLI bundle with its prompts: copy
maestro-cli.jstogether with theprompts/coredirectory beside it. Without the prompts the host refuses to start, because a turn would have no Maestro system prompt and an Auto Run could not run. better-sqlite3built for that Node, installed next to the bundle:npm install --prefix <dir of maestro-cli.js> better-sqlite3. The host records usage and Cue history in SQLite.- The provider CLIs your agents use (Claude Code, Codex, OpenCode, and so on), on the host’s
PATH.
maestro-cli host start --foreground (with --data-dir if the directory is not the default) and stop it with SIGTERM, which drains as described above. Run the Host as a Service has the unit files.
Run the Host as a Service
maestro-cli host start detaches, but nothing starts it again after a reboot or a crash. A service manager does both. Maestro does not install these files: copy the one for your platform and change the paths.
Run the service as the user your SSH agents run as. The host and the SSH agents then share one home directory, so the host reads the same provider logins and session files. A host that runs as another user cannot resume the provider sessions of an agent lifted onto it. This is not the maestro system user that the server bundle creates for a dedicated Cue engine: that user is for an engine that owns its whole machine, and an SSH agent you start does not run as it.
The service manager starts the host with a small environment, not your login shell’s. Put the directories of the provider CLIs your agents use (Claude Code, Codex, and so on) on the service’s PATH, as both files below do. Run command -v claude (or the CLI you use) in your own shell to find them.
Do not run the service on a data directory the desktop app also opens. While the desktop app holds the directory, the host refuses to start, and the service manager starts it again and again.
systemd (Linux)
Save this as~/.config/systemd/user/maestro-host.service. ExecStart takes the absolute path of your maestro-cli wrapper (command -v maestro-cli prints it; %h is your home directory):
host start and host status warn when lingering is off for your user (linger: false in --json), and remote-host status passes that warning through to the laptop.
launchd (macOS)
Save this as~/Library/LaunchAgents/sh.maestro.host.plist. It runs the CLI under the app’s own executable, the same command the wrapper runs:
/Users/you with your home directory: launchd does not expand ~ or $HOME. KeepAlive with SuccessfulExit set to false starts the host again when it exits with an error, and leaves it stopped after a clean stop. Load it, and check it:
--no-prevent-sleep under host start).
Cue Automation
Interact with Maestro Cue subscriptions directly from the command line.Listing Subscriptions
List all Cue subscriptions across all agents:Recent Runs
running, completed, failed), subscription, event type, and duration (or how long a running one has run so far).
Turning a Subscription On or Off
enabled flag into its cue.yaml, as the switch on the Cue dashboard does. Pass --agent, or the full id from cue list --json, when two agents share the name.
Where the Answer Comes From
cue list, cue activity, cue enable, and cue disable ask whatever serves the data directory (--data-dir, or MAESTRO_USER_DATA, or the platform default):
A host draining after SIGTERM or SIGINT keeps answering reads (
cue list and cue activity) and refuses cue enable, cue disable, and cue trigger until its Cue runs have finished. host stop (any form) unpublishes first, so from then on reads fall back as if no host were running (cue list reports exit 3, cue activity reads cue.db). If another engine takes the Cue lock over from a host, the host stops answering for Cue, so reads come from disk and a refused change names the engine that now holds the lock.
An older desktop build that does not know a command, or a peer that does not answer in time, is still reported as an error (exit 4 or 5): the CLI never covers a real failure with figures from disk. With Maestro Cue switched off in Encore Features every cue verb except cue engine stop is refused before it asks anyone (cue engine stop remains available so an existing standalone runner can be stopped). cue pipeline manages the desktop Pipeline Graph layout and is not served by a host; beside one it says so and points at cue.yaml.
Scheduling Tasks
cue schedule is the command surface for anything time-driven: a one-shot reminder, a daily job, or a repeating check. It writes straight to the agent’s .maestro/cue.yaml, so it works with the desktop app closed, and everything it creates shows up in the app under Maestro Cue → Scheduled Tasks (maestro-cli open cue --tab scheduled).
Notes:
--in,--at,--daily-at, and--everyare mutually exclusive: a task fires once, on a daily clock, or on an interval.- A task with both
--promptand--notifybecomes two subscriptions sharing one fire time (<name>-promptand<name>-notify). --agentis a hard scope on--cancel,--reschedule,--pause, and--resume. When one name exists on two agents the command refuses to guess and lists the candidates.- One-shot tasks delete themselves from the YAML after they fire. Repeating tasks stay until you cancel them.
--tier,--model, and--effortapply to the prompt run and need--prompt. To change them on a task that already exists, usecue modelbelow.
Setting a Subscription’s Model
cue model shows or changes the model one Cue subscription runs on: a model tier or an exact model, plus an effort level. It works on any subscription that runs an agent, including scheduled tasks and pipeline steps, and it is the CLI twin of the Model and Effort selectors on a Pipeline Graph agent node and in the Scheduled Tasks form. Like cue schedule, it edits the agent’s .maestro/cue.yaml directly, so it works with the desktop app closed.
Notes:
- On a server, pass
--data-dir(or setMAESTRO_USER_DATA) so the command reads the same agents the Cue engine runs. A running engine picks the editedcue.yamlup on its own. - With no setting flag the command only reads. Its output names the stored choice and the model and effort the run resolves to.
--tierand--modelare two spellings of one choice. Setting one removes the other.- When the agent’s provider cannot honor a tier or an effort, the command still saves it and prints a warning. The run then uses the agent’s own value.
- Subscriptions with
action: commandoraction: notifyrun no agent, so the command refuses to set a model on them.
Triggering a Subscription
Manually trigger a Cue subscription by name, bypassing its normal event conditions. A subscription in a pipeline fires together with its siblings: every subscription in the same pipeline whose trigger settings (event, schedule, interval, filter, and so on) are identical to its own. That is what a real tick does, since the pipeline editor writes a fan-out to several targets as one subscription per target, each repeating the trigger. To fire one subscription alone, give it a pipeline of its own or trigger settings that differ from the others’. A chain step (agent.completed), a subscription with no pipeline, and a trigger with --prompt fire only the named subscription.
The
--prompt flag is especially useful for cli.trigger subscriptions, where the prompt text is available in the subscription’s template as {{CUE_CLI_PROMPT}}.
Examples:
Director’s Notes
Director’s Notes is an Encore feature (encoreFeatures.directorNotes) that builds a unified history view across every agent in your fleet, plus an AI-generated synopsis of recent activity.
synopsis requires the desktop app to be running; history reads from disk and works offline. If encoreFeatures.directorNotes is disabled, enable it first with maestro-cli settings set encoreFeatures.directorNotes true.
The provider follows the app’s Director’s Notes setting. By default that is “use the first available provider”, so the desktop picks an installed agent when the run starts and --json reports which one actually ran. Pin it with maestro-cli settings set directorNotesSettings.autoSelectProvider false.
Publishing Session Transcripts to Gists
Publish an agent’s session transcript to a GitHub gist so you can share it with collaborators or attach it to a bug report. Routes through the running Maestro desktop app (which holds the live transcript) and uses the user’s authenticatedgh CLI under the hood.
Output is JSON with the gist URL on success:
Publishing a headless session
Without--session, gist create publishes the transcripts of the agent’s open AI tabs in the desktop app. Headless callers (chat bridges, playbooks, Cue pipelines, CI) run their conversations with maestro-cli send -s <session-id> and have no tab, so for them that publishes an unrelated conversation - and a gist is readable by anyone holding the URL.
Pass the same session id you sent with:
GIST_CREATE_FAILED rather than falling back to the open tabs. The response echoes agentSessionId so you can confirm what was published.
Requires the Maestro desktop app to be running and gh to be authenticated (gh auth login). Error codes: AGENT_NOT_FOUND, INVALID_SESSION, MAESTRO_NOT_RUNNING, GIST_CREATE_FAILED.
Scheduling with Cron
Agent Integration
Maestro agents are automatically informed aboutmaestro-cli through the system prompt. Each agent receives the platform-appropriate CLI invocation command via the {{MAESTRO_CLI_PATH}} template variable, which resolves to the full node "/path/to/maestro-cli.js" command for the current OS. That form runs on whatever node is on the agent’s PATH, which is enough for every command that talks to the running app or host. The Cue commands that open cue.db themselves (cue engine, host start, and cue activity when neither the app nor the host answers for Cue) need the app’s runtime when using the bundled Electron-built SQLite module, or Node with a compatible better-sqlite3 build as described under Installing on a Machine With No Desktop.
This means agents can:
- Read settings to understand the current Maestro configuration
- Change settings on behalf of the user (e.g., “switch to the nord theme”, “increase font size”)
- Manage agent configs (e.g., “set the Codex context window to 128000”)
- List resources like agents, groups, and playbooks
- Open files in the Maestro file preview tab
- Refresh the file tree after creating or modifying files
- Configure and launch auto-runs with documents they create
- Send messages to other agents for inter-agent coordination
- Discover Cue subscriptions with
cue listand trigger automation pipelines withcue trigger
settings list -v to discover available settings with descriptions, giving them full context to reason about configuration changes.
Requirements
- At least one AI agent CLI must be installed and in PATH (Claude Code, Codex, or OpenCode)
- Maestro config files must exist (created automatically when you use the GUI)