CLI command reference

Every roscoe subcommand and flag.

Every subcommand of the roscoe binary, with full flag tables and worked examples. Where source matters, apps/cli/src/commands/<name>.ts is the canonical definition.

The compiled binary (bun run compileapps/cli/dist/roscoe) and the source-mode runner (bun apps/cli/src/index.ts) expose the same commands. Examples below assume the binary is on PATH.

Command Purpose
roscoe init Scaffold data dirs, run DB migrations, seed example workflows
roscoe serve Start the HTTP server + embedded web UI
roscoe stop Stop the running server for the current ROSCOE_HOME
roscoe locate Print where a running server is, or exit 3 if none
roscoe mcp Start the MCP stdio server (for Claude Code)
roscoe run Run a workflow by id or YAML path
roscoe list List available workflows from project + global sources
roscoe resume Resume a paused run with a chosen transition
roscoe monitor Print the current state of a run
roscoe history List recent runs for the current project
roscoe validate Validate a workflow YAML against the schema
roscoe version Print the installed version + check for a newer release
roscoe update Download + apply the latest release; auto-restart roscoe serve
roscoe install-mcp Register the binary as an MCP server for Claude Code or Desktop
roscoe register Register a project so it shows in the UI project switcher
roscoe unregister Remove a project from the UI project switcher (deletes no files)

roscoe init#

Scaffold the global data dir (~/.roscoe/) and (when inside a git repo) the project data dir (<repo>/.roscoe/). Runs DB migrations, writes a roscoe.yaml config + JSON Schema, wires .vscode/settings.json for YAML autocomplete, drops a self-contained .gitignore inside <repo>/.roscoe/ (create-only — --force does not overwrite it), and seeds the starter workflows into the global dir.

Whenever global init runs (anything but --repo-only), it also installs the global /roscoe slash command at ~/.claude/commands/roscoe.md so Claude Code / Cowork users can launch workflows with /roscoe <workflow-name>. The write is best-effort and idempotent (always overwritten) — a failure (e.g. a read-only home) is logged but never aborts init.

roscoe init [--global-only] [--repo-only] [--force] [--no-examples]
Flag Type Default Behaviour
--global-only boolean false Touch only ~/.roscoe/; skip repo scaffolding
--repo-only boolean false Scaffold only <repo>/.roscoe/; skip global init and DB migrations
--force boolean false Overwrite existing example workflow files and roscoe.yaml
--no-examples boolean false Skip seeding example workflows (commander negation pattern)

Example workflows seeded into ~/.roscoe/workflows/: demo-1-the-panel, demo-2-week-of-dinners, demo-3-name-tournament, demo-4-second-draft, demo-5-reality-check, demo-6-gift-shortlist, demo-7-fair-price, demo-8-smell-test, demo-9-check-one-claim. The repo workflow dir is left empty so a team can fill it with shared workflows.

Exit codes: 0 always (failures in individual steps are logged but never abort init — by design, you can re-run it any time).

# Default: scaffold both global and (if in a repo) project dirs
roscoe init

# I'm not in a repo and don't want the warning
roscoe init --global-only

# Refresh the roscoe.yaml schema and re-seed examples
roscoe init --force

roscoe serve#

Start the HTTP server with the embedded web UI on a single port. Available only from the compiled binary (the source-mode dev server is bun run dev, which runs API + Vite separately).

roscoe serve [-p <port>] [--no-open] [--no-register]
Flag Type Default Behaviour
-p, --port <port> number see below Port to listen on. 1..65535
--no-open boolean opens Skip auto-opening the browser once the server is up
--no-register boolean registers Skip auto-registering the current git root as the active project

By default, roscoe serve registers the detected git root (making it the active project) and opens the URL in a browser. Auto-register failure is soft: it's logged and the server still starts.

Port resolution order (first match wins):

  1. --port flag
  2. ROSCOE_PORT env var
  3. ports.serve in roscoe.yaml
  4. Built-in fallback 7777

When the port comes from the fallback only, roscoe serve will auto-bump up to 9 ports forward to find a free one (so 7777, 7778, … 7786). Any explicitly set port is never silently bumped — if it's busy, roscoe serve exits with [roscoe] Port <N> is in use.

Exit codes: 0 on clean shutdown (SIGINT/SIGTERM), 1 on bind failure or invalid port.

roscoe serve                  # listens on 7777 (or next free port)
roscoe serve -p 8000          # listens on 8000 only; fails if busy
ROSCOE_PORT=9001 roscoe serve  # same effect via env var

roscoe stop#

Stop the running roscoe serve for the current ROSCOE_HOME, and wait for it to actually exit before returning. It triggers the server's own authenticated self-shutdown (POST /api/serve/restart, which SIGTERMs its own process), not a cross-process kill, so it's safe regardless of who started the server.

roscoe stop

No flags. Waits up to 10 seconds for the listener to disappear after requesting the stop.

The server aborts in-flight runs before it exits, so executor children of a running workflow are torn down rather than orphaned.

Exit codes: 0 if stopped (or nothing was running), 1 if a server is running but couldn't be stopped (missing auth token, or the request failed).

roscoe stop  # e.g. before roscoe update installs a new binary manually

roscoe locate#

Print where a running roscoe serve is for the current ROSCOE_HOME, without starting one. This is the attach-logic primitive the desktop shell uses to decide whether to reuse an existing server or spawn a new one; it's also useful from a shell to find the port and auth token of a server you started elsewhere.

roscoe locate [--json]
Flag Behaviour
--json Emit machine-readable output

Exit codes: 0 if a server is found, 3 if none is running (the signal an attach caller uses to decide to spawn one), 1 on an unexpected error.

roscoe locate
roscoe locate --json | jq .baseUrl

roscoe mcp#

Start the MCP stdio server. Reads protocol messages on stdin, writes them on stdout — never use console.log from this command, or you'll corrupt the protocol stream. Diagnostic output goes to stderr.

roscoe mcp

No flags. Auto-loads .env.dev from the current repo if present (so a dev shell pointing at ~/.roscoe-dev keeps working without explicit env). Sets ROSCOE_SWEEP_TEST_RUNS=1 to enable cleanup of orphaned test_workflow rows.

Exit codes: 0 on SIGINT/SIGTERM. There is no explicit failure exit — a throw during tool registration or transport connect propagates and exits non-zero through Node's default handler (typically a corrupted DB — see Database recovery).

This is the entry point that Claude Code shells out to. Don't run it directly unless you're debugging the MCP layer; use roscoe install-mcp to register it with Claude Code instead.

roscoe run <workflow>#

Run a workflow by id (resolved from project or global sources) or by explicit YAML path. Routes through a running roscoe serve (or the dev API under bun run dev) — if neither is up, exits with a hint naming roscoe serve and ROSCOE_PORT. Running from source, the hint names the dev server too.

In a TTY, mounts a live Ink view with a node tree, status, and HITL prompt for human nodes. Press q to detach (the run keeps executing on the server), c to cancel, or o to open the run page in the browser.

With --json, the run is polled to completion (up to 120 s) and the result is printed as { runId, status, totalCostUsd, meteredCostUsd, subscriptionEquivCostUsd, billingMode, inputTokens, outputTokens }, plus stopReason when one is set. Outside a TTY without --json, no JSON is printed — just the run id and a monitor hint.

roscoe run [<workflow>] [-v key=value...] [-i name=value...] [-s global|repo]
          [--json] [--no-tty] [--attach <runId>] [--run-here]
          [--open cowork|claude_code] [--spending-cap <usd>]
          [--spending-cap-tokens <n>]
Argument / flag Type Default Behaviour
<workflow> string Workflow id (e.g. demo-1-the-panel) or path (e.g. examples/foo.workflow.yaml)
-v, --var <pairs> string[] Context variables as key=value, repeatable
-i, --input <pairs> string[] A declared workflow input as name=value, repeatable. JSON-parsed when valid, else used as a raw string.
-s, --source enum Force global or repo when both have the same id
--attach <runId> string Skip workflow start; mount the live view against an existing run. Requires an interactive TTY — exits 1 with --json, --no-tty, or piped output
--json boolean false Emit a JSON result and exit (no live UI)
--no-tty boolean false Force non-interactive mode even in a TTY
--run-here boolean false Run server-side, skipping the subscription-handoff prompt. Metered only if ANTHROPIC_API_KEY is set; otherwise it still runs on your Claude subscription
--open <cowork|claude_code> string Hand the run off to that surface instead of running it here
--spending-cap <usd> number Cancel the run once its running cost exceeds this many USD
--spending-cap-tokens <n> number Cancel the run once its total input+output tokens exceed this count

Workflow resolution — if the argument contains /, \, ends in .yaml / .yml, or names an existing file, it's treated as a path. Otherwise it's a workflow id. When the same id exists in both sources, repo wins; pass --source global to override. When repo shadows global, a # resolved from repo (shadows global) notice is printed. Path-based starts are realpath-checked server-side; non-existent or unreadable paths return 400.

Where the run executes. --run-here and --open are explicit overrides; absent either, the choice follows preferences.defaultRunLocation in roscoe.yaml (here, cowork, claude_code, or the default ask). ask prompts interactively in a TTY when Claude Desktop is installed, and falls back to here otherwise (so CI never hangs). See Running on your Claude subscription for what runs where, and roscoe.yaml for the preference.

Exit codes: 0 on successful start (or live-view detach), 1 on resolution / start / server-not-running failure.

roscoe run demo-1-the-panel
roscoe run demo-1-the-panel --source global
roscoe run examples/hello-world.workflow.yaml
roscoe run demo-4-second-draft --input goal="warm but firm"
roscoe run summarize --input document="my text" --input maxWords=50
roscoe run demo-1-the-panel --json                  # CI / agent
roscoe run --attach 01HXYZ...                       # reattach to a running run
roscoe run demo-1-the-panel --run-here              # skip the handoff prompt
roscoe run demo-1-the-panel --spending-cap 2.50     # cancel past $2.50

roscoe list#

List workflows from both project (<repo>/.roscoe/workflows/) and global (~/.roscoe/workflows/) sources. The project section is omitted entirely when there's no project source.

roscoe list

No flags. Shadowed entries (same id in both sources) are marked [shadowed by repo] in the global section.

Exit codes: 0 always.

roscoe resume <run-id> [transition]#

Resume a paused run. With a transition (positional or --transition), runs non-interactively (suitable for scripts and CI). In a TTY without one, mounts the same interactive Select picker roscoe run uses for HITL prompts. Like roscoe run, requires a running server.

roscoe resume <run-id> [transition] [--transition <name>] [--no-tty]
Argument / flag Type Behaviour
<run-id> string Run id from roscoe run / roscoe history
<transition> string Optional transition key; required outside a TTY
--transition <name> string Scriptable alternative to the positional transition; wins if both are given
--no-tty boolean Force non-interactive mode (a transition is then required)

Exit codes: 0 on success, 1 if the run can't be resumed (not paused, unknown transition, run not found, server not running).

roscoe resume r-abc123 approve
roscoe resume r-abc123 reject
roscoe resume r-abc123 --transition approve
roscoe resume r-abc123                # interactive picker (TTY only)

roscoe monitor <run-id>#

Inspect a run. In a TTY, mounts the same live Ink view as roscoe run without starting a new run — useful for picking up an in-progress run from another shell. With --json or when piped, prints a one-shot snapshot (run + per-node trace) and exits without contacting the server.

roscoe monitor <run-id> [--json] [--no-tty]
Argument / flag Type Behaviour
<run-id> string Run id from roscoe run / roscoe history
--json boolean One-shot JSON snapshot (reads local DB, no server required)
--no-tty boolean One-shot text snapshot (reads local DB, no server required)

Exit codes: 0 on success, 1 if the run isn't found or the live view can't reach the server.

roscoe history#

List recent runs for the current project, newest first.

roscoe history [-n <limit>] [--all]
Flag Type Default Behaviour
-n, --limit number 20 Max rows. Invalid values fall back to 20.
--all boolean false Show runs from every project, not only the current git repo

When run outside a git repo, defaults to --all semantics with a notice. Output columns: STATUS · WORKFLOW · AGE · ID · DURATION. Paused rows print a roscoe resume <id> hint underneath.

Exit codes: 0 always.

roscoe validate <file>#

Validate a workflow YAML file against the Zod schema, the integrity checker, and the cross-workflow graph check. Prints schema errors first, then integrity errors, then graph errors, stopping at the first failing stage. Paths are resolved from the current working directory.

roscoe validate <file>
Argument Type Behaviour
<file> string Path to a *.workflow.yaml file

The graph check follows subworkflow nodes to their child workflow files and reports missing children, unknown child input/output names, reference cycles, and depth violations. It's a no-op for workflows with no subworkflow nodes.

Exit codes: 0 if valid, 1 if any of the three checks fails.

roscoe validate examples/my-workflow.workflow.yaml

When the workflow's version: field doesn't match the binary's supported schema version, the error tells you which way the mismatch goes and what to do — newer-than-binary asks you to run roscoe update, older-than-binary asks you to update the file or downgrade.

roscoe version#

Print the installed version and check the release manifest for a newer one.

roscoe version [--json] [--check] [--no-fetch]
Flag Behaviour
--json Emit { current, latest, updateAvailable, pending, swapped, restartRequired, releasedAt, lastChecked, lastError } instead of a human-readable line. For scripts.
--check Exit code 0 if up to date, 1 if a newer release is available. Useful in cron + CI gates.
--no-fetch Never reach the network; report only current plus the cached poll state. Used by the desktop shell, which needs current and must not block startup on a fetch.
# Human output
roscoe version

# Script-friendly
roscoe version --json | jq .updateAvailable

# CI gate — fail the build if the local binary is stale
roscoe version --check || echo "::warning::roscoe binary out of date"

The manifest URL defaults to https://roscoe.run/v/manifest.json; override with ROSCOE_MANIFEST_URL (used by tests and self-hosted mirrors).

roscoe update#

Download the latest release, verify its sha256 against the manifest, and swap it over the installed binary at ~/.roscoe/bin/roscoe (or wherever ROSCOE_INSTALL_DIR points). If a roscoe serve process is running on this machine the update token from ~/.roscoe/serve.lock is used to ask it to restart cleanly on the new version.

roscoe update [--check] [--force] [--rollback] [--no-restart]
Flag Behaviour
--check Report whether a newer release exists without installing. Exits 1 if an update is available, 0 if up to date.
--force Reinstall the latest version even if it matches the running one (useful when a download was interrupted).
--rollback Restore the previous binary from ~/.roscoe/updates/previous/roscoe. Reverts the most recent update.
--no-restart Skip the auto-restart of the local roscoe serve. Useful when running under a supervisor.
# Standard upgrade path
roscoe update

# Will an update do anything? (handy in a Makefile)
roscoe update --check

# Revert the last update
roscoe update --rollback

roscoe serve also pre-downloads new releases in the background, so when you (or the Settings → Updates button) trigger an install the asset is already on disk and the swap is near-instant. The download path verifies sha256 before the rename, and on macOS the new binary is xattr-stripped and ad-hoc codesigned so Apple Silicon doesn't refuse to launch it. The previous binary stays on disk at ~/.roscoe/updates/previous/roscoe until the next update overwrites it.

roscoe update refuses to run when the active binary isn't at the canonical install location (e.g. you copied roscoe to /usr/local/bin/). In that case it prints the path it found and asks you to re-run install.sh or use your package manager's update command.

roscoe install-mcp#

Register the roscoe entry so a Claude MCP client launches the compiled binary as its MCP server. By default it targets Claude Code and writes the user-global ~/.claude.json (top-level mcpServers), so the install applies to every project; the machine-specific binary path stays out of any shared repo. Pass --client claude-desktop to target the Claude desktop app instead. Must be run from the compiled binary, not from source.

roscoe install-mcp [--client <name>] [--project] [--path <file>] [--command-path <path>]
Flag Type Default Behaviour
--client <name> string claude-code Target MCP client: claude-code (~/.claude.json) or claude-desktop
--project boolean false Write a project-local ./.mcp.json instead of ~/.claude.json (Claude Code only)
--path <file> string (scope default) Explicit config file path; overrides the scope default
--command-path <path> string this binary Binary path to record as the MCP server command. The desktop app passes the canonical ~/.roscoe/bin/roscoe path so the entry stays valid across app updates and deletion.

With --client claude-desktop the entry is written to the desktop app's config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, ~/.config/Claude/claude_desktop_config.json on Linux, %APPDATA%\Claude\claude_desktop_config.json on Windows, or the Microsoft Store build's per-package path under %LOCALAPPDATA%\Packages\Claude_*\), creating the directory if needed. --project is not valid for Claude Desktop (it has no project-local config).

All target files use the same top-level mcpServers shape. Writes:

{
  "mcpServers": {
    "roscoe": {
      "command": "<path-to-roscoe-binary>",
      "args": ["mcp"]
    }
  }
}

Every other key in the file is preserved: ~/.claude.json's projects map and any other MCP servers stay put, only mcpServers.roscoe is replaced. The write is atomic (temp file + rename), so a populated ~/.claude.json is never truncated. A missing target file is created. Refuses to run from bun (source mode), since the resulting process.execPath would be the bun interpreter, not the roscoe command.

/roscoe slash command. The default user-global Claude Code install also writes ~/.claude/commands/roscoe.md (the same /roscoe command roscoe init installs) and prints its path. This is deliberately skipped for --project (a repo-local opt-in shouldn't create a machine-wide command file), for --client claude-desktop (the desktop app has no slash commands), and for --path targets. The write is best-effort — a failure is reported but never fails the MCP registration.

Exit codes: 0 on success, 1 if run from source or if the target config file exists but is malformed JSON (in which case it is left untouched).

roscoe register#

Register a project in the local project registry so it appears in the web UI's project switcher (the roscoe serve UI scopes itself to the active registered project). Registration does not move or copy any files; it only records the path.

roscoe register [path] [--activate] [--name <name>]
Argument / flag Type Default Behaviour
[path] string enclosing git repo Project path; defaults to the git root of the current directory
--activate boolean false Set this project as the active one after registering
--name <name> string directory basename Custom display name shown in the switcher

Entries are keyed on the resolved (realpath) git root, so registering the same repo twice is idempotent and sibling worktrees of one repo each get their own entry. Exits 1 when path isn't inside a git repo.

roscoe register                       # register the current repo
roscoe register ~/dev/projectA --activate
roscoe register . --name "Billing service"

roscoe unregister#

Remove a project from the registry — the inverse of roscoe register. This only unregisters the project from the UI project switcher; it deletes no files (workflows, roscoe.yaml, and run history all stay on disk).

roscoe unregister [path]
Argument Type Default Behaviour
[path] string enclosing git repo Project path; defaults to the git root of the current directory

Removal is keyed on path, so it targets exactly one entry even when sibling worktrees of the same repo share a registry id. It resolves the same candidate paths registration uses (git root + realpath), so it still matches an entry for a path whose git root can no longer be resolved (e.g. a removed worktree). Exits 1 when no registered project matches the path.

roscoe unregister                     # unregister the current repo
roscoe unregister ~/dev/projectA

Environment variables#

Variable Affects Behaviour
ROSCOE_HOME All Override the global home dir (default ~/.roscoe)
ROSCOE_WORKFLOWS_DIR Resolution Override only the global workflows dir (back-compat)
ROSCOE_REPO_HOME Resolution Override the detected repo root (set automatically)
ROSCOE_PORT roscoe serve Default port. Beaten only by --port; it in turn beats roscoe.yaml's ports.serve (see the precedence list above)
ANTHROPIC_API_KEY AI nodes If set, AI nodes use the Anthropic SDK directly
ROSCOE_SWEEP_TEST_RUNS roscoe mcp When 1, sweeps orphaned test_workflow rows on startup
ROSCOE_MANIFEST_URL roscoe version, roscoe update Override the release manifest URL (tests, self-hosted mirrors)
ROSCOE_INSTALL_DIR roscoe update Where the installed binary lives (default <ROSCOE_HOME>/bin)

For the dev server (bun run dev), .env.dev at the repo root sets ROSCOE_API_PORT (default 8080), ROSCOE_WEB_PORT (default 3000), and ROSCOE_HOME=~/.roscoe-dev so dev runs don't pollute the production DB.

Where to next#

  • Running workflows from the CLI — the same commands as a walkthrough rather than a lookup table.
  • MCP tools — what roscoe mcp exposes once an agent is connected to it.
  • REST API — the HTTP surface roscoe serve puts these commands on top of.
  • Environment variables — the full list, of which the table above covers only what the CLI reads.
  • roscoe.yaml — the config file init writes and serve reads for its port.

View this page as Markdown

Predictable workflows from unpredictable AI