The crow CLI communicates over a Unix socket at ~/.local/share/crow/crow.sock (override with CROW_SOCKET). A server must be listening on it for RPC commands to succeed — the crowd daemon owns this socket. crow setup and crow autostart are the only subcommands that work with nothing listening.
All commands print JSON to stdout on success. Session and terminal identifiers are full UUIDs (e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890) — short names are not accepted.
Every subcommand source lives in Packages/CrowCLI/Sources/CrowCLILib/Commands/.
This page is the guide: what each verb is for, what it returns, and where it bites. For the exhaustive machine-generated surface — every subcommand and every flag, straight from the commands themselves — see cli.md. A test fails the build if a verb documented there is missing here (CROW-808).
First-time setup wizard. Checks for runtime dependencies (git, gh, claude), prompts for a development root and workspaces, then writes ~/Library/Application Support/crow/devroot and scaffolds {devRoot}/.claude/.
crow setup
crow setup --dev-root ~/Dev| Flag | Required | Description |
|---|---|---|
--dev-root |
no | Skip the interactive dev-root prompt |
This is one of two subcommands that do not require a running daemon (see crow autostart).
Registers crowd to start at login — a launchd LaunchAgent at ~/Library/LaunchAgents/com.corveil.crowd.plist, logging to ~/Library/Logs/crow/crowd.log. Runs locally instead of over the socket, so it works with the daemon down (which is the point). macOS only for now; on other platforms it reports supported: false rather than pretending to register anything. The same control lives at Settings → General → Autostart for a local browser.
crow autostart install
crow autostart install --host 0.0.0.0 --port 8080 --dev-root ~/Dev
crow autostart status --json
crow autostart uninstallBare crow autostart is status. install is idempotent and re-points the login item at the current crowd on every run, so an upgrade never leaves a stale plist. When a crowd is already running, install writes the login item and leaves launchd alone — it takes effect at next login rather than spawning a duplicate the single-instance guard would refuse.
| Flag | Applies to | Description |
|---|---|---|
--binary |
install, status | Path to crowd (default: next to this crow, then PATH) |
--host |
install | Bind host passed to crowd |
--port |
install | HTTP port passed to crowd |
--dev-root |
install | Development root passed to crowd |
--socket |
install | Unix socket path passed to crowd |
--json |
all | Print the status object instead of the human summary |
The status object reports enabled (registered at login), running (a crowd is answering right now), loaded (launchd knows it in this session), and stale (the registration points at a different binary than --binary / the resolved one).
Create a new session.
crow new-session --name "feature-name"| Flag | Required | Description |
|---|---|---|
--name |
yes | Session name |
Returns {"session_id": "<uuid>", "name": "..."}.
crow rename-session --session <uuid> "new-name"| Arg / Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
(positional) NAME |
yes | New name |
Make the given session the active session in the web UI.
crow select-session --session <uuid>Print all sessions.
crow list-sessionscrow get-session --session <uuid>Returns full session details: id, name, status, ticket metadata, worktrees, terminals, and links.
crow set-status --session <uuid> active
crow set-status --session <uuid> paused
crow set-status --session <uuid> inReview
crow set-status --session <uuid> completed
crow set-status --session <uuid> archived| Arg / Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
(positional) STATUS |
yes | active, paused, inReview, completed, archived |
Lock a session so the retention reaper leaves it alone. Cleanup deletes completed/archived sessions once they age past the retention_hours set under Settings Commands — locking exempts a session from that, and unlocking returns it to the pool. Independent of status: a locked session still moves through active → completed normally, it just never gets swept.
crow set-locked --session <uuid> true
crow set-locked --session <uuid> false
crow set-pinned --session <uuid> true # deprecated alias, still accepted| Arg / Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
(positional) LOCKED |
yes | true or false |
crow set-pinned is a deprecated alias kept working for existing scripts (the field was renamed in CROW-573). It is hidden from crow --help and takes the same arguments — prefer set-locked.
The session row/menu actions from the web UI, as CLI verbs. Each maps 1:1 onto the RPC method of the same name, so the CLI and the browser drive a session through its lifecycle by exactly the same path. All five take only --session.
Preconditions are enforced server-side, because the browser enforces them by hiding menu items and a CLI has no equivalent affordance. They are not gated on the session's current status, though — the UI's active/inReview/completed conditions decide which menu items to draw, not what's legal, so crow complete-session is safe to run twice.
| Command | Effect | Requires |
|---|---|---|
crow mark-in-review |
Moves the linked ticket to In Review on the provider's board, then session status → inReview |
A linked ticket + a resolvable provider |
crow complete-session |
Session status → completed |
— |
crow set-session-active |
Session status → active |
— |
crow mark-issue-done |
Closes the linked issue on the provider, then completes the session | A linked ticket + a resolvable provider |
crow add-merge-label |
Adds the crow:merge label to the session's PR (warns when the watcher can't act on it) |
A linked PR + a backend supporting auto-merge labels |
The three provider-side verbs report real outcomes: an unmet precondition (no ticket, no PR, no provider, provider lacks the capability, unparseable repo slug) and a failed provider call both exit non-zero with the reason. They never print a success receipt for an action that didn't happen — and when an action did happen but can't have its intended effect, the receipt carries a warning rather than a bare all-clear.
crow mark-in-review --session <uuid>
crow complete-session --session <uuid>
crow set-session-active --session <uuid>
crow mark-issue-done --session <uuid>
crow add-merge-label --session <uuid>| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
The three status verbs return {"session_id": "…", "status": "…"}; mark-issue-done and add-merge-label return {"ok": true, "session_id": "…"}.
mark-in-review transitions the board before it writes the session status, so a failed transition exits non-zero and leaves the session where it was — there is no half-done state to reconcile. When the provider has no In Review status to move to, that is not a failure: nothing was ever going to move, so the session transition still happens and the receipt carries a warning. That covers GitLab (no project-board status at all) and any GitHub board whose column isn't named "In Review" or "Review".
{
"session_id": "3f2a…",
"status": "inReview",
"warning": "Session moved to In Review, but the ticket did not: no In Review status is available for https://gitlab.com/acme/api/-/issues/7."
}add-merge-label additionally returns a warning string when the label landed but auto-merge won't follow — the watcher is off, or Crow has already given up on that PR (e.g. the repo has GitHub's Allow auto-merge setting disabled). ok stays true, because the label really is on the PR; the key is omitted entirely when there's nothing to warn about, so jq -e .warning is a reliable test (#888).
{
"ok": true,
"session_id": "3f2a…",
"warning": "The label was added, but Crow's auto-merge watcher is off, so nothing will merge this PR. Turn it on in Settings → Automation, or run `crow automation set --auto-merge-watcher-enabled true`."
}Manager sessions are rejected by all five — they stay always-active and never move through the review/complete lifecycle, matching the web UI, which never offers these actions for a Manager.
complete-session and set-session-active write Crow's own session status. To move the provider's board for those, use crow transition-ticket. mark-in-review and mark-issue-done move the board themselves.
Switch a session to a different coding agent mid-flight (e.g. when credits run out). Preserves session identity, worktree, branch, and ticket context. Tears down the managed agent terminal and launches the target agent with a handoff prompt. Conversation history does not transfer across agents — see ADR 0009.
crow handoff-agent --session <uuid> --agent cursor
crow handoff-agent --session <uuid> --agent claude-code --note "Hit credit limit; continue from failing tests"| Flag / Arg | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--agent |
yes | Target kind: claude-code, cursor, codex, opencode |
--note |
no | Optional resume note for the incoming agent |
Returns {"session_id":"…","agent_kind":"…","terminal_id":"…"}. Manager sessions are not supported — change the Manager agent in Settings and restart instead.
crow delete-session --session <uuid>Deletes the session metadata. Sessions on protected branches (main/master/develop) preserve the repo folder and branch — see Configuration › Safe Deletion.
Attach ticket metadata (URL, title, number, priority) to a session. At least one of --url, --title, --number, or --priority must be provided.
crow set-ticket --session <uuid> --url "https://github.com/org/repo/issues/123" --title "Fix bug" --number 123
crow set-ticket --session <uuid> --priority high| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--url |
no¹ | Ticket URL |
--title |
no¹ | Ticket title |
--number |
no¹ | Ticket number |
--priority |
no¹ | Ticket priority: highest, high, medium, low, or lowest (case-insensitive). Feeds the session's alignment weight (ADR 0008 follow-up 8). |
¹ At least one of --url, --title, --number, --priority is required.
Set or clear the org-goal tag on a session — the org goal/KPI the session's work ladders up to (ADR 0008 follow-up 8). The tag feeds the session's alignment weight, read back via crow get-session (org_goal, ticket_priority, alignment_weight). Exactly one of --goal or --clear is required.
crow set-goal --session <uuid> --goal "Q3 latency KPI"
crow set-goal --session <uuid> --clear| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--goal |
no² | Org goal/KPI tag (free text, non-blank) |
--clear |
no² | Clear the tag (back to neutral weight) |
² Exactly one of --goal, --clear is required.
Add a link (issue, PR, repo, or custom) to a session.
crow add-link --session <uuid> --label "Issue #123" --url "https://..." --type ticket| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--label |
yes | Display label |
--url |
yes | Target URL |
--type |
no | ticket, pr, repo, or custom (default: custom) |
crow list-links --session <uuid>Returns each link's id, label, url, and type — the id is what edit-link / remove-link take.
Detach a link from a session, identified by its link ID (from list-links) or its URL.
crow remove-link --session <uuid> --id <link-uuid>
crow remove-link --session <uuid> --url "https://..."| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--id |
one of | Link UUID (from list-links) |
--url |
one of | Link URL (alternative to --id) |
Provide at least one of --id / --url. Returns {"removed": N}.
Update a link's label, URL, or type in place. The link is selected by --id or its current --url; only the fields you pass change. Because --url selects the link, the new URL is set via --new-url.
crow edit-link --session <uuid> --id <link-uuid> --label "PR #42" --type pr
crow edit-link --session <uuid> --url "https://old..." --new-url "https://new..."| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--id |
one of | Link UUID to edit (from list-links) |
--url |
one of | Current link URL to match (alternative to --id) |
--label |
no | New display label |
--new-url |
no | New target URL |
--type |
no | New type: ticket, pr, repo, or custom |
Provide at least one selector (--id / --url) and at least one field to change (--label / --new-url / --type). Returns {"updated": N}.
Move the session's linked ticket to a pipeline status on the provider's board, without touching Crow's own session status. Use it to move a board independently of the session lifecycle verbs — for the In Progress transition at session start (what setup.sh calls), or to correct a board out of band. Unlike mark-in-review, it is best-effort: failures are logged, not reported.
crow transition-ticket --session <uuid> --to inProgress
crow transition-ticket --session <uuid> --to inReview
crow transition-ticket --session <uuid> --to done| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--to |
yes | inProgress, inReview, or done (matched case-insensitively) |
Jira maps the pipeline status onto a real workflow transition through jiraStatusMap (see Configuration). Requires a linked ticket and a provider that supports transitions.
Re-sync every Jira-backed session's ticket to the status implied by its Crow session state — one-shot remediation for tickets left in Backlog because earlier sessions never transitioned them (CROW-529).
crow resync-jiraTakes no arguments and walks every session, so it is safe but not cheap. Nothing happens for sessions whose ticket is not Jira-backed.
The Claude Code permission allowlist, aggregated across the global ~/.claude/settings.json and every registered worktree's .claude/settings.local.json. crowd owns the scan (it is pure disk I/O), so these work with the desktop app closed.
List every known pattern and where it is defined.
crow list-allowlistTakes no flags. Returns:
{
"entries": [
{ "pattern": "Bash(npm test:*)", "is_global": false, "worktree_session_names": ["acme-api-197"] }
],
"loading": false
}is_global means the pattern is in ~/.claude/settings.json; worktree_session_names lists the sessions whose worktree settings declare it. This reads the daemon's last scan rather than re-reading disk — run crow refresh-allowlist first if a settings file changed underneath it.
Copy worktree-local patterns into the global ~/.claude/settings.json, then re-scan.
crow promote-allowlist --pattern 'Bash(npm test:*)' --pattern 'Read'| Flag | Required | Description |
|---|---|---|
--pattern |
yes | Pattern to promote; repeat for more than one |
Quote your patterns. (, ), and * are shell metacharacters — --pattern 'Bash(npm test:*)', not --pattern Bash(npm test:*).
Returns which patterns actually changed:
{
"ok": true,
"added": ["Bash(npm test:*)"],
"already_global": ["Read"],
"global_settings_path": "/Users/you/.claude/settings.json"
}Promoting an already-global pattern is a no-op, not an error — it comes back under already_global with an empty added.
A write that cannot land is an error (non-zero exit), never {"ok": true}. Two cases are refused outright rather than half-applied:
- the global settings file exists but is not valid JSON — it is left byte-for-byte untouched rather than being replaced by one containing only
permissions.allow; - any element of the request is not a string — the whole call is rejected instead of silently promoting the valid subset.
Promote everything not yet global:
crow list-allowlist \
| jq -r '.entries[] | select(.is_global | not) | "--pattern", .pattern' \
| xargs crow promote-allowlistRe-scan the global and per-worktree settings files from disk.
crow refresh-allowlistTakes no flags. Returns {"ok": true}.
The General-tab settings that were previously web-only. Every set is a patch — only the flags you pass change, and passing none is an error rather than a silent no-op. Each set echoes the resulting subtree plus a restart_required boolean, so a write is self-verifying without a follow-up get.
Booleans take an explicit value (--enabled true), not a bare flag, so that a patch can express "set to false" as distinct from "leave alone". Only the literals true and false are accepted.
Agent selection is a General-tab setting too, but has its own group — see Agent Commands.
Session-analytics collection over Claude Code's OpenTelemetry exporter.
crow telemetry get
crow telemetry set --enabled true --port 4318
crow telemetry set --retention-days 0| Flag | Required | Description |
|---|---|---|
--enabled |
no¹ | true or false — enable the OTLP receiver |
--port |
no¹ | OTLP HTTP receiver port, 1024–65535 (default 4318) |
--retention-days |
no¹ | Days of telemetry to keep; 0 keeps forever (default 180) |
¹ At least one flag is required.
--enabled and --port are read once when crowd starts, and the port is baked into every agent launch's OTEL_EXPORTER_OTLP_ENDPOINT, so changing either returns "restart_required": true (plus a warning on stderr). Restart crowd to apply. --retention-days drives the prune that runs at startup, so it likewise takes effect at the next daemon start.
Automatic deletion of completed and archived sessions.
crow cleanup get
crow cleanup set --enabled true --retention-hours 72
crow cleanup set --enabled false| Flag | Required | Description |
|---|---|---|
--enabled |
no¹ | true or false — enable auto-cleanup |
--retention-hours |
no¹ | Hours to keep completed/archived sessions, minimum 1 (default 24) |
¹ At least one flag is required.
Cleanup deletes eligible sessions including their worktree and branch. Manager, virtual, and locked sessions are never deleted. Unlike telemetry this setting is live: the board poll re-reads it from disk each cycle, so enabling it starts deleting within about a minute — no restart, and no confirmation prompt.
Display preferences for the web UI. This does not start, stop, or open the UI.
crow ui get
crow ui set --hide-session-details true| Flag | Required | Description |
|---|---|---|
--hide-session-details |
yes | true or false — hide ticket title and repo/branch sidebar lines |
Settings are grouped by the config block they belong to, so get returns {"ui": {"sidebar": {...}}} and gains further blocks as more view options become configurable. Connected browsers pick the change up within a couple of seconds — no reload.
Compare the running build against corveil/crow main (CROW-938). The daemon owns the GitHub compare call and caches the result; the CLI and Settings → About read it back.
crow version
crow version --check
crow version check
crow version get
crow version set --enabled false
crow version set --interval-hours 12| Flag / subcommand | Description |
|---|---|
--check |
Force a fresh compare and print a human summary (exit 0 up to date, 1 behind, 2 could not check) |
check |
Same as --check |
get |
JSON: version_update settings plus the cached status |
set --enabled |
Opt out of periodic checks (true or false) |
set --interval-hours |
Hours between automatic checks (minimum 6, default 6) |
get returns {"version_update":{"enabled":…,"interval_hours":…},"status":{…}} where status.state is up_to_date, behind, or unknown. A dev build SHA or an unrecognized commit reports unknown — never a false up-to-date.
Workspace and automation defaults — the defaults block of config.json, behind Settings → Workspaces (provider, branch prefix), → Automation (the board filter lists) and → General (the corveil binary path).
crow defaults get
crow defaults set --provider gitlab --cli glab
crow defaults set --branch-prefix 'feat/'
crow defaults set --add-exclude-review-repo 'acme/legacy-*' --add-exclude-review-repo acme/docs
crow defaults set --remove-ignore-review-label wip
crow defaults set --clear-exclude-ticket-repos
crow defaults set --binary corveil=/opt/corveil/bin/corveil
crow defaults set --binary corveil= # remove the override| Flag | Required | Description |
|---|---|---|
--provider |
no¹ | Forge for new workspaces: github or gitlab |
--cli |
no¹ | Forge CLI for new workspaces: gh or glab |
--branch-prefix |
no¹ | Prefix for new session branches, e.g. feature/; empty means no prefix |
--binary NAME=PATH |
no¹ | Absolute binary path override, repeatable; NAME= removes the entry |
--add-exclude-review-repo |
no¹ | Hide a repo from the review board, repeatable; one * wildcard allowed |
--remove-exclude-review-repo |
no¹ | Stop hiding a repo from the review board, repeatable |
--clear-exclude-review-repos |
no¹ | Empty the review-board repo exclusions |
--add-exclude-ticket-repo |
no¹ | Hide a repo from the ticket board, repeatable; one * wildcard allowed |
--remove-exclude-ticket-repo |
no¹ | Stop hiding a repo from the ticket board, repeatable |
--clear-exclude-ticket-repos |
no¹ | Empty the ticket-board repo exclusions |
--add-ignore-review-label |
no¹ | PR label that hides a review, repeatable; exact match, no wildcards |
--remove-ignore-review-label |
no¹ | Stop ignoring a PR label, repeatable |
--clear-ignore-review-labels |
no¹ | Empty the ignored review labels |
¹ At least one flag is required.
get echoes the whole block — including exclude_dirs and mirror_claude_mcp_to_codex, which set does not write and which have no Settings UI either. It also returns config_readable; when that is false, config.json exists but could not be decoded and the values shown are the built-in defaults rather than yours.
Lists are edited incrementally, not replaced — excluding one more repo shouldn't mean restating the list. --add- and --remove- compose in one call (remove is applied first, so naming a value in both means "ensure it's there"); --clear- is rejected alongside --add-/--remove- for that same list, but clearing one list while editing another is fine. Matching is case-insensitive, mirroring how the board filters actually compare — --remove-exclude-review-repo ACME/Docs removes a stored acme/docs. Re-adding a value that is already stored keeps the stored casing.
Note that the review board filters on the union of this list and every workspace's own excludeReviewRepos, so --clear-exclude-review-repos does not unhide a repo a workspace excludes. --add-exclude-ticket-repo and --add-ignore-review-label have no workspace-level counterpart.
Most of these are live: --provider/--cli are re-read on each repo scan, the three lists on each board poll (about a minute), and --branch-prefix when a workspace is created. --binary is the exception — agent binary discovery and the {devRoot}/.claude/bin symlinks are both set up at startup, so a change returns "restart_required": true and needs a crowd restart. That includes removing one: nothing re-scaffolds on a config change, so the stale symlink keeps shadowing PATH until the next launch. crow is rejected as a binary name — Crow re-points that symlink at its own CLI every launch, so an override there could never take effect.
A path that isn't executable right now is saved with a warning, not rejected (binaries_not_executable in the response) — pointing at a tool you haven't installed yet is a legitimate flow, and the symlink is simply skipped until the target appears. --provider and --cli are stored independently and neither implies the other, matching how repo scanning reads them; setting only one warns via provider_cli_mismatch if the resulting pair is crossed.
crow defaults set --binary is local-only — binary overrides are absolute paths that execute at the next agent launch, so the remote /rpc path refuses any defaults-set carrying them. Every other flag, and defaults get, work remotely.
Concurrent edits. An open web Settings modal snapshots the whole config when you open it and re-sends all of it on Save, so saving a stale modal reverts a CLI write made in the meantime. This applies equally to
crow notifications set,crow ui setandcrow job add; close or reopen Settings after a CLI change.
Scheduled jobs (CROW-604) are timed prompt-sets scoped to one repo in one workspace — the Jobs sidebar feature, as CLI verbs. When a job fires, Crow creates a session, clones the repo if needed, launches the configured agent, and sends the job's prompts in order.
Every subcommand goes through the running app's RPC socket, so mutations land on the live in-memory config: the scheduler and the web Settings UI pick them up immediately, with no restart. Jobs are addressed by UUID (--id), which crow job list prints.
crow job listReturns {"jobs": [...]} — every job with its schedule, prompts, and enabled flag.
crow job get --id <job-uuid>| Flag | Required | Description |
|---|---|---|
--id |
yes | Job UUID |
Create a job. Prompts are sent in the order given: every --prompt first, then the contents of every --prompt-file.
Exactly one schedule is required — either --interval-seconds or --daily-at HH:MM, the latter optionally narrowed with --weekdays.
crow job add --name "nightly-audit" --workspace Corveil --repo corveil/crow \
--prompt "Run the test suite and summarise failures" --daily-at 02:00
crow job add --name "hourly-triage" --workspace Corveil --repo corveil/crow \
--prompt-file ./prompts/triage.md --interval-seconds 3600 --disabled| Flag | Required | Description |
|---|---|---|
--name |
yes | Job name; must be unique |
--workspace |
yes | Workspace name (a folder under the dev root) |
--repo |
yes | Repository slug, owner/repo |
--prompt |
one of | Prompt text; repeatable, sent in order |
--prompt-file |
one of | Read a prompt from a file; repeatable. - reads stdin (at most once) |
--interval-seconds |
one of | Run every N seconds |
--daily-at |
one of | Run daily at HH:MM, 24-hour local time |
--weekdays |
no | Comma-separated days for --daily-at (sun,mon,… or 1-7); omit for every day |
--disabled |
no | Create the job disabled |
Returns {"job": {...}}.
Update fields on an existing job. Only the flags you pass change — with two sharp edges:
- Any
--prompt/--prompt-filereplaces the whole prompt list, not appends to it. - Any schedule flag replaces the whole schedule, so changing
--weekdaysmeans restating--daily-atalongside it.
crow job edit --id <job-uuid> --name "nightly-audit-v2"
crow job edit --id <job-uuid> --daily-at 03:30 --weekdays mon,tue,wed,thu,fri| Flag | Required | Description |
|---|---|---|
--id |
yes | Job UUID |
--name |
no | New name; must still be unique |
--workspace |
no | New workspace name |
--repo |
no | New repository slug |
--prompt |
no | Replacement prompt text (repeatable) |
--prompt-file |
no | Replacement prompt read from a file (repeatable) |
--interval-seconds |
no | Replacement interval schedule |
--daily-at |
no | Replacement daily schedule |
--weekdays |
no | Weekday filter for --daily-at |
At least one field flag is required. Use enable / disable rather than edit to toggle the enabled flag.
crow job enable --id <job-uuid>
crow job disable --id <job-uuid>| Flag | Required | Description |
|---|---|---|
--id |
yes | Job UUID |
Run a job right now, whatever its schedule says and even if it is disabled.
crow job run --id <job-uuid>Returns {"job_id": "…", "session_id": "…", "terminal_id": "…"}. Needs tmux on the daemon host. The CLI waits up to 120s because a first run may clone the repo — and the run continues inside the app even if the CLI gives up waiting.
crow job delete --id <job-uuid>Returns {"deleted": true, "job_id": "…"}.
Copy a job. The copy is created disabled, with a uniquified name, so it can be edited before it fires.
crow job duplicate --id <job-uuid>Which coding harness Crow launches — AppConfig.defaultAgentKind plus the per-role overrides in AppConfig.agentsByKind, the same fields the web Settings → General "Agent" pickers edit. Resolution is agentsByKind[<role>] falling back to defaultAgentKind.
The four roles are work (coding sessions), review (PR reviews), job (scheduled jobs), and manager (the Manager session).
Writes land under the shared config lock and take effect within about one board poll — no restart, and an open browser tab repaints within a couple of seconds. There is no restart_required in the response for that reason.
Show what this daemon can launch, what you configured, and what each role resolves to.
crow agents list
crow agents list | jq -r '.agents.effective'Takes no flags.
{
"agents": {
"known": [
{ "kind": "claude-code", "name": "Claude Code", "binary": "claude", "available": true },
{ "kind": "codex", "name": "OpenAI Codex", "binary": "codex", "available": true },
{ "kind": "antigravity", "name": "Antigravity", "binary": "agy", "available": false }
],
"default_agent_kind": "claude-code",
"by_kind": { "review": "codex" },
"effective": {
"work": "claude-code",
"review": "codex",
"job": "claude-code",
"manager": "claude-code"
},
"config_readable": true
}
}Change the default agent or a per-role override. Only the flags you pass change; at least one is required.
crow agents set --default codex
crow agents set --work codex --review cursor
crow agents set --clear review --clear job| Flag | Required | Description |
|---|---|---|
--default |
no | Agent for sessions with no per-role override |
--work |
no | Agent for new coding sessions |
--review |
no | Agent for PR-review sessions |
--job |
no | Agent for scheduled-job sessions |
--manager |
no | Agent for the Manager session |
--clear <role> |
no | Remove that role's override; repeatable (work|review|job|manager) |
Returns the same agents subtree as list, plus "saved": true.
Notes:
knownlists every agent Crow ships, installed or not, each with anavailableflag — the same surface-but-disable roster the web pickers show (#879), so an off-PATH harness reads as "not installed" rather than vanishing from the list.binaryis thePATHtoken to install (never the resolved absolute path, which would leak the daemon host's layout).availableis decided whencrowdstarts and is binary-dependent — Codex, Cursor, OpenCode and Antigravity register only if their CLI was onPATHat boot. Installing an agent therefore needs a daemon restart before it can be selected.- Only
available: truekinds are selectable; anything else is rejected and nothing is written — no lock taken, noconfig.jsonrewrite. This is stricter thancrow new-session --agent, which falls back to the default instead; here the configured value is trusted at launch time, so a typo would persist sessions that never start. A kind that's listed but uninstalled gets its own message naming the binary, rather than an "expected one of" list that would be confusing when the agent is right there incrow agents list. - A role also rejects an agent that can't run that kind of session. No agent is review-incapable today — review-on-Antigravity was the last such case and its review dispatch landed in #902, so the gate refuses nothing right now. It is kept in lockstep with
crow handoff-agent(which throws on the same predicate) so a future review-incapable harness is refused on both surfaces at once, rather than being configured into "created but never launches". The gate applies to the role flags only, not--default: validating the resolved outcome would make a pre-existing default fail every later write, including patches to unrelated roles. A default that resolves review to a review-incapable agent surfaces at launch time instead, where Crow writes an explanatory line into the terminal. --clear <role>removes the override key; it does not write a null. (agentsByKindis a[String: AgentKind]map that cannot decode a JSON null — a single one would make the wholeconfig.jsonundecodable.) Passing--clear Xtogether with--X <kind>is rejected rather than resolved by precedence.- Patch semantics: a role you don't mention keeps its stored value.
config_readable: falsemeansconfig.jsonexists but would not decode. The values shown are then defaults, not your settings.effectivereports what a new session of each role would get. If it names a kind that isn't available — an agent whose binary leftPATH, say — the CLI warns on stderr: that role's sessions will not launch.by_kindechoes the stored map verbatim, including a hand-edited key that isn't a real role. Such a key shows up inby_kindand is absent fromeffective; that difference is the tell.- Not to be confused with the older web-facing
list-agentsRPC, whose per-agentdefaultflag means the registry default (whichever agent registered first).default_agent_kindhere is the configured default; aknownrow deliberately carries nodefaultfield.
Settings → Automation: which sessions launch in auto permission mode, whether Crow watches for crow:auto / crow:merge labels, and whether it responds to changes-requested reviews and failed checks on your behalf.
The tab also renders three board-filter lists (excluded review repos, ignored review labels, excluded ticket repos). Those are AppConfig.defaults fields and are written by crow defaults set — one writer, one set of list semantics. crow automation get echoes them read-only so the tab still reads as a whole from one call.
Same patch contract as the settings verbs above — only the flags you pass change, and passing none is an error rather than a silent no-op. Booleans take an explicit true/false, which matters more here than anywhere else: six of these twelve toggles default to on, so a bare-flag design could never turn one off.
crow automation get
crow automation set --auto-merge-watcher-enabled true
crow automation set --respond-to-failed-checks true --auto-rebase-and-resolve-conflicts true
crow automation set --manager-auto-permission-mode false && crow restart-managerPermission modes — each passes --permission-mode auto to the agent at launch. --jobs-auto-permission-mode is here rather than under crow job so all five read and write as one group, even though the web UI renders it on the Jobs tab.
| Flag | Default | Description |
|---|---|---|
--remote-control-enabled |
false |
Launch new Claude Code sessions with --rc (drivable from claude.ai) |
--manager-auto-permission-mode |
true |
Manager terminal runs crow/gh/git without per-call approval |
--review-auto-permission-mode |
true |
Code-review sessions run their review flow unattended |
--coder-view-auto-permission-mode |
false |
New work coder views start in auto-accept instead of plan mode |
--jobs-auto-permission-mode |
true |
Scheduled jobs run unattended |
Attribution & watchers
| Flag | Default | Description |
|---|---|---|
--attribution-trailers |
true |
Write a per-worktree hook adding a Crow-Session: <uuid> commit trailer |
--auto-create-watcher-enabled |
false |
Auto-launch a workspace for issues assigned to you labeled crow:auto |
--auto-merge-watcher-enabled |
false |
Auto-merge Crow-authored PRs labeled crow:merge |
Auto-respond — Crow acts on a PR's behalf without you asking: three of these type an instruction into the session's agent terminal, one calls the host API directly.
| Flag | Default | Description |
|---|---|---|
--respond-to-changes-requested |
true |
Respond to a changes-requested review |
--respond-to-failed-checks |
false |
Respond to failed CI checks |
--auto-rebase-and-resolve-conflicts |
false |
Rebase onto the base branch and --force-with-lease push |
--auto-re-request-review |
true |
Re-request review once a changes-requested PR's fix has landed |
At least one flag from any group is required.
Returns:
{
"automation": {
"remote_control_enabled": false,
"manager_auto_permission_mode": true,
"review_auto_permission_mode": true,
"coder_view_auto_permission_mode": false,
"jobs_auto_permission_mode": true,
"attribution_trailers": true,
"auto_create_watcher_enabled": false,
"auto_merge_watcher_enabled": true,
"auto_respond": {
"respond_to_changes_requested": true,
"respond_to_failed_checks": false,
"auto_rebase_and_resolve_conflicts": false,
"auto_re_request_review": true
},
"defaults": {
"exclude_review_repos": ["corveil/*"],
"ignore_review_labels": [],
"exclude_ticket_repos": [],
"effective_exclude_review_repos": ["corveil/*", "acme/legacy"]
},
"config_readable": true
},
"restart_required": false,
"manager_restart_required": false
}Notes:
- Nothing here needs a
crowdrestart. The daemon re-readsconfig.jsonrather than holding a snapshot, sorestart_requiredis alwaysfalseand every change is live within about one board poll (~60s). The permission modes and--remote-control-enabledapply to newly launched sessions and--attribution-trailersto newly created worktrees; running sessions and existing worktrees are untouched. --manager-auto-permission-modeis the one exception. It is baked into the Manager terminal's stored shell command, so it only takes effect on a Manager rebuild. Changing it returns"manager_restart_required": true(plus a warning on stderr) — runcrow restart-managerto apply it.- The whole
defaultsblock in the response is read-only here. Write those three lists withcrow defaults set;automation-setignoresadd_*/remove_*/clear_*params even if a hand-rolled RPC call sends them. defaults.effective_exclude_review_reposis additionally derived — the global exclude list unioned with every workspace's ownexcludeReviewRepos, which is what the review board actually filters on. It is the only way to see the per-workspace half from the CLI, and explains whycrow defaults set --clear-exclude-review-reposmay not unhide a repo.config_readable: falsemeansconfig.jsonexists but could not be decoded: the values shown are defaults, not what is on disk. Asetagainst such a file is refused rather than overwriting it.
Register a git worktree for a session. The app uses --repo-path to run git commands against the main repo when needed.
crow add-worktree \
--session <uuid> \
--repo "acme-api" \
--repo-path "/Users/you/Dev/Corveil/acme-api" \
--path "/Users/you/Dev/Corveil/acme-api-123-feature" \
--branch "feature/acme-api-123" \
--primary| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--repo |
yes | Repo name |
--path |
yes | Worktree path |
--branch |
yes | Branch name |
--repo-path |
no | Main repo path (used when shelling out to git against the primary repo) |
--primary |
no | Flag — mark this as the primary worktree for the session |
Note:
add-worktreedoes not support a--workspaceflag. Workspace association is derived from--repo-path.
crow list-worktrees --session <uuid>Create a new terminal tab inside a session. Use --managed for the primary Claude Code terminal that Crow auto-starts and tracks readiness for.
crow new-terminal --session <uuid> --cwd "/path/to/worktree" --name "Claude Code" --command "claude" --managed| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--cwd |
yes | Working directory |
--name |
no | Terminal display name |
--command |
no | Command to run once the shell is ready |
--managed |
no | Flag — mark as a managed Claude Code terminal (readiness tracking, auto-launch) |
crow list-terminals --session <uuid>Close a terminal tab in a session.
crow close-terminal --session <uuid> --terminal <uuid>Rebuild a terminal whose tmux window has degraded scrollback (CROW-804). Kills the stale window, creates a fresh correctly-configured one at the same worktree, and relaunches the session's agent with --continue so the conversation carries over.
crow recreate-terminal --session <uuid> --terminal <uuid>| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--terminal |
yes | Terminal UUID |
Destructive to whatever is running in that pane — anything mid-flight dies with the window. The web UI asks for confirmation first; from the CLI that judgement is yours.
Rename a terminal tab. The new name is positional.
crow rename-terminal --session <uuid> --terminal <uuid> "Build watcher"| Arg / Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--terminal |
yes | Terminal UUID |
(positional) NAME |
yes | New terminal name |
Returns {"session_id": "...", "terminal_id": "...", "name": "..."}.
Write text to a terminal. Newlines in TEXT are converted to Enter keypresses; include a trailing newline to submit a command.
crow send --session <uuid> --terminal <uuid> "claude --continue"$'\n'| Arg / Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--terminal |
yes | Terminal UUID |
(positional) TEXT |
yes | Text to send |
CLI parity for the maintenance actions that used to be web-only — Settings → About's maintenance group and the session-header host-app buttons. Each verb maps 1:1 to the RPC method of the same name.
All of these need tmux on the daemon host. Without it the daemon answers
… requires tmux on the daemon host.
Relaunch the Manager's agent process in place after it has exited (crash, kill, OOM). The Manager session row and its UUID survive; only the dead terminal surface is replaced, so the new terminal gets a fresh UUID.
crow restart-managerOnly the primary Manager session (00000000-0000-0000-0000-000000000000) is restarted —
secondary Manager sessions are untouched.
Returns {"ok": true}.
Restart the shared tmux server. Destructive: this kills every pane — every agent in every
session dies — then relaunches each persisted terminal (the Manager via its stored command,
work sessions via claude --continue).
crow restart-tmux-serverThe web UI confirms first; from the CLI the caller owns that choice, the same stance as
recreate-terminal. There is no --yes flag.
Returns {"ok": true} as soon as the teardown is done — the per-terminal rebuild continues
in the background, so don't chain a crow send straight after this or you'll race a
half-rebuilt surface.
Reload the bundled tmux config into the running server (tmux source-file) without restarting
it. Non-destructive: windows, sessions, and running agents are unaffected.
crow reload-tmux-configReturns {"ok": true}, or errors with tmux server is not running /
bundled crow-tmux.conf not found.
Launch the session's coding agent in a terminal whose shell is ready.
crow launch-agent --terminal <uuid>| Flag | Required | Description |
|---|---|---|
--terminal |
yes | Terminal UUID |
Note this takes only --terminal — the terminal id alone identifies the surface, so unlike
new-terminal / send / rename-terminal there is no --session flag.
Returns {"ok": true}. The daemon applies this only to a terminal that is shell-ready and
still pending auto-launch; in any other state it is a silent no-op, so ok means the request
was accepted, not that an agent was started.
Re-arm the tmux readiness watch for a terminal whose first attempt timed out, with a longer budget. Clears the Retry overlay in the UI.
crow retry-readiness --terminal <uuid>| Flag | Required | Description |
|---|---|---|
--terminal |
yes | Terminal UUID |
Takes only --terminal, like launch-agent. Returns {"ok": true}; the daemon applies it
only to a terminal that timed out or never reached shell-ready, so ok again means
"accepted", not "applied".
Open the session's primary worktree in VS Code on the daemon host.
crow open-in-vscode --session <uuid>| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
Requires the code CLI on PATH (or a standard VS Code install) and a worktree attached to the
session. Returns {"opened": true}, or errors with No worktree for session /
VS Code CLI not found.
Open a macOS Terminal.app window on the daemon host, cd'd to the session's primary worktree.
crow open-terminal --session <uuid>| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
This is not a Crow terminal tab — use crow new-terminal for that.
macOS only. Returns {"opened": true}, or errors with No worktree for session.
open-in-vscode and open-terminal launch a GUI app on the host, so the daemon restricts them
to local callers. The CLI always qualifies: it reaches the daemon over its 0600 Unix socket,
not the network /rpc endpoint.
Read-only reads of the daemon's own state, plus the one idempotent rebuild verb. All local.
Print the private efficiency scorecard.
crow get-scorecard | jq '{telemetryEnabled, snapshotCount}'Takes no flags. The result object is the scorecard — grade, weekly rollups, baseline medians, per-session rows, and Manager usage weeks — not a wrapper around one. Grading runs daemon-side so the CLI, web, and desktop can never drift apart.
With telemetry disabled the result is an empty shell; check telemetryEnabled and snapshotCount before reading grades. All timestamps are epoch milliseconds, not ISO 8601.
Backfill analytics snapshots for sessions recorded before snapshotting existed, recompute the Manager weekly rollups, and refresh the capture-status line.
crow rebuild-scorecardTakes no flags. Returns {"rebuilt": true}. Idempotent, and overlapping callers coalesce into a single rebuild rather than racing the same database — success is only ever reported for work that actually ran. Errors when telemetry is disabled, since there is no database to rebuild from. Uses an extended timeout; a first rebuild over a large history is not instant.
Print the daemon's entire render-state snapshot in one call.
crow get-state | jq 'keys'Takes no flags. The result object is the snapshot: sessions, terminals, worktrees, links, hookStates, terminalReadiness, prStatus, reviewRequests, assignedIssues, allowEntries, remoteControlActiveTerminals, remoteControlEnabled, activeTerminalID, and config. Credentials (Jira token, gateway auth headers, web-password hash and salt) are stripped before transport.
This is deliberately everything at once, and it is large — the snapshot carries every assigned issue's full body text, and the socket caps a response at 1 MB. On a busy install the command fails with a message naming the narrower reads. Prefer crow list-sessions, crow get-session --session <uuid>, crow list-links, or crow list-terminals when you want one slice.
List the images an agent dropped in a session's artifacts directory.
crow list-artifacts --session <uuid>| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
Returns, newest first:
{
"dir": "/var/folders/.../crow/artifacts/<session-uuid>",
"images": [
{
"name": "shot.png",
"size": 20418,
"mtime": "2026-07-27T10:14:02Z",
"url": "/artifacts/<session-uuid>/shot.png",
"path": "/var/folders/.../crow/artifacts/<session-uuid>/shot.png"
}
]
}path and dir are absolute on-disk locations — use these from a shell. url only resolves against the daemon's own web server and is there for the web UI. The directory is the one agents see as $CROW_ARTIFACTS_DIR; it lives under $TMPDIR and does not survive a reboot.
Read and write AppConfig.notifications — the same settings the web Settings → Notifications tab edits. Writes land under the shared config lock, and the daemon's config poll broadcasts a configReloaded event, so an open web tab refreshes within a couple of seconds.
Notifications cascade: one fires only if globalMute is off, the matching global category toggle is on, and the per-event toggle is on.
The ten events are taskComplete, agentWaiting, reviewRequested, changesRequested, checksFailing, autoWorkspaceCreated, autoMergeEnabled, autoRebasePushed, autoRebaseConflicts, and configReloaded.
Show the global toggles, every event's effective settings, the built-in sound names, and whether the config was readable. Events absent from config.json are reported with the defaults they will actually fire with.
crow notifications get
crow notifications get --event checksFailing| Flag | Required | Description |
|---|---|---|
--event |
no | Restrict the event list to one event |
Returns:
{
"notifications": {
"global_mute": false,
"sound_enabled": true,
"system_notifications_enabled": true,
"events": {
"taskComplete": {
"enabled": true,
"sound_enabled": true,
"system_notification_enabled": true,
"sound_name": "Glass"
}
},
"available_sounds": ["Basso", "Blow", "..."],
"config_readable": true
}
}--event narrows events to one entry but always keeps the global toggles — global_mute can be the reason an event never fires. config_readable is false when config.json exists but could not be decoded, meaning the values shown are defaults rather than your real settings.
Change global toggles, one event's settings, or both. Only the provided flags change; everything else keeps its value.
crow notifications set --global-mute
crow notifications set --no-global-mute --sound-enabled
crow notifications set --event checksFailing --event-sound-name Hero --no-event-sound-enabled| Flag | Required | Description |
|---|---|---|
--global-mute / --no-global-mute |
no | Master mute — suppresses every sound and system notification |
--sound-enabled / --no-sound-enabled |
no | Global sound-playback toggle |
--system-notifications-enabled / --no-… |
no | Global system-notification toggle |
--event |
no | Event to change — required by every --event-* flag |
--event-enabled / --no-event-enabled |
no | Whether this event notifies at all |
--event-sound-enabled / --no-… |
no | Whether this event plays a sound |
--event-system-notification-enabled / --no-… |
no | Whether this event posts a system notification |
--event-sound-name |
no | Sound for this event — a built-in name, matched case-insensitively |
Returns the resulting settings in the same shape as get, plus "saved": true.
Notes:
- Every toggle is a
--flag/--no-flagpair; omitting it leaves the stored value alone.--flag --no-flagtogether is rejected rather than silently resolved. - The global toggle is
--system-notifications-enabled(plural) and the per-event one is--event-system-notification-enabled(singular). The flag names mirror the config field names, which differ the same way. --event-sound-nameaccepts only the built-in sounds listed underavailable_sounds, matching the Settings sound picker. A config that already stores a custom sound path keeps it — reads never validate — but the CLI won't set a new one.- Only the event you name is written to
config.json. Events you never touch stay absent and keep following the current defaults — a globals-onlysetleaveseventSettingsalone entirely. - Each write seeds the entry from the event's real default before applying your flags, so
setnever has to be told a sound just to change a toggle. Hand-editing has no such help: an event entry written withoutsoundNamefalls back toGlassrather than that event's default (see configuration.md).
Read and write AppConfig.workspaces — the same list the web Settings → Workspaces tab edits. Writes land under the shared config lock, and the daemon's config poll broadcasts a configReloaded event, so an open web tab refreshes within a couple of seconds.
A workspace maps to a directory under the dev root and decides which forge its repos live on, where its tickets come from, and what extra context its sessions get.
--workspace accepts a workspace name (case-insensitive) or a workspace UUID. A name that matches more than one workspace is an error rather than a guess — nothing enforced name uniqueness before these verbs, so an older config.json can hold duplicates.
The per-workspace AI gateway is not managed here: it carries credentials and is local-only, so it stays with crow gateway. These verbs preserve a workspace's stored gateway across every edit and report only whether one is set.
List every configured workspace.
crow workspace list
crow workspace list | jq '.workspaces[] | select(.task_provider == "jira") | .name'Takes no flags. Returns {"workspaces": [...], "config_readable": true}, each entry in the shape shown under get. config_readable is false when config.json exists but could not be decoded, meaning the empty list is a fallback rather than the truth (the CLI also warns on stderr).
Show one workspace's full configuration.
crow workspace get --workspace Acme
crow workspace get --workspace 3f7c1a90-2b4e-4f18-9c33-5a1d0e6b8742| Flag | Required | Description |
|---|---|---|
--workspace |
yes | Workspace name or UUID |
Returns:
{
"workspace": {
"id": "3f7c1a90-2b4e-4f18-9c33-5a1d0e6b8742",
"name": "Acme",
"provider": "gitlab",
"cli": "glab",
"host": "gitlab.acme.io",
"task_provider": "jira",
"task_provider_explicit": true,
"jira_site": "acme.atlassian.net",
"jira_project_key": "PROPS",
"jira_jql": "assignee = currentUser() AND statusCategory != Done",
"jira_status_map": { "In Progress": "In Dev" },
"corveil_host": null,
"always_include": ["acme/api"],
"auto_review_repos": ["acme/web"],
"exclude_review_repos": [],
"custom_instructions": "Always run make test before pushing.",
"session_env": { "AWS_PROFILE": "dev" },
"gateway_set": true,
"gateway_base_url": "https://gw.acme.io"
}
}task_provider is the effective provider, so it falls back to provider when none is set; task_provider_explicit distinguishes "follow the code provider" from a task provider that happens to equal it. Unset optionals are explicit nulls rather than missing keys. gateway_set and gateway_base_url are all a workspace payload ever reveals about its gateway — the auth header names and values are never included. Use crow gateway get --workspace <name> --reveal for those.
Create a workspace. Only --name is required; every other field takes its documented default and can be set later with edit.
crow workspace add --name Acme
crow workspace add --name Acme --provider gitlab --host gitlab.acme.io \
--task-provider jira --jira-site acme.atlassian.net --jira-project-key PROPS \
--always-include acme/api --session-env AWS_PROFILE=devTakes --name plus every field flag. Returns {"workspace": {...}, "saved": true}.
The name becomes a directory under the dev root, so it must be non-blank, must not contain / or :, must not be . or .., and must not collide case-insensitively with an existing workspace. Creating a workspace does not create its directory — the daemon scaffolds it at next launch.
Change fields on an existing workspace. Only the provided flags change.
crow workspace edit --workspace Acme --host gitlab.acme.io
crow workspace edit --workspace Acme --always-include acme/api --always-include acme/web
crow workspace edit --workspace Acme --clear-always-include
crow workspace edit --workspace Acme --jira-status-in-progress "In Dev"
crow workspace edit --workspace Acme --name Acme2 --force| Flag | Required | Description |
|---|---|---|
--workspace |
yes | Workspace name or UUID |
--name |
no¹ | New name — see the rename guard below |
--force |
no | Rename even when sessions or jobs reference this workspace |
¹ At least one of --name or a field flag is required; --force alone is not a change.
Returns {"workspace": {...}, "saved": <bool>}, plus renamed_from, orphaned_sessions and orphaned_jobs when the name changed.
Renaming is guarded. Nothing stores a workspace id: a session is tied to its workspace only by its worktree living at {devRoot}/{workspace}/{repo}, and a job only by the job.workspace string. Renaming moves no directory, so it silently breaks AI-gateway resolution for those sessions, code-provider detection (which falls back to GitHub), job launches, and the /crow-workspace skill's config lookups — while the daemon scaffolds a new empty directory beside the old populated one. So a rename with live references is refused; --force proceeds and reports the counts.
An idempotent edit is free. If every value you pass already holds, the response is "saved": false and config.json is not rewritten — re-running the same command doesn't churn the file or chime "Config reloaded" in every open browser.
Delete a workspace from the configuration.
crow workspace remove --workspace Acme
crow workspace remove --workspace Acme --force| Flag | Required | Description |
|---|---|---|
--workspace |
yes | Workspace name or UUID |
--force |
no | Remove even when sessions or jobs reference this workspace |
Returns:
{
"removed": true,
"id": "3f7c1a90-2b4e-4f18-9c33-5a1d0e6b8742",
"name": "Acme",
"worktree_dir_kept": "/Users/you/Dev/Acme",
"gateway_discarded": true,
"orphaned_sessions": 0,
"orphaned_jobs": 0
}Notes:
- This removes the config entry only. The workspace directory under the dev root, its worktrees, and its branches are left on disk — same as the web UI.
worktree_dir_keptnames the path so a script can clean up deliberately. - Any AI gateway stored for the workspace is discarded with it, and there is no undo.
gateway_discardedsays whether one was lost; the CLI also warns on stderr. - Like rename, removal is refused while sessions or jobs still reference the workspace unless
--forceis passed.
add and edit accept the same field flags.
| Flag | Description |
|---|---|
--provider |
Code/PR host: github or gitlab |
--host |
GitLab host, e.g. gitlab.example.com — GitLab workspaces only |
--task-provider |
Where tickets live: github, gitlab, jira, corveil, or "" to follow the code provider |
--jira-site |
Atlassian site, e.g. acme.atlassian.net |
--jira-project-key |
Jira project key, e.g. PROPS |
--jira-jql |
JQL for this workspace's ticket board |
--jira-status-backlog |
Jira workflow status name for Backlog |
--jira-status-ready |
Jira workflow status name for Ready |
--jira-status-in-progress |
Jira workflow status name for In Progress |
--jira-status-in-review |
Jira workflow status name for In Review |
--jira-status-done |
Jira workflow status name for Done |
--clear-jira-status-map |
Drop every Crow→Jira status mapping |
--corveil-host |
Self-hosted Corveil host — blank means the public corveil.io |
--custom-instructions |
Free text appended to this workspace's session prompts |
--custom-instructions-file |
Read --custom-instructions from a file; - reads stdin |
--always-include |
Repo always listed in the prompt table (repeatable) |
--clear-always-include |
Empty the always-include list |
--auto-review-repo |
Repo whose review requests auto-create a session (repeatable) |
--clear-auto-review-repos |
Empty the auto-review list |
--exclude-review-repo |
Repo hidden from the review board (repeatable) |
--clear-exclude-review-repos |
Empty the exclude-from-reviews list |
--session-env |
KEY=VALUE exported into agents in this workspace (repeatable) |
--clear-session-env |
Drop every session env var |
Notes:
- Clearing. An optional scalar clears with an empty string —
--host ""— matching the Settings form, where blanking a text input stores nothing. Lists and maps can't say "empty" that way, so they take an explicit--clear-*flag.--jira-status-ready ""clears just that one mapping. - Repeatable flags replace, they don't append. Every
--always-includeon one invocation is the complete new list, matchingcrow job edit --prompt. Same for--auto-review-repo,--exclude-review-repo, and--session-env. --jira-status-*patches per key. Setting one leaves the other four alone — unlike the list flags.- Fields are checked against the resulting workspace.
--hoston a GitHub workspace, or any--jira-*flag on a workspace whose task provider isn't Jira, is an error rather than a value that would be stored and never read. Set the provider in the same invocation and both apply. Clearing a stranded field is always allowed. --session-envis one variable per entry. The/crow-workspacesetup script reads the map as oneKEY=VALUEper line and splits each at the first=, so both delimiters are reserved: a newline in a key or value is rejected (it would smuggle in a second variable), and so is a=in a key (it would come back as a different variable). A=in a value is fine — the split takes only the first one. Keys additionally may not contain whitespace or control characters, since no shell could reference them. All enforced server-side, not just by the CLI.--session-envvalues are not credentials. Unlike a gateway header they are stored in plainconfig.jsonand are not stripped from the web Settings payload. Put tokens in a gateway header instead.cliis derived, never set. It follows--provider(gh/glab) on every write, so a stale value from an older config is repaired by any edit.- There is no
--gatewayflag; see Gateway Commands.
The CLI half of the actions the web Ticket Board and Reviews board expose as buttons — so a Manager agent or a shell script can drive the board without a browser.
Prerequisites differ by verb:
list-tickets/list-reviewsneed a provider-configured tracker. Without one they return an empty board rather than failing. No tmux required.refresh-ticketsneeds a provider-configured tracker and errors without one. No tmux required.work-on-issue,batch-work-on-issues,start-review, andcreate-managerspawn or drive sessions, so they need tmux on the daemon host.quick-actionneeds either the Crow desktop app or tmux on the daemon host.
Print the Ticket Board payload verbatim: every assigned issue, per-status counts, the 24-hour done count, and whether a poll is in flight. Filter with jq.
crow list-tickets
crow list-tickets | jq '.issues[] | select(.project_status == "In Progress")'
crow list-tickets | jq -r '.issues[] | select(.linked_session_id == null) | .url'Returns {"issues": [...], "counts": {"Backlog": N, "Ready": N, "In Progress": N, "In Review": N, "Done": N, "All": N}, "done_last_24h": N, "loading": bool}. Each issue carries id, number, title, state, url, repo, provider, pr_number, pr_url, updated_at, project_status, labels, body, author, created_at, comments_count, pr_state, checks, and linked_session_id (null when no session is working it — the same field the web uses to decide whether to offer "Start Working").
Print the Reviews board payload verbatim: PRs awaiting your review.
crow list-reviews
crow list-reviews | jq -r '.reviews[] | select(.review_session_id == null) | .url'Returns {"reviews": [...], "loading": bool, "unseen": N}. Each review carries id, pr_number, title, url, repo, author, head_branch, base_branch, is_draft, requested_at, labels, provider, and review_session_id (null when no review session exists yet).
Re-poll the ticket provider now instead of waiting for the next automatic poll. Shells out to gh / glab / Jira across every configured repo, so it uses a 120s timeout.
crow refresh-ticketsReturns {"ok": true} after the poll completes, so a following crow list-tickets already sees the new snapshot. Two cases return {"ok": true} without polling at all: a refresh is already in flight, or the provider is rate-limited.
Start working on a ticket. Types /crow-workspace <url> into the primary Manager terminal and lets that agent do the worktree/session setup — identical to the board's "Start Working" button.
crow work-on-issue --url "https://github.com/corveil/crow/issues/817"| Flag | Required | Description |
|---|---|---|
--url |
yes | Ticket / issue URL |
The URL must be an http(s) URL with no whitespace or control characters — it becomes terminal keystrokes, and crow send semantics turn a newline into an Enter press. Returns {"ok": true}; the session appears once the Manager agent has set it up.
Batch counterpart of work-on-issue: types one /crow-batch-workspace <url1> <url2> … line so the Manager runs the parallel batch skill once instead of N sequential submissions.
crow batch-work-on-issues --url "https://github.com/o/r/issues/1" --url "https://github.com/o/r/issues/2"
gh issue list -R corveil/crow --json url --jq '.[].url' \
| crow batch-work-on-issues --urls-file -| Flag | Required | Description |
|---|---|---|
--url |
one of | Ticket / issue URL (repeatable) |
--urls-file |
one of | Read newline-delimited URLs from a file; - reads stdin |
URLs are sent in order: every --url first, then the file's lines (blank lines dropped). Malformed URLs are not rejected locally — the daemon drops them into rejected and starts the rest, so one bad ticket can't block the batch. Duplicates are deduped server-side.
Returns {"ok": true, "sent": N, "rejected": ["..."]}. Check rejected — a non-empty array is a partial success, not a failure.
Clone a pull request, scaffold the review skill, and spawn a review session — identical to the Reviews board's "Start Review" button. Uses a 120s timeout because it clones a repo.
crow start-review --url "https://github.com/corveil/crow/pull/842"| Flag | Required | Description |
|---|---|---|
--url |
yes | Pull request URL |
Unlike work-on-issue, the URL is only checked for non-emptiness: it goes to git clone rather than terminal keystrokes. Returns {"session_id": "..."}.
Create an additional Manager session, named with the lowest unused Manager N — identical to the sidebar's + button.
crow create-manager
crow create-manager --agent cursor| Flag | Required | Description |
|---|---|---|
--agent |
no | Coding agent kind (claude-code, cursor, codex, opencode); default agent when omitted |
Returns {"session_id": "...", "name": "Manager N"}.
--agent is used as given. AgentKind is an open RawRepresentable struct, so any non-empty string parses and an unrecognized kind is not rejected — a typo like claude_code stamps the Manager with a kind no agent is registered for (the terminal then launches with the registry's default agent, but the session record keeps the bogus kind). Only omitting --agent falls back to the configured Manager default. Same open-kind model as crow handoff-agent.
Dispatch a PR next-step into a session's managed agent terminal — identical to the PR badge buttons on a session card. The prompt is deterministic and shared with the auto-respond pipeline.
crow quick-action --session <uuid> --action fixChecks| Flag | Required | Description |
|---|---|---|
--session |
yes | Session UUID |
--action |
yes | fixConflicts, addressChanges, fixChecks, mergePR, or reReview |
Returns {"dispatched": true, "action": "..."} on success. A skipped dispatch is reported as {"dispatched": false, "action": "...", "reason": "..."} with a zero exit code — the RPC succeeded, the session just had no managed terminal, no ready surface, or no linked PR. Scripts must branch on dispatched, not the exit code.
addressChanges, fixChecks, and fixConflicts are refused on a review session (reason: "a reviewer can't modify the branch under review — use Re-review instead"); use reReview there.
An AI gateway is a base URL plus the auth headers sent with it. Agents launched into a workspace inherit them as ANTHROPIC_BASE_URL / ANTHROPIC_CUSTOM_HEADERS; the Manager session has its own gateway (AppConfig.managerGateway) rather than following a workspace's. Header values may be op://… 1Password references, resolved at launch so the secret never rests in config.json — see Configuration.
Local-only. These run over the Unix socket, which is local by construction. The same RPC methods are refused for remote /rpc web clients (RPCWebSocketHandler.localOnlyDenial), because gateway headers are credentials — reads included. A local browser can drive the equivalent controls at Settings → the workspace / Manager gateway editor.
Every subcommand takes exactly one target: --manager, or --workspace with a workspace name or UUID.
Show a gateway's base URL and header names. Header values are blanked unless --reveal, so the default output is safe to paste into a ticket.
crow gateway get --manager
crow gateway get --workspace Corveil --reveal| Flag | Required | Description |
|---|---|---|
--manager |
one of | Target the Manager AI gateway |
--workspace |
one of | Target a workspace's gateway (name or UUID) |
--reveal |
no | Print header values instead of blanking them |
Returns {"gateway_set": true, "base_url": "...", "headers": {...}}, plus workspace_id / workspace_name for a workspace target.
Set a gateway. --base-url and at least one --header are both required — a gateway needs both or neither, so a half-filled one is rejected rather than persisted.
A --header with a blank value keeps the secret already stored under that name. That is how to change a base URL without restating credentials:
crow gateway set --manager --base-url https://gw.example.com --header "X-Api-Key: sk-…"
crow gateway set --manager --base-url https://gw2.example.com --header "X-Api-Key:"
crow gateway set --workspace Corveil \
--base-url https://gw.example.com \
--header "X-Api-Key: op://Prod/Gateway/api_key" \
--header "X-Tenant: acme"| Flag | Required | Description |
|---|---|---|
--manager |
one of | Target the Manager AI gateway |
--workspace |
one of | Target a workspace's gateway (name or UUID) |
--base-url |
yes | Gateway base URL |
--header |
yes | Header as Name: Value (repeatable) |
Returns {"saved": true, "gateway_set": true}.
Remove a gateway. Agents launched afterwards fall back to vanilla Anthropic (the env vars are explicitly unset, so a global ~/.zshrc export cannot leak in).
crow gateway clear --manager
crow gateway clear --workspace Corveil| Flag | Required | Description |
|---|---|---|
--manager |
one of | Target the Manager AI gateway |
--workspace |
one of | Target a workspace's gateway (name or UUID) |
Returns {"saved": true, "gateway_set": false}.
Manage the password that gates the crowd web UI for non-loopback clients. It is stored as a PBKDF2-HMAC-SHA256 hash (210,000 iterations, random per-password salt); the plaintext is never persisted and never leaves the local socket. Local-only, like the gateway commands.
crow web-password status
crow web-password set # prompts twice, echo off
printf '%s' "$PW" | crow web-password set --stdin # for scripts
crow web-password clear| Flag | Applies to | Description |
|---|---|---|
--stdin |
set | Read the password from stdin instead of prompting |
There is deliberately no --password flag — a plaintext password in argv lands in shell history and is visible to any local ps. Changing the password does not require the old one; the local-only gate is the control, matching the web UI. Clearing it means remote web clients are no longer challenged, so check how crowd is bound first.
status returns {"password_set": true, "iterations": 210000} — never the hash or salt. set / clear return {"saved": true, "password_set": <bool>}.
The Jira credential (AppConfig.jiraCredential) is intentionally UI-only. It is a op:// 1Password reference managed outside Crow, and the app resolves it at call time rather than storing a secret — so there is nothing for a CLI verb to write that editing the reference in Settings (or 1Password) does not already cover.
Forwards a Claude Code hook event (e.g. Stop, Notification, PreToolUse) to crowd. The JSON payload is read from stdin and wrapped in an RPC call. This is wired up automatically by Claude Code's hook system — you do not invoke it by hand.
echo '{"tool":"Bash"}' | crow hook-event --session <uuid> --event PreToolUseOn success it is silent; on error it prints JSON to stdout.
The same bridge for Codex, whose notify config is global rather than per-session and passes its JSON payload as the final positional argument instead of on stdin. Crow resolves the session from the payload's cwd, matching it against registered worktree paths — which is why no --session is needed (and why Codex could not supply one anyway).
crow codex-notify '{"type":"agent-turn-complete","cwd":"/path/to/worktree"}'Wired up automatically when a session is handed off to Codex; you do not invoke it by hand.
0— success- non-zero — connection error, validation failure, or RPC error (details on stderr)
When crowd returns an RPC error, the command prints JSON of the form:
{"error": "..."}and exits non-zero. Common causes: crowd is not running (socket connection refused), an invalid UUID, or a session/terminal that does not exist.