#CLI Reference

The CLI ships with the SDK — the anvil command comes from installing @useanvil/sdk. Once the SDK is in a project, run everything below from that project via npx anvil <command>, or bare anvil if you installed the SDK globally. Starting from nothing, npx -y @useanvil/sdk@latest protect <action> installs the SDK and wraps your first call in one step. Everything below is task-first, then a full reference per command.

Requires Node.js 20 or newer. npx is part of Node, so on a machine without it every command here fails with command not found: npx before Anvil is ever downloaded. Check with node --version; install from nodejs.org if it errors. See Quickstart → Before you start.

Most commands take --help. Five don't — serve, setup, init, start and remove — and anvil setup --help runs setup and writes files rather than printing anything, so don't reach for it to find out what setup does. For those five, the Syntax line in their entry below is the reference. Most commands support --json, which emits a single JSON envelope to stdout and routes every progress line to stderr, so anvil <cmd> --json | jq … is a first-class scripting path — where it's scoped to particular subcommands, the entry below says so. --version is a top-level probe: anvil --version, not anvil <cmd> --version.


#1. What do you want to do?

I want to…Run
Connect this machine to my accountanvil login
Protect a new risky callanvil protect <action>
Stop protecting oneanvil remove <action>
Open my dashboardanvil open
Check that everything's healthyanvil doctor
See what Anvil would have caughtanvil audit stats --since 7d
List the calls enforce actually blockedanvil audit query --status blocked
Roll up recent decisionsanvil audit stats --since 24h
Export the whole ledgeranvil audit query --all --json
Flip an action from ghost to enforcethe dashboard · or anvil control set-action-mode <action> enforce
Trip the global kill switchanvil control kill-switch on --reason "…"
Get told when a call is heldanvil approvals webhook set <url>
Start a local dashboard for developmentanvil serve
Verify my audit log wasn't tampered withanvil verify
Inspect a specific receiptanvil receipts inspect <receipt-id>
Replace a key I think has leakedanvil keys rotate --yes
Unstick a key whose retries are refused as duplicatesanvil db settle-claim --list, then anvil db settle-claim <key> --reason "…"
Refresh where the dashboard thinks my code isanvil sync
Find out which calls here are worth protectinganvil scan
See inventory, health and rollout state on one screenanvil cc

The commands below are the ones you'll reach for. anvil --help lists everything the CLI ships.


#2. Command reference

Alphabetical. Every entry uses the same shape: description → gotcha → syntax → key flags → example → side effects → exit codes.


#anvil audit

Query or roll up your decision log — every ghost verdict and every enforce block.

  • ⚠ Gotcha — --status matches the recorded verdict, not the ghost one. A ghost call ran, so its record stores status: "executed" and the hypothetical verdict rides inside ghost.decision (would_block, would_dedup). --status blocked filters the stored field, so it returns enforce blocks only — in ghost it comes back empty, however much Anvil would have caught. For the ghost view use anvil audit stats, which counts would-catches separately, or read ghost.decision straight from the raw log at ./artifacts/anvil-audit.jsonl.
  • ⚠ --status is not validated. A value Anvil doesn't recognise isn't rejected — it's passed to the store, matches nothing, and prints No matching records. with exit 0. That is indistinguishable from "you genuinely have none." Two ways to get bitten: a typo, and anvil audit query --status --json, where --json is swallowed as the status value. Put --status last, or check your spelling against the six below.
  • ⚠ An unparseable --since is neither honoured nor fatal. Only <n>m, <n>h and <n>d parse — 30m and 90d are as valid as 24h. Anything else prints Invalid --since value '…', computes all-time figures, and exits 1 — under a header still naming the window you asked for. The header and the numbers beneath it can disagree, so in a script trust the exit code, not the heading.
  • Syntax: anvil audit <stats|query|incident|replay-spool> [flags]
  • Key flags: --since 24h|7d|30d, --status executed|duplicate|conflict|blocked|unknown|unavailable, --action <action>, --limit <n>, --json, --local
  • Which ledger it reads. With ANVIL_API_KEY set — or on a machine paired with anvil login — it reads your workspace's hosted ledger, the same records the dashboard shows. An environment key always outranks the pairing. Otherwise it reads whatever store this install is configured with (Postgres if ANVIL_POSTGRES_URL is set, else the JSONL log). --local forces the local file.
  • A capped read says so. anvil audit stats asks the store for far more records than it will return; the store stops at its ceiling (10,000). Rather than compute an "all time" roll-up from the newest ten thousand, it now prints every figure in that read as N+ and names the cap above them. 10,000+ decisions means at least that many — read the whole block that way, not just the headline.
  • --all exports the whole ledger, walking the hosted cursor to the end rather than the newest page. It never truncates silently: a walk that stops early prints why and exits 1, so an incomplete export can't be mistaken for a complete one.
  • Example:
    sh
      anvil audit query --status blocked --since 7d --json
      
  • Side effects: Read-only, except replay-spool (below).
  • Exit codes: 0 success — including an unrecognised --status (see the gotcha). 1 unknown subcommand, incident without --action, or replay-spool leaving records stranded.

#anvil audit replay-spool

Replay audit records the durable store refused back into the ledger.

A decision that could not be recorded is spooled to a local dead-letter file rather than dropped — that spool is what makes it safe for the writer to shed load during a store outage instead of queueing until it runs out of memory. This is how you get those records back.

  • Syntax: anvil audit replay-spool [--dry-run] [--file <path>] [--json]
  • Key flags:
  • --dry-run — report what is stranded without writing anything.
  • --file <path> — a spool other than $ANVIL_AUDIT_DEAD_LETTER (default <ANVIL_AUDIT_LOG>.deadletter.jsonl).
  • Example:
    sh
      anvil audit replay-spool --dry-run   # what is missing from the ledger?
      anvil audit replay-spool             # put it back
      
  • Side effects: Writes each spooled record into the ledger and removes it from the spool. Replayed records are re-chained at the tail — their original chain links name a head the ledger has moved past, so re-inserting them verbatim would fork the chain. Record content, including recordedAt, is untouched, so a row lands in the right place chronologically and replaying twice is a no-op rather than a duplicate. Records that fail again stay in the spool; the command is safe to re-run.
  • Exit codes: 0 nothing is stranded — the spool was empty, or every record in it replayed. 1 records remain outside the ledger — including unreadable spool lines, which are kept for inspection rather than discarded.

#anvil control

Show or change the resolved runtime mode — global default, per-service, or per-action — plus the global kill switch.

  • Where it writes, and why that's the whole story. Control has to be written where enforcement is read, so this command routes on your credentials rather than making you pick:
You haveIt writesAvailable there
ANVIL_API_KEY, no ANVIL_CONTROL_URL — what anvil protect leaves you withYour hosted workspaceshow · set-action-mode · kill-switch
ANVIL_CONTROL_URL + ANVIL_CONTROL_TOKENYour own control planeshow · set-action-mode
Neither, or --local anywhere in the commandThis machine's local RedisEverything

This is the break-glass: with only the key in your .env you can stop enforcing from a terminal, with no dashboard, no browser, and no second credential. A subcommand that isn't available on your transport is refused by name and tells you to add --local — it never writes somewhere nothing reads and reports success.

  • ⚠ What's my service? Whatever ANVIL_SERVICE is set to in the app doing the guarding. Unset — the common single-service deploy — the service is the part of the action name before the first dot, so refund.issue lives under refund. On the hosted path the service is derived from the action and a service argument is accepted and ignored, so both set-action-mode refund.issue enforce and set-action-mode refund refund.issue enforce do the same thing. Against local Redis and against a control plane you run yourself, all three are required — set-action-mode <service> <action> <mode>; only the hosted path takes the short form. anvil control resolve <service> <action> prints the service, mode and policy that will actually apply — both arguments are required, and it runs against the local store only.
  • ⚠ Who asks for a reason. Weakening protection (enforce → ghost) is refused without --reason <text> of 5+ characters by a control plane you run yourself (ANVIL_CONTROL_URL) and by the dashboard — the server is the gate, so a raw API call can't slip past the audit trail either. The hosted CLI path (ANVIL_API_KEY, what anvil protect leaves you with) and the local store both accept a downgrade without one. Pass it anyway: on every transport the reason is what makes the downgrade answerable in the control history, and it is the first thing an incident review asks for. The kill switch is the exception that is strict everywhere — engaging it needs a reason; lifting it never does.
  • Syntax: anvil control <show|resolve|set-mode|kill-switch|set-service-mode|set-action-mode|clear-action-mode|set-service-policy> [--local]
  • Key flags: --json; --local (force this machine's Redis, from any transport); --reason <text> (5+ characters; always required to engage the kill switch, and required for a downgrade on the transports named in the gotcha above); --if-not-stronger (register ghost only if the action has no mode yet — it never downgrades an existing enforce, which is how the editor registers a wrap safely).
  • Example:
    sh
      anvil control set-action-mode refund.issue ghost --reason "spike in blocks"
      anvil control kill-switch on --reason "investigating retry storm"
      anvil control show --action refund.issue      # what does the plane say right now?
      
  • Reading it back. show without --action answers the workspace-wide question — kill switch and whether an Anvil operator has capped you to observe. Verify against the plane rather than trusting the printout of the command you just ran.
  • How fast it applies. The command prints the window it applied within — the plane's own snapshot TTL, ANVIL_HOSTED_POLICY_CACHE_TTL_MS, 5 seconds by default, across every replica. That is not the whole trip. On the hosted path a flip has to clear two caches in series: the plane's 5 s snapshot, and then each SDK process's in-process control cache (ANVIL_CONTROL_CACHE_TTL_MS, 3 s). Budget up to ~8 seconds before every process is enforcing the new mode, not the 5 the command reports. In an incident, wait the full window before concluding a flip didn't take — and reach for anvil control show --action <action> to ask the plane rather than re-issuing the write.
  • Side effects: Writes the control config where your enforcement reads it. Engaging the kill switch and every downgrade append to the append-only control history; lifting the switch does not require a reason, because making the safe direction harder is how a lever goes unused in an incident.
  • Exit codes: 0 success, 1 invalid subcommand / missing args / a subcommand unavailable on this transport / weakening protection without a reason.

#anvil cc

The control centre — inventory, health, money and rollout state for the whole workspace on one screen. It writes a self-contained HTML file you open in a browser; --serve runs the same view live, with control history and in-UI apply.

Reach for it when the question is "what is the state of everything?" rather than "what is this one action doing?" — the dashboard's Actions tab is better at the second.

  • ⚠ Gotcha — --serve is live, not a preview. The written HTML is a read-only snapshot, but --serve's Control composer applies real changes: its apply button writes the same control config anvil control set-action-mode writes, and enforcement reads it within the usual window. Treat that screen the way you'd treat the dashboard, not a report.
  • Syntax: anvil cc [--window 1h|24h|7d] [--out <path>] · anvil cc --serve [--port N] [--refresh-seconds N]
  • Key flags:
  • --window 1h|24h|7d — the period every figure covers. Default 24h.
  • --out <path> — where to write the HTML. Default ./artifacts/anvil-control-center.html.
  • --serve — run it as a live-refreshing local server instead of writing a file. This is the mode with control history and in-UI apply.
  • --port N — port for --serve. Default 3088.
  • --refresh-seconds N — how often --serve re-reads. Default 15.
  • ⚠ An unrecognised value is ignored, not rejected. --window, --port and --refresh-seconds each validate their argument and silently fall back to the default when it doesn't parse — so --window 30d gives you a 24-hour report that says nothing about it. Check the window in the rendered header before you read the numbers.
  • No --json. This command's output is the HTML. For machine-readable state use anvil control show --json, anvil doctor --json, or anvil audit stats --json.
  • Example:
    sh
      anvil cc --window 7d
      # Wrote /path/to/artifacts/anvil-control-center.html
    
      anvil cc --serve --port 3088     # live, with in-UI apply
      
  • Side effects: Writes the HTML file, and — only when the inventory holds at least one service — anvil-service-owners.template.json beside it. Reads your configured control store, ledger and Postgres. --serve starts a local server and its in-UI apply writes real control config — the same write anvil control set-action-mode makes, so treat that screen as live, not a preview.
  • Exit codes: 0 the report was written (or the server started), 1 it could not be.

#anvil db settle-claim

Settle one durable claim stuck pending, the way the reclaim sweep would, and record who did it. A stuck claim refuses every retry of its key as a duplicate. This ends that without psql. It is for whoever holds the Postgres the claims live in: a self-hosted operator, or the Anvil team on hosted.

It decides nothing itself. The sweep's rules apply to the named claim only:

  • unknown, the usual result: no receipt proves what happened. The key stays closed and retries are still refused. Check the provider for that idempotency key. If the claim came through a plane, the workspace owner then resolves it with POST /v1/resolve, where not_executed reopens it. A claim taken in-process by a self-hosted SDK is resolved through the attempt receipt its guard() wrote, which the settle names: anvil receipts resolve <receipt-id> safe_to_retry reopens the key, confirmed_succeeded keeps it closed. Either way the unknown is also written to the ledger, so it shows up with every other unknown outcome until someone resolves it.
  • executed: the ledger holds an execution receipt. The key stays closed.
  • failed: the ledger holds a failure receipt written for this attempt. The key reopens. A failure receipt from an earlier attempt is refused (evidence_predates_claim), because it says nothing about this one.

In practice a settle ends unknown or executed. Today's writers record a definitive failure as unknown with reason execution_failed, which the review shows but which is not proof, so the key stays closed until someone resolves it.

Before it asks, it prints the claim's owner, age, lease, evidence, the newest ledger rows for the key, what it will set, and who is running it. The settlement and one ledger record naming the operator (anvil.claim.settled, readable by the workspace owner) commit in a single transaction, so a settle nobody is named for cannot land.

  • Syntax: anvil db settle-claim <key> --reason "<why>" [--workspace <id>] [--operator <who>] [--dry-run] [--yes] [--json] · anvil db settle-claim --list [--json]
  • Key flags:
  • --list — stuck claims, oldest first, with what settling each would do. Changes nothing.
  • --workspace <id> — defaults to ws_default, where a self-hosted SDK writes its claims. It is never picked for you: a key pending only in another workspace is reported, not settled.
  • --reason "<why>" — required to write. Recorded with the settlement.
  • --operator <who> — defaults to your anvil login identity, then $USER. A machine account such as root is refused.
  • --dry-run — the full review, nothing written.
  • --yes, -y — skip the prompt. Required in CI / non-TTY / --json.
  • -- — ends the flags, for a key that starts with -.
  • ⚠ Refused, nothing written: a claim under an hour old (no flag lowers it); an owner still holding its lease (lease_live); a claim with no lease that is under 7 days old; an owner that renews, or a newer attempt that takes the key, while you review (raced, changed_since_review); a claim row another session holds (lock_busy); a database more than 150ms away, because settling holds the claim row for about one round trip.
  • ⚠ Could not look is never "nothing stuck": a database without migration 029 answers migration_029_missing ("cannot sweep here yet"), and one with no claim table answers claim_table_missing.
  • Reads ANVIL_POSTGRES_URL (or ANVIL_DATABASE_URL, PGURL, DATABASE_URL), like anvil db migrate. Without database access on Anvil Cloud, POST /v1/resolve accepts a pending claim directly.
  • Example:
    sh
      anvil db settle-claim --list
      anvil db settle-claim 'payment.charge:ord_1001' --reason "pod OOM mid-charge, ticket 4411"
      
  • Side effects: One transaction: the claim's status and one audit record. --list and --dry-run are read-only.
  • Exit codes: 0 settled, dry run or listed · 1 refused, nothing written · 2 could not read or write.

#anvil doctor

Health check for your Anvil install. Reports on Node version, Redis reachability, active policy, durability backend, stranded durable claims, control-plane key, idempotency proof, local audit sinks, and approval notifications — then an Account section (is this machine paired, as whom, and is its key still live), a Runtime section, and an Auto-instrumentation section (what Anvil is in the path of, and whether that runtime can write durable receipts). Ends with a Traffic Readiness verdict.

The sections most worth reading:

  • Account — not paired tells you hosted surfaces have nothing to talk to and to run anvil login. A key revoked server-side is called out as such, because every hosted call is refused until you pair again. If ANVIL_API_KEY is set, it says so and notes that it wins over any pairing.
  • Approval notifications — warns loudly when calls are held right now and nothing is configured to push them.
  • Auto-instrumentation — names each covered action as live via stripe@<version>, or gives the reason it isn't covered. Live means Anvil is in the path and inert: it costs nothing until that action has a mode.
  • Service key — self-hosted only. The plane stores a mode flip under its ANVIL_SERVICE (else the action's namespace); your app reads it under its own. If the two disagree, the flip shows on the dashboard and never reaches guard(). With ANVIL_CONTROL_URL set, doctor asks the plane for its key and fails (exit 1) naming both — plane writes "billing", this process reads "payment" — and a fix that depends on which side sets it. If only the plane does: set the same value where the app runs (the app then also takes that service's default and policy), or unset it on a plane that serves only this app and re-set its flips — the plane files every app's flips under one key. If the app sets its own: where the app runs, set its modes with anvil control set-action-mode <service> <action> <mode> --local — a flip sent through the plane is filed under the plane's key, even from that command without --local. Until the keys agree, the dashboard can neither set nor show this app's modes (it shows the plane's bucket) and this check stays red; to align them, give a plane that serves only this app the same value. Doctor never tells you to unset the app's ANVIL_SERVICE: that also drops its service default, policy and service allowlist. That compares this process's ANVIL_SERVICE, so run doctor where the app runs to judge the app. Without ANVIL_CONTROL_URL it reads the shared control store and warns (amber, never an exit code) about any override filed where this process won't look — stored under "billing", this process reads "payment" — since another deploy with that ANVIL_SERVICE may be its reader. It prints nothing when they agree. not checked means the plane or store could not be read, or the plane predates publishing its key; it is never a match. ANVIL_SERVICE_NAME plays no part. --json carries the verdict as serviceKey: null when there is nothing to compare, otherwise a status of match, mismatch, stranded or unchecked, with the actions and a one-line fix. A mismatch exits 1 under --json too — the one --json failure that does not need --ready-for-traffic.
  • Stranded durable claims — counts the claims still pending past their lease. Each one is a customer key whose retries are refused until someone resolves it, so this is money waiting on a decision. It renders amber, never red — a stranded row is an unknown outcome, not a broken deployment, and turning doctor red on one would push you to settle claims in a hurry. See them with anvil db settle-claim --list. not checked is not a zero: it means no durable store is configured, or one predating the claim-lease migration.
  • ⚠ Gotcha: On a fresh hosted install, Redis will show as unreachable or not configured — that's expected. anvil protect sets up the hosted API path; local Redis is only needed on self-hosted deployments running enforce with the default failMode=closed.
  • Syntax: anvil doctor [--deploy] [--ready-for-traffic] [--json]
  • Key flags:
  • --deploy — runs the hosted-topology preflight instead (checks DNS, API reachability, dashboard URL). Point it at a staging topology with --control-url, --api-url and --ghost-url, or the matching ANVIL_DEPLOY_{CONTROL,API,GHOST}_URL variables; without them it checks ours.
  • --ready-for-traffic — exit 1 if not ready. Perfect for pre-deploy gates.
  • --json — machine-readable output.
  • Example:
    sh
      anvil doctor --ready-for-traffic
      
  • Side effects: Read-only. Pings your configured Redis / Postgres / hosted API to check reachability.
  • Exit codes: 0 healthy, 1 critical issues found or --ready-for-traffic failed.

#anvil keys

See, retire and replace this workspace's API keys. Reach for this the moment you think a key has leaked — see the Leaked API Key Runbook in docs/OPERATIONS.md for the full incident path.

  • Syntax: anvil keys <list|revoke|rotate> [<prefix>] [--all] [--reason <why>] [--label <name>] [--yes] [--json]
  • Subcommands:
  • list — every live key: prefix, label, created, last used. The key your shell is authenticated with is flagged live·this. --all includes revoked keys, with the reason, the key that authorised each revocation, and the successor of a rotation.
  • revoke <prefix> — retire one key. Takes effect on the next call made with it — there is no cache to bust.
  • rotate [<prefix>] — mint a replacement and retire the old key atomically. Defaults to the key this shell is using. This is the safe move on a running service.
  • Key flags:
  • --reason <why> — recorded against the revocation, for the incident write-up.
  • --label <name> — name the key rotate mints (otherwise it inherits the old key's label).
  • --yes, -y — skip the confirmation prompt. Required in CI / non-TTY / --json; a destructive key operation is never auto-confirmed just because nobody is watching.
  • --json — envelope on stdout.
  • ⚠ Keys are addressed by PREFIX (anv_live_xxxxxxx), never by the secret. Anvil stores only a hash, so a rotated key is printed once and can never be shown again.
  • ⚠ The last live key cannot be revoked. Revoking it would lock the workspace out of its own guard API, so it is refused (409 last_active_key). Rotate instead.
  • ⚠ A revoked key fails CLOSED under enforce, regardless of ANVIL_FAIL_MODE / ANVIL_HOSTED_FAIL_MODE. The SDK raises AnvilCredentialRevokedError and the side effect does not run. Those fail-open knobs exist for outages; a revoked credential is not one, and honouring them would mean revoking a leaked key silently ran your money path unprotected. Ghost mode is the exception — it still executes (that is ghost's contract) but logs loudly that Anvil is now observing nothing.
  • Example:
    sh
      anvil keys list                                   # what can reach my money path right now?
      anvil keys rotate --yes                           # replace THIS shell's key, atomically
      anvil keys revoke anv_live_a3f9k2q --reason "in a CI log"
      anvil keys list --all --json | jq '.data.keys[] | select(.active | not)'
      
  • Every revoke/rotate is audited. It lands in the tenant's tamper-evident ledger as anvil.key.revoked / anvil.key.rotated, naming what was retired, which key authorised it, and why — on the same hash chain as guard decisions, so it cannot be removed or back-dated without anvil verify reporting a broken chain. Read it with anvil audit query --action anvil.key.revoked. This is what makes a leaked key's use of revoke undeniable: an attacker holding your key can retire your others, but not invisibly.
  • Side effects: revoke and rotate write to the hosted control plane and take effect immediately. rotate also refreshes the machine-local key cache (~/.anvil/workspace-keys.json) so this machine stops presenting the retired key — but you must still put the new key in your .env and redeploy.
  • Exit codes: 0 done, 1 refused or unreachable.

#anvil login

Pair this machine with your Anvil account. One browser round trip, and no email prompts ever again.

  • What happens. Anvil opens your browser to a consent page that names the account you are signed in as, and prints a code in your terminal. Check the code on the page matches, approve, and the pairing returns to your terminal — this machine gets its own key, labelled cli:<hostname>, which you can revoke on its own if the laptop walks away. It never rotates your other keys, and it never disconnects an editor.
  • If the codes don't match, don't approve. Cancel the page and run the command again. That comparison is the entire reason both sides show a code.
  • Syntax: anvil login [--paste] [--token <t>] [--switch] [--json]
  • Key flags:
  • --paste — no browser round trip. Anvil prints a link to open anywhere you're signed in; approve there and paste the code back. This is the SSH and container path.
  • --token <t> — the non-interactive form of --paste, for CI. (Space-separated, not --token=.)
  • --switch — pair a different account on a machine that's already paired. Anvil offers to retire the outgoing key; --revoke skips the question.
  • ⚠ ANVIL_API_KEY always wins. An environment key outranks any pairing, on purpose — that is the CI contract, and it means pairing your laptop can never change what a deployed service authenticates as.
  • Example:
    sh
      anvil login                 # the normal path
      anvil login --paste         # over SSH, or when no browser can open here
      anvil login --switch        # pair a different account
      
  • A pairing you never finish leaves nothing behind. The key is created when your machine collects it, not when you click Approve — so an abandoned tab, a closed terminal, or a blocked redirect writes no credential at all. There is nothing stranded and nothing to clean up; just run it again.
  • Side effects: Stores the machine's key in ~/.anvil/workspace-keys.json and makes it the active identity. Does not touch your project's .env — if a project already names a workspace there, that is still where your app reports.
  • Exit codes: 0 paired (or already paired), 1 refused, timed out, or no browser available without --paste.

#anvil logout

Unpair this machine. By default it asks whether to also revoke the key server-side.

  • Syntax: anvil logout [--revoke] [--keep-key] [--all] [--json]
  • --revoke retires the key too — the right call on a shared machine. --keep-key skips the question. --all also forgets every other account cached here; add --revoke to retire those keys as well.

#anvil open (alias: anvil dashboard)

Open the Anvil dashboard in your browser.

  • ⚠ It never starts a server. anvil open opens a local plane only when one is already serving; otherwise it opens your hosted dashboard and says why. (It used to boot one, which sent first-time users to an empty local plane they couldn't sign in to.) Start a local plane yourself with anvil serve.
  • Which dashboard you get, in order: ANVIL_DASHBOARD_URL if you set one · the account this machine is paired with · a local plane that is already running · the hosted dashboard. A paired machine goes to its own workspace even when a local plane is up, and says so.
  • ⚠ A machine that has never signed in goes to sign-in, even when a local plane is running. A local plane serves your policies; it holds no account and cannot create one, so opening it for someone with no account lands them on a dashboard that says "No account on this machine". anvil open names the running plane and the flag that opens it. Once you have signed in on this machine, anvil serve → anvil open behaves exactly as it always has.
  • First run on a new machine offers to pair it first — a short browser approval, capped at 45 seconds. It is advisory: decline it, or let it time out, and anvil open still opens your dashboard and still exits 0.
  • Syntax: anvil open [--print] [--no-browser] [--hosted] [--local] [--no-boot] [--json]
  • Key flags:
  • --print — print the URL instead of launching a browser. (--no-browser and --url do the same thing.)
  • --hosted — force the hosted URL even if a local plane is up.
  • --local — open a running local plane even though this machine isn't signed in. The self-hoster's flag; ANVIL_DASHBOARD_URL does the same thing permanently.
  • --no-boot — accepted for compatibility; anvil open no longer starts a plane in any case.
  • --json — emit {ok, command:"open", data:{url, opened:false, isHosted, portHeldByWrongServer, status, hostedReason}} on stdout and NEVER launch a browser (the JSON is the return value). hostedReason says why the hosted URL was chosen: "no-account" (a local plane is running, but this machine is signed in nowhere), "not-running" / "no-boot" (no local plane), or null.
  • Example:
    sh
      anvil open              # opens http://localhost:7446/app (or hosted, based on config)
      anvil open --print      # prints the URL — useful in SSH sessions
      anvil open --local      # the local plane, even on a machine with no Anvil account
      anvil open --json | jq -r .data.url   # scriptable URL fetch
      
  • Side effects: None on the server side — it never starts a plane. Shells out to open / xdg-open / start depending on OS.
  • Exit codes: 0 always — a URL is always resolvable. Reachability is reported in the output, not in the exit code.

#anvil approvals

Configure where Anvil pushes a notification the moment a call is held for a human.

  • A hold always tries to reach a person — check who. confirmation_gate holds a call and waits. With a webhook, Anvil POSTs it; with no webhook it emails the addresses your workspace has proven (a verified account email, or one linked by a Google sign-in), and if the webhook fails it falls back to that same email. With neither, the hold reaches nobody. anvil approvals webhook show and anvil doctor both print which of the three you are in — "Holds are emailed to h…@acme.com", or "Holds reach no one — verify your email in the dashboard, or set a webhook".
  • Syntax: anvil approvals webhook <set <url> | show | clear> [--secret <s>] [--json]
  • The signing secret is shown exactly once. Anvil generates one if you don't supply it, prints it at set, and never displays it again — show reveals only the first few characters. Requests carry x-anvil-signature, HMAC-SHA-256.
  • ⚠ Running set again rotates the secret, every time. There is no change-the-URL-keep-the-secret path, so re-point a webhook and re-deploy the receiver together.
  • ⚠ A secret shorter than 16 characters is discarded and replaced with a generated one, silently. If you supply your own, make it 16+ or your receiver will verify against a secret Anvil never used.
  • Example:
    sh
      anvil approvals webhook set https://hooks.example.com/anvil --secret "$(openssl rand -hex 24)"
      anvil approvals webhook show
      
  • Refusals are the server's own words. The URL must be a public HTTP(S) host — Anvil rejects private, loopback, and link-local addresses, and relays the reason verbatim. That screen runs again at delivery time against the resolved address, so a hostname that later points somewhere private fails closed and logs that no notification was sent.
  • Exit codes: 0 done, 1 refused or unreachable.

#anvil remove

Remove an action: off your dashboard, and the wrapper back out of your code.

  • ⚠ It asks the plane first. If the control plane refuses or can't be reached, your code is left completely untouched — Anvil will not unwrap a call it hasn't confirmed is no longer protected.
  • Syntax: anvil remove <action> [--reason "why"] [--dir <path>] [--dry-run] [--json] [--local-only]
  • Key flags: --dry-run writes nothing and prints what it would do · --dir <path> pins the project · --reason is recorded with the removal.
  • ⚠ --local-only skips the plane and only unwraps the code. It is the one way past the ask-the-plane-first rule above, so it will strip the wrapper off a call the plane is still enforcing — leaving a live action with nothing wrapped behind it. Reach for it when the plane is already clean and only the code is behind; never as a way around a refusal.
  • What "removed" means. A tombstone, not a delete: the action leaves your sidebar and stops being enforced, its receipts stay, and reviving it restores the mode it had before. Deleting the entry outright is the thing Anvil refuses to do — with no override, a still-wrapped call would fall back to the workspace default and start blocking live traffic.
  • ⚠ No --help. anvil remove --help reports an unknown flag; the usage line above is the reference.
  • Exit codes: 0 removed (or nothing to do), 1 refused, unreachable, or a file it couldn't cleanly unwrap.

#anvil sync

Tell your dashboard where this project's protected calls live. Paths and line numbers only, never your source.

  • When to run it. After you move code, rename files, or restructure a project — the dashboard's "where does this call live?" and the + Protect new search both read this map, and it goes stale silently otherwise. anvil protect publishes it too, so a fresh project is already mapped.
  • ⚠ It does nothing in CI. With CI set to anything but false, it prints Skipped in CI — a build machine must not overwrite your map. and exits 0. That is deliberate: a runner's checkout would replace a human's map with container paths and an "as of" that tracks the last pipeline. Run it from a developer machine — a green anvil sync in a deploy step has published nothing.
  • Syntax: anvil sync [--dir <path>] [--forget] [--json]
  • --forget deletes the stored map. The dashboard forgets where your code is; nothing else changes. ANVIL_NO_CODE_MAP=1 prevents it being sent in the first place.
  • ⚠ Which workspace it publishes to. The project's own .env outranks this machine's pairing, deliberately — a project belongs to whichever workspace its .env names, so a cached identity can never publish project A's map into workspace B.
  • Exit codes: 0 published (or nothing to publish), 1 refused.

#anvil scan

Find the riskiest calls in this project and offer to watch them in ghost. Reads your source on this machine; nothing leaves it unless you say yes.

  • What it is for. It answers the question anvil protect <action> assumes you can already answer. Someone meeting Anvil for the first time usually cannot name which of their own calls is dangerous, and this reads their code and names at most three.
  • At most three, or an honest none. It is not a linter and never becomes one: three rows, or one, or a plain sentence saying it is not sure — never a severity report. It exits 0 either way, because it is a suggestion and not a gate. Use anvil doctor or the wrap itself if you want something that can fail a build.
  • You pick which ones. The question is which, not whether: press enter to take all of them, type the row numbers (1 3) to take a subset, or n for none. Three rows are three independent judgements — recognising two of them should not force you to accept the third or throw away the two.
  • Nothing is blocked and nothing is written to your repo. No codemod, no config file, no .env edit. Saying yes registers each call in ghost, where Anvil records what it would have done and blocks nothing; only the action names, callees, categories and file paths are sent, never your source.
  • Syntax: anvil scan [--dir <path>] [--yes] [--json]
  • Key flags: --yes skips the question and takes all of them · --dir <path> scans a project other than the current directory · --json reports the rows and never prompts (add --yes to register under --json too).
  • ⚠ In a pipe or CI it registers nothing unless you pass --yes. It prints the rows and says so, rather than hanging on a prompt nobody can answer or failing the way a destructive command would.
  • If you have no workspace yet, it keeps the answer. The three are written to ~/.anvil before any network call, and the next anvil login or anvil sync registers them. It never asks you twice.
  • After it runs, add --import @useanvil/sdk/init to your start command; the instrument then sees the real calls as they happen, whatever your wrappers are named.
  • Bounded on purpose: 25,000 files or 15 seconds, whichever comes first. If it truncates it says so, and it will not print an exact file count for a partial read.
  • Exit codes: 0 always, except 1 for a --dir that does not exist.

#anvil protect

Wrap a risky call with Anvil. The marquee command — this is what a first-time user runs.

  • ⚠ Gotcha: On first run in a TTY it plays a spinning-anvil intro and offers two choices — wrap the call here, or wrap it and then walk through the dashboard tour. Both wrap it; they differ only in where you end up. Later runs skip the menu, and in CI, with ANVIL_NO_MOTION=1, or with --json, so does the intro.
  • ⚠ It asks for a work email on a machine that isn't paired, and creates a workspace under it. Pair first with anvil login if you already have an account, or set ANVIL_PROTECT_EMAIL for an unattended run — otherwise you can end up with a second workspace under a second address.
  • Syntax: anvil protect <action-name> [--call <exact.callee>] [--invite <code>] [--dir <path>] [--strict] [--json]
  • --call <exact.callee> names the call precisely when the action label doesn't match what's in your source — --call stripe.refunds.create. This is the fix for "no calls found."
  • Key flags:
  • --invite <code> — beta invite code, used when the hosted invite gate is on.
  • --dir <path> — pin the project to patch (bypasses auto-detection; required in --json when no project is unambiguously findable).
  • --strict — commit the patch, fail on a dirty tree, require a preexisting ANVIL_API_KEY.
  • --yes, -y — auto-confirm the single-project prompt. This is what CI needs beside --dir; without it the command waits on a prompt no runner can answer.
  • --key-expression <js> — supply the idempotency-key expression yourself instead of accepting the one the codemod derives. Reach for it when the call site carries no durable id and the derived key would fingerprint the whole payload.
  • --json — emit an envelope on stdout and NEVER prompt; the browser tab is not launched. See §4 for the full shape.
  • Example:
    sh
      anvil protect payment.charge
      anvil protect refund.issue --invite abc123
      anvil protect payment.charge --json --dir ./api | jq -r .data.dashboardUrl
    
      # unattended — the CLI's own non-interactive form
      CI=1 anvil protect payment.charge --dir . --yes --json
      
  • Side effects: Substantial. Runs a codemod that modifies source files (with a diff preview and confirmation). Writes ANVIL_API_KEY, ANVIL_API_BASE_URL, and ANVIL_MODE=ghost into .env. On success, auto-boots the local control plane and opens the dashboard in your browser. In --json mode the browser tab is not launched, the API-key provision is skipped (set ANVIL_API_KEY yourself), and the envelope reports what happened.
  • Exit codes: 0 success — and also 0 when no matching call was found in text mode, since nothing to do isn't a failure. 1 codemod failed / user cancelled / strict-mode refusal. In --json mode no_call_found exits 1 so jq -e .ok catches "action doesn't exist yet."

#anvil receipts

List, inspect, resolve, or reconcile execution receipts — the durable record of every enforce-mode call.

  • ⚠ Gotcha: resolve and reconcile mutate receipt state and are typically used by ops during incident recovery. list and inspect are read-only and safe to run anytime.
  • ⚠ These act on the local receipt store, chosen by ANVIL_POSTGRES_URL (else the local JSONL journal). They behave identically whether or not ANVIL_API_KEY is set — so they cannot close a hosted unknown, whose keys live server-side under your workspace namespace. The hosted exit is POST /v1/resolve. (anvil audit and anvil verify do switch to the hosted ledger on ANVIL_API_KEY; these don't.)
  • Syntax: anvil receipts <list|inspect|resolve|reconcile|reconcile-auto> [flags]
  • Key flags: --status executed|duplicate|conflict|blocked|unknown, --current-status <the same, plus recovered>, --action <a>, --service <s>, --limit <n>, --note <text> (on resolve/reconcile), --json
  • --status vs --current-status: --status is the immutable verdict recorded at call time. --current-status is the state after triage — identical for every outcome except unknown, which becomes recovered once you resolve it with safe_to_retry, confirmed_succeeded, or confirmed_failed. Resolving with manual_review_required deliberately leaves it unknown — it's still open. recovered is the only value the second flag has that the first doesn't, and the only reason to reach for it.
  • The resolution vocabularies differ, and both are validated (unlike --status):
  • resolve <receipt-id> <safe_to_retry|confirmed_succeeded|confirmed_failed|manual_review_required>
  • reconcile <receipt-id> <confirmed_applied|provider_mismatch|pending_verification|failed_after_attempt>
  • reconcile-auto closes what it can on its own, and it writes. Run --dry-run first, every time — a sweep over unknown is the one receipts invocation that can change many money outcomes at once. One receipt is reconcile-auto <receipt-id>; a sweep is reconcile-auto --status unknown --limit 50.
  • Example:
    sh
      anvil receipts list --status executed --limit 20 --json
      anvil receipts inspect rc_abc123
      
  • Side effects: list / inspect are read-only. resolve / reconcile write to the receipt store and append an audit record.
  • Exit codes: 0 success, 1 receipt not found or invalid state transition.

#anvil serve

Start the local Anvil control plane on 127.0.0.1:7446 in the background.

  • ⚠ Gotcha: This is a local development tool. It forces ANVIL_DEV_NO_AUTH=1 and binds to loopback — not the same as running Anvil self-hosted in production. For production self-hosting see Where Anvil Runs.
  • Syntax: anvil serve [--port N] [--json]
  • Key flags:
  • --port N (or -p N / --port=N) — override the default 7446.
  • --json — emit {ok, command:"serve", data:{url, port, alreadyRunning}} on stdout. alreadyRunning:true distinguishes idempotent re-runs from fresh boots.
  • Example:
    sh
      anvil serve
      # ✓ Anvil control plane started at http://127.0.0.1:7446
      #   Dashboard: http://127.0.0.1:7446/app · Open it: anvil open
    
      export ANVIL_URL=$(anvil serve --json | jq -r .data.url)
      
  • ⚠ A bare anvil serve walks ports. If 7446 is busy it moves to the next free one and reports where it landed. A pinned port — --port, POLICY_IMPORT_PORT, or PORT — is honoured exactly and fails rather than moving.
  • Side effects: Spawns the control-plane server as a detached background process. Writes to the workspace directory (~/.anvil/) and, if configured, your Postgres. Idempotent — re-running when already up is a no-op.
  • Exit codes: 0 server is up (either it was already or we started it), 1 couldn't start (script missing, health check timed out, or no free port in the walk range).

#anvil verify

Verify the tamper-evident HMAC hash chain of the audit ledger. Requires ANVIL_AUDIT_SIGNING_KEY (32+ characters) to have been set at the time each record was written; unsigned installs are a supported no-op so this command is safe to drop into a nightly CI job without wrapping it in a shell condition.

Which ledger it reads. With ANVIL_API_KEY set it verifies your workspace's hosted ledger over the API — the same records the dashboard shows. Otherwise it verifies the local JSONL log. --local or --log <path> forces the file. This matters because a hosted install's local log is empty by design, and verifying an empty file used to report a green check.

  • Syntax: anvil verify [--log <path>] [--all] [--local] [--json] [--strict]
  • Key flags:
  • --log <path> — verify a specific log file (default is $ANVIL_AUDIT_LOG). Implies --local.
  • --all — verify the current log plus any rotated files.
  • --local — verify the local JSONL log even on a hosted install.
  • --json — machine-readable result. Emits code, ok, strict, and exitCode alongside the break-location fields, so shell tools that pipe through jq never have to also inspect $?.
  • --strict — refuse to pass anything that was not completely verified: no signing key, no chained records, a bounded window of a longer chain, or no head anchor to rule out a removed tail. Use this in security-strict pipelines.
  • Example:
    sh
      anvil verify --all
      # ✓ Chain intact — 1,247 record(s) verified from genesis
      #   Head confirmed against the chain anchor — nothing was removed from the end.
      
  • Side effects: Read-only. Streams the log file(s) — or reads the hosted ledger — and re-computes the HMAC chain in memory.
  • Exit codes. Three outcomes, not two: 0 verified, 1 tampered, 2 could not be proven. The middle state is the point — a verifier that only had "pass" and "fail" had to round "the log is empty" to one of them, and it used to round it to green.
SituationcodeExit code
Chain intact and completeintact0
Record altered, or a record inserted/deleted mid-chainrecord_hash_mismatch / prev_hash_mismatch1
Records removed from the end (the head anchor names a record that is gone)truncated_tail1
Torn or corrupt linesmalformed_line1
No records at all — empty, absent, or emptied logno_records2
Chain does not reach genesis — earlier records missing (rotation, retention, or a stripped head)missing_origin2
No signing key setno_key0, or 2 with --strict — stderr carries ⚠ Chain verification skipped …
Records present but none signedno_chain0, or 2 with --strict
Intact, but only a window was read or no anchor was foundintact (+ partial / tailAnchor:"absent")0, or 2 with --strict

If your log rotates and you keep the rotated files, run anvil verify --all — verifying only the active file after a rotation legitimately reports missing_origin.


Advanced / internal-only commands — db, mcp, key, commands, init, install, counterparty, policy, prove, ghost, enforce alerts|weekly, rules, setup, init-agents, status, demo, start, audit incident — exist for specific workflows and live in their own guides. Run anvil --help for the full list.


#3. Environment variables

The env vars you'll actually set, grouped by when you'd touch them.

#Set by anvil protect — don't touch unless rotating

NameDefaultWhat it does
ANVIL_API_KEY(none — written by anvil protect)Authenticates your SDK to Anvil Cloud.
ANVIL_API_BASE_URLhttps://api.anvilcontrol.aiHosted API endpoint. Change only to point at a self-hosted instance.
ANVIL_PROTECT_EMAIL(unset)The work email anvil protect would otherwise prompt for on an unpaired machine — set it for an unattended first wrap. If you already have an account, anvil login is the better route.
ANVIL_MODEenforceClient-declared intent. anvil protect writes ANVIL_MODE=ghost into a new .env; when the variable is unset the SDK declares enforce. Governs fail-open vs fail-closed during an Anvil outage. Per-action mode is set from the dashboard and is the authority on the hosted transport.

#Tune to taste

NameDefaultWhat it does
ANVIL_FAIL_MODEclosedWhat happens on enforce when Anvil Cloud is unreachable: closed (throw), graceful (hold money-moving calls, pass through the rest), open (pass through with warning). An invalid value throws at startup rather than being coerced. ⚠ If this is unset and the legacy ANVIL_REDIS_UNAVAILABLE_BEHAVIOR=passthrough is set, the fail mode resolves to open.
ANVIL_GRACEFUL_MAX_UNCHECKED_CENTS0For failMode=graceful — the amount threshold below which a call passes through during an outage.
ANVIL_HOSTED_FAIL_MODEclosedThe same decision, made server-side by Anvil Cloud, for when the plane cannot evaluate your policy at all. Only open and graceful open the gate — every other value, including a misspelling or a stray space, silently means closed. Pairs with ANVIL_HOSTED_GRACEFUL_MAX_UNCHECKED_CENTS.
ANVIL_AUDIT_LOG./artifacts/anvil-audit.jsonlPath to the local JSONL audit sink.
ANVIL_AUDIT_DEAD_LETTER<ANVIL_AUDIT_LOG>.deadletter.jsonlWhere a decision the durable store refused is spooled instead of being dropped. anvil audit replay-spool reads it.
ANVIL_AUDIT_SIGNING_KEY(unset)32+ character secret that enables tamper-evident HMAC hash-chaining of the audit log.
ANVIL_AUDIT_TAMPER_EVIDENCE0Set to 1 to make ANVIL_AUDIT_SIGNING_KEY mandatory at startup (fail-fast if missing).
ANVIL_NO_MOTION(unset)Set to any value to skip the spinning-anvil intro and other CLI animations.
ANVIL_SERVICE(unset)Names this deployment's service for anvil control set-action-mode scoping.

#Advanced / self-host

NameDefaultWhat it does
ANVIL_POSTGRES_URL(unset)Postgres connection string for a self-hosted install. Set it and Anvil registers the durable adapter itself — receipts, the audit ledger and the durable dedup claim all become durable. Leave it unset and an enforce-mode money call is refused with blockedBy: setup_required; non-money actions enforce without it. Not needed on Anvil Cloud — that Postgres is ours.
ANVIL_REDIS_URL(unset — hosted path doesn't need it)Redis connection for local dedup coordination on self-hosted deployments.
POLICY_IMPORT_PORT7446Port the local control plane binds to. Also honored by PORT.
POLICY_IMPORT_HOST127.0.0.1 (dev) / 0.0.0.0 (prod)Host the control plane binds to.
ANVIL_CONTROL_URL(unset)Overrides the local control store with a remote control plane over HTTP.
ANVIL_CONTROL_TOKEN(unset)Bearer token required by a self-hosted control plane when its perimeter is on.
ANVIL_DASHBOARD_URL(unset)Overrides the URL anvil open opens — and the control plane anvil login pairs against, for a self-hosted deployment. A pin here beats a pairing.
ANVIL_APPROVAL_WEBHOOK_URL(unset)Self-hosted: where to POST when a call is held. On hosted, anvil approvals webhook set configures this per workspace instead.
ANVIL_NO_CODE_MAP(unset)Set to 1 and the CLI never publishes call-site paths and line numbers. anvil sync --forget deletes any already stored.
ANVIL_NO_INSTRUMENT(unset)Set to 1 and --import @useanvil/sdk/init installs no hooks at all — the way to rule instrumentation out of a problem without changing your start command.
ANVIL_GHOST_OBSERVATION_TIMEOUT_MS50Cap on how long ghost-mode observation can take before it's abandoned.
ANVIL_INVITE_CODES(unset)Comma-separated invite codes accepted by the hosted invite gate.
ANVIL_CONTROL_CACHE_TTL_MS3000How long an SDK process caches the control config before re-reading it. This is the second of the two caches a hosted mode flip has to clear; lower it to make flips land faster, at the cost of a control-store read per process per window.
ANVIL_HOSTED_POLICY_CACHE_TTL_MS5000Read by the plane, not by your SDK — how long an API replica caches a workspace's policy snapshot. It is the first of the two caches a mode flip clears, and the window anvil control reports back as applies_within_ms. On Anvil Cloud it is ours; set it only on a plane you run yourself.

#4. Scripting Anvil in CI

The one line every deploy pipeline should have:

yaml
- run: npx @useanvil/sdk doctor --ready-for-traffic

--ready-for-traffic exits 1 if any critical dependency is missing — perfect as a pre-deploy gate.

A few notes for automation:

  • Most commands support --json. Where it's scoped to particular subcommands, that command's entry says so. In --json mode:
  • stdout carries exactly one JSON object at completion, so anvil <cmd> --json | jq … always parses.
  • stderr carries every progress line, banner, and warning — pipe it away with 2>/dev/null if you don't need it.
  • Success envelope: { ok: true, command, message?, data? }.
  • Error envelope: { ok: false, command, error: { code, message, ... } } with code a stable machine-branchable slug (no_call_found, missing_action_arg, boot_failed, template_missing, …). Exit is nonzero.
  • Interactive prompts are suppressed — commands that would normally prompt (protect for project confirmation, API-key email) instead fail loudly with a specific error.code and a hint. Pass the equivalent flag (--dir, ANVIL_PROTECT_EMAIL=…) or drop --json for that one run.
  • Two shapes, and which one you get. Nine commands emit the envelope above: protect, open, serve, init, login, logout, sync, keys, db settle-claim. Branch on .ok and read .data.<field> for those. Every other --json command still emits its historical raw payload — arrays, top-level fields — so a jq consumer reaching in with .[0] or .checks[] keeps working. Both routes share one emitter, and a future major will move everything onto the envelope; until then, check this list before you write a parser.
  • anvil protect is idempotent. Running it a second time on the same action is a no-op, safe to include in setup scripts. In --json mode a repeat run reports { data: { alreadyWrapped: true, changed: false } } so scripts can distinguish first-run wraps from re-runs.
  • anvil verify is safe to drop into any CI step. By default an unsigned log is a green skip (exit 0, ⚠ on stderr), an intact chain is exit 0, a tampered one is exit 1, and a chain that could not be proven complete — an empty ledger, or one that does not reach its origin — is exit 2. Treat 2 as "investigate", not "fail the build", unless you have opted into --strict, which also turns "no signing key", "no chained records", and "verified only a window / with no head anchor" into exit 2. Because the three outcomes are distinct, a nightly gate can be as strict or as forgiving as you want without a shell condition: anvil verify --json | jq -e '.exitCode == 0'.

#--json per-command shapes (new commands)

CommandSuccess dataNotable error codes
protect{ action, wrapped: [{file, sites}], envPath, envWritten, mode, sdkVerified, resolves, dashboardUrl, rollbackScript, changed }missing_action_arg, invalid_action_name, no_call_found, no_project_found, ambiguous_project, confirm_project, strict_dirty_tree, sdk_install_failed, patch_stage_failed, patch_apply_failed, verification_failed, strict_commit_failed
open{ url, opened: false, isHosted, portHeldByWrongServer, reachable, status, identity, workspace, identityVerified, keyAgeDays, hostedReason }(no error path — always succeeds; a URL is always resolvable)
serve{ url, port, alreadyRunning }boot_failed, no_server_script
init{ envPath, wrote, skipped, force, editorRecommendation }template_missing
keys list{ keys: [{key_prefix, label, created_at, last_used_at, revoked_at, revoked_reason, revoked_by, rotated_to, active, current}], active_count }(errors throw — unexpected_error envelope with the server's own message)
keys revoke{ revoked: true, already_revoked, key }server codes: last_active_key, ambiguous_prefix, key_not_found
keys rotate{ api_key, key_prefix, label, revoked }server codes: ambiguous_prefix, key_not_found
scan{ rows: [{action, callee, category, file, line, reason}], files_scanned, truncated, configured, unread, sources, registered, ghost }dir_not_found
db settle-claim{ subcommand, outcome, database, claim, evidence, decision, keyReopened, ledger, operator, reason, attribution, next, recoveryId } (--list: { claims, truncated })refused: invalid_arguments, operator_required, claim_not_found, claim_not_pending, claim_too_young, lease_live, lease_missing_too_young, evidence_predates_claim, connection_too_slow, confirmation_required, cancelled, changed_since_review, raced, lock_busy · could not: no_database, migration_029_missing, claim_table_missing, store_error, attribution_failed, commit_unconfirmed

anvil open --json never launches a browser — the envelope is the return value, so a script consumer decides what to do with the URL.

Esc
↵ to open · ↑↓ to move · Esc to close