CLI reference

The bandwise command line, on npm as @bandwise/cli. Local and live runs, receipts, Claude Code hooks and the first hosted commands work today; the rest arrives in Phase 3.

Hosted commands are in early access

The hosted commands below (spec, publish, rollback, rollout and report --remote) work for orgs in the hosted early access. Sets, evals, approvals and the other management commands arrive in Phase 3. This page will list every command then.

Install

The CLI is published on npm as @bandwise/cli and needs Node.js 22.13 or later. Run it with npx, or install it once:

npx @bandwise/cli run --local spec.json state.json
npm install -g @bandwise/cli    # then: bandwise run --local spec.json state.json

What works today

One command works now:

bandwise run --local <spec.json> <state.json> [--json]
  [--rollout shadow|controlled|full|paused]
  [--channel production|staging]
  [--provider typesafe|openrouter|vercel]

It runs a spec on a state with fixture answers. It makes no network call and needs no key. See the quickstart.

Live runs with your own key

From version 0.2.0 the CLI can call the real model with your own key:

bandwise run --live <spec.json> <state.json> [same options as --local] [--receipts[=<path>]]

The key comes only from your environment: TYPESAFE_API_KEY, or OPENROUTER_API_KEY with --provider openrouter, or AI_GATEWAY_API_KEY with --provider vercel. The CLI never reads a key from a flag, a spec, a profile or a file, never prints it, and sends it only to that provider's API. When the variable is missing, the error names it.

--receipts appends one line per run to ~/.bandwise/receipts.jsonl, or to the path you give with --receipts=<path>. A receipt holds the set, a hash of the spec, each decision's value, band and action, the rollout stage, the System One cost, the counterfactual LLM cost and the latency. It never holds the state.

bandwise report [--since 7d] [--set <slug>] [--receipts <path>] [--compare profile] [--json]

report sums the receipts per set: runs, decisions, the band mix, how many runs would act in controlled (high band and the policy says auto) and on which route, System One spend, counterfactual spend and estimated savings. The savings are estimates. The counterfactual prices one comparator LLM call per decision from the price book.

A receipt from the Stop hook also records the task behind it, as counts only: time from your request to the stop, agent turns, tool calls, a hash of the session id, and the launch profile when one was set (BANDWISE_LAUNCH_PROFILE and BANDWISE_LAUNCH_PICKED). --compare profile groups those receipts by the profile used and the profile picked, and shows the median time to stop, turns, tool calls and done-check outcomes per group. Every row shows how many tasks it covers. Read that count first: a median over five tasks says very little.

Claude Code hooks

bandwise hook <Stop|PreToolUse|UserPromptSubmit> --set <spec.json>
  [--rollout shadow|controlled|full] [--provider typesafe|openrouter|vercel]
  [--timeout-ms 3000] [--receipts <path>] [--drop <field>]
bandwise hooks install [--sets-dir .bandwise/sets] [--command bandwise] [--rollout shadow]

hook reads Claude Code's hook input on stdin, keeps only the fields the set's input schema names, redacts secret-shaped text, runs the set live and writes a receipt. Redaction is pattern based: it covers keys, tokens, auth headers, URL passwords and secret-named values, but cannot promise every private value is gone. Use --drop <field> for any field that must never leave your machine. It prints hook output only when the set may act:

RolloutWhat the hook does
shadow (default)Runs and writes a receipt. Never blocks, denies or adds context.
controlledActs only on a high band answer: done-check sends the agent back, action-risk-gate turns the call into a permission prompt, model-tier adds advice for mechanical tasks.
fullActs on any answer whose action is auto.

Any error, a missing key or a timeout (3 seconds by default) ends the hook with exit code 0 and no output, so a hook never breaks a session. The risk gate skips plain read commands such as git status and ls, and tools other than Bash, Edit, Write, MultiEdit and NotebookEdit.

hooks install prints the .claude/settings.json entries for the agent pack sets in a folder (done-check, action-risk-gate, model-tier), each with --rollout shadow. It writes nothing: review the output and paste it in yourself.

Launch profiles

bandwise launch [--task <text>] [--set .bandwise/sets/launch-profile.json]
  [--profiles .bandwise/profiles.json] [--rollout shadow|controlled|full]
  [--provider typesafe|openrouter|vercel] [--timeout-ms 3000] [--receipts <path>]
  [--print | -- <claude args>]

A running Claude Code session cannot change its own model, but whoever starts it can pass --model and --effort. launch makes that pick before a session starts. It runs the launch-profile set on the task, then starts claude with the chosen profile's --model and --effort:

bandwise launch -- -p "Rename getUser to fetchUser everywhere"

Everything after -- goes to claude unchanged. The task is --task, or else the argument right after -p, or else the only argument after -- (as in claude "prompt"). Any other form, such as --resume abc or options before a bare prompt, has no task, so launch starts the default profile and tells you to pass --task. It never guesses which of claude's options take a value. launch adds only --model and --effort, and not when you passed them yourself. It never adds or changes a permission flag. It starts claude with an argument list and no shell, hands it your terminal, and exits with its exit code.

--print makes the same pick and prints it as one line of JSON instead. It starts nothing, so your own host, script or CI job can start the agent:

{"profile":"standard","sessions":[{"model":"sonnet","effort":"medium"}],"picked":"deep","rollout":"shadow"}

The profiles file is an allowlist you review like code. It names the agent program (claude), a default profile, and each profile as one or two sessions with a model and an effort (low, medium, high, xhigh or max). Nothing else is allowed in it: no flags, paths or commands. A file that breaks that shape stops the command with an error that names the field. launch starts only a profile's first session; a second one is reported on stderr for you to start if you want it.

The set can only pick a profile id from that file. An answer that is not a profile id counts as no pick.

RolloutProfile used
shadow (default)Always the default. The receipt, and picked with --print, show what the set would have chosen.
controlledThe pick, but only on a high band answer. Otherwise the default.
fullThe pick whenever its action is auto.

A missing key, a set that will not load, an error or a timeout uses the default, with one line on stderr saying why, so a launch is never blocked. The task goes to the model with the same redaction as the hooks. The receipt records the profile used and the one picked, never the task. The session gets BANDWISE_LAUNCH_PROFILE and BANDWISE_LAUNCH_PICKED, so its hooks can record the profile for report --compare profile. Apart from those two, it gets the same environment claude would get if you started it from the same shell: launch adds no key and removes none. If your key is in that shell, the session inherits it, which the hooks inside the session need. Notices print before the session starts.

Hosted Bandwise

With a Bandwise token, the CLI manages sets on hosted Bandwise over the HTTP API. It reads the token from BANDWISE_TOKEN and the address from BANDWISE_BASE_URL (default https://app.bandwise.dev). In this mode it never holds a TypeSafe, OpenRouter or AI Gateway key: the server runs the model. The token goes only into the Authorization: Bearer header, over https (plain http only to your own machine). It never appears in output, errors or receipts.

bandwise spec pull <set> [--out <file>] [--json]
bandwise spec push <file> [--set <slug>] [--goal <goalId> [--name <text>]] [--if-match <etag>] [--json]
bandwise spec diff <file> [--set <slug>] [--version <n>] [--json]
bandwise publish <set> [--channel production|staging] [--changelog <text>] [--if-match <etag>] [--json]
bandwise rollback <set> [--channel production|staging] [--to <n>] [--json]
bandwise rollout <set> <inactive|shadow|controlled|full|paused> [--channel production|staging] [--reason <text>] [--json]
bandwise report --remote [--since 7d] [--set <slug>] [--made-by <token name>] [--json]
CommandWhat it does
spec pullPrints the set's draft spec, or writes it to --out. The draft ETag goes to stderr.
spec pushReplaces the draft with the file, then prints the lint errors and warnings. The set slug is the file name without .json, or --set. When the set does not exist yet, --goal creates it first. The CLI reads the draft ETag from the server and sends it as If-Match; pass --if-match to use the ETag from your last pull instead, so a change someone made since then is refused.
spec diffCompares the file with the server draft, or with a published --version. Each change shows its JSON Pointer, the server value and the file value.
publishPublishes the draft as the next version on a channel (default production).
rollbackPoints a channel back at the version it served before, or at an earlier version it served with --to. Never waits for an approval. To move forward, publish.
rolloutSets a channel's rollout stage, with --reason recorded in the audit log.
report --remoteRuns per set from the server for --since (default the last 7 days) and --set: the band mix, errors, System One cost, estimated savings and LLM calls avoided. --made-by counts only the runs one agent token made, by its name or id, and --json then names it as madeBy on each set row.

When a person must approve a change, publish, rollback and rollout print the approval id and the console link, and exit 3. A failed gate prints each gate with what it needs and what it measured. Errors print one line with the code, the HTTP status, the message and the request id, then each detail, then what to do next. For example, a draft that changed since you read it:

error precondition_failed (HTTP 412): The draft changed. [request req_01J...]
  the draft changed on the server (now 9f2c...). Run bandwise spec diff, merge, then push again

Reads retry after a network error, a 429 or a retryable 503. Writes carry an Idempotency-Key and retry after a 429 or a retryable 503 with the same key. The server stores each write's answer under its key in the same transaction as the write, for 24 hours, so a retry of a write that already landed gets the stored answer back (with the header Idempotent-Replayed: true) and nothing runs twice. A write that hits a network error is not retried, because it may have landed: check with a read, such as bandwise spec pull or the console, before you send it again. bandwise hook never retries its run.

Hooks on hosted Bandwise

With BANDWISE_TOKEN set, bandwise hook calls POST /api/v1/sets/<slug>/run instead of the model. The slug is the --set file name without .json, or --remote-set. The local spec still decides which fields leave your machine, with the same redaction and --drop. The set's rollout stage on the server decides whether the hook may act, so --rollout and --provider do nothing in this mode, and a set moves from shadow to controlled without editing .claude/settings.json. The timeout and the rule that any error exits 0 with no output stay the same. Receipts are still written, with provider: "bandwise" and a remote field holding the host, version, channel and run id. Without BANDWISE_TOKEN, hooks run live with your own key as before.

Conventions the full CLI will follow

  • Every command supports --json for scripts and agents.
  • It never prompts when input is not a terminal. A command that needs confirmation then fails and asks for --yes.
  • Exit codes: 0 ok, 1 error, 2 a diff or drift was found, 3 an approval is pending.
  • It reads BANDWISE_TOKEN and BANDWISE_BASE_URL today, and a named profile later. Hosted commands never hold a TypeSafe, OpenRouter or AI Gateway key. Only run --live, launch and hook without a token read your own key, from your environment.

Bandwise is an independent product built on TypeSafe's System One models. It is not TypeSafe's documentation. For the System One models themselves, see docs.typesafe.ai.

On this page