# Run402 CLI -- Agent Reference — Auth, billing, sign-in, doctor, logs > Slice `ops` of the Run402 CLI reference. Index (start here): https://docs.run402.com/llms-cli.txt · Whole reference: https://docs.run402.com/llms-cli-full.txt This slice serves customer people working within their own projects and organizations. Human sign-in, project administration and staff administration are distinct authorities. staff commands require separate platform credentials; they are not a repair shortcut for a customer permission denial. ### auth Manage project user authentication: passwordless email links/codes, trusted invites, passwords, passkeys, and auth settings. - `run402 auth magic-link --email [--delivery link|code|both] [--redirect ] [--intent signin|invite|claim|recovery] [--state ] [--project ]` — request passwordless email auth. Link is the wire default; redirect is required for link/both. JSON says `accepted:true`, preserves `message`/`warnings`, and returns opaque `challenge_id` for code/both without claiming delivery. - `run402 auth verify --token [--project ]` — exchange a magic-link token - `run402 auth verify --challenge-id --code [--project ]` — exchange an email code. Token and challenge/code modes are mutually exclusive; the code is secret and is never retried or persisted by the CLI. - `run402 auth create-user --email [--admin ] [--invite] [--redirect ] [--project ]` — service-key create/update auth user - `run402 auth invite-user --email --redirect [--admin ] [--project ]` — create/update user and send trusted invite - `run402 auth set-password --token --new [--current ]` — change, reset, or set password - `run402 auth settings [--allow-password-set ] [--preferred ] [--public-signup ] [--require-admin-passkey ] [--allowed-email-domains ] [--project ]` — update auth settings (requires service_key). `--allowed-email-domains` is a comma-separated list restricting hosted Google sign-in to those domains (enforced at token issuance); `none` clears it; empty = unrestricted. - `run402 auth passkey-register-options --token --app-origin [--project ]` — create WebAuthn registration options - `run402 auth passkey-register-verify --token --challenge --response [--label ] [--project ]` — verify and store passkey registration - `run402 auth passkey-login-options --app-origin [--email ] [--project ]` — create WebAuthn login options - `run402 auth passkey-login-verify --challenge --response [--project ]` — verify passkey login and return session tokens - `run402 auth passkeys --token [--project ]` — list authenticated user's passkeys - `run402 auth delete-passkey --token --id [--project ]` — delete one passkey - `run402 auth providers [--project ]` — list available auth providers - `run402 auth scaffold-roles [--table ] [--user-col ] [--role-col ] [--roles ] [--cache-ttl ]` — offline generator: emits a role-table migration + `requireRole` gate snippet + first-owner bootstrap (JSON out; no project/network). Pipe through `jq` (e.g. `| jq -r .migration`). Machine email-code flow: request `--delivery both` → retain `challenge_id` → read the code from email → verify the handle/code pair. The link in the same email remains a one-tap fallback. Credentials expire in 15 minutes and are single-use. `challenge_id` is public; never put the code, link token, cookie, or returned session tokens in logs/files. A request is accepted, not proven delivered. ### billing Email orgs + Stripe checkouts; pay by card or scale email beyond tier caps. - `run402 billing create-email ` — create an email-based organization (Stripe-only, no wallet). Sends a verification email. Idempotent. - `run402 billing link-wallet ` — link a wallet to an email organization for hybrid Stripe + x402 access. Response includes a `pool_implications` block (organization `tier`, `projects_in_pool_count`, `org_api_calls_current`, `org_storage_bytes_current`, `tier_limits`, `over_limit`) — inspect `over_limit` before linking a wallet whose usage might push the merged pool past the tier cap. - `run402 billing checkout --product tier --tier ` — start/renew/upgrade a tier lease via Stripe (hobby $5 / team $20; prototype is free on testnet — use `run402 tier set prototype`, not a Stripe charge). Returns a checkout URL. - `run402 billing checkout --product email-pack` — buy a $5 email pack (10,000 emails, never expire). Returns a Stripe checkout URL. - `run402 billing checkout --product balance-topup --amount ` — add allowance to the organization. Returns a Stripe checkout URL. - `run402 billing topup --sats [--wait] [--qr ] [--timeout ] [--idempotency-key ]` — top up the allowance over Lightning (`lightning-cash-topup`): mints a bolt11 invoice for `n` sats (100–1,000,000) through the platform's hosted wallet provider and prints it (`lightning:`, pay from any Lightning wallet). `--qr ` also writes the invoice as a scannable PNG (uppercase `LIGHTNING:` URI) for posting into a chat or showing on a screen. The dollar value is quoted at mint and credited as quoted, exactly once, when the payment lands (also up to an hour after the invoice expires — `paid_late`). `--wait` blocks until `paid`, `paid_late`, or `expired` (exit 2 on expired/timeout; a late payment still credits) and prints the receipt. No Stripe, no node; the gateway answers `LIGHTNING_TOPUP_NOT_CONFIGURED` (503) when no provider is wired — use `billing checkout` then. - `run402 billing auto-recharge [--threshold ]` — auto-repurchase $5 packs when the allowance drops below threshold. Requires saved Stripe payment method. - `run402 billing balance ` — `allowance_usd_micros` + email_credits_remaining + tier + lease + auto_recharge state (response includes `org_id`). A wallet/email is resolved to its organization via `GET /orgs/v1/lookup?wallet=|?email=`; an organization id (UUID) reads `GET /orgs/v1/:org_id/billing` directly. - `run402 billing history [--limit ]` — ledger history. Keyed by organization id: a wallet/email is resolved to its `org_id` first, then `GET /orgs/v1/:org_id/billing/history`. Auth: `balance`/`history` require SIWX from linked wallet or admin; wallet lookup requires matching SIWX (email lookup staff-only). `link-wallet` requires body wallet SIWX/admin; `checkout`/`auto-recharge` require linked wallet SIWX/admin. CLI signs from the local wallet, so that wallet must belong to the queried/linked/billed org. Non-member/guessed org id -> 403 no existence leak. `create-email` unauthenticated. Email packs only activate when the tier daily limit is exhausted AND the project has a ProjectDomain with verified custom email sending (spam protection for Run402-managed mail reputation). ### contracts Signers — an Ethereum wallet Run402 keeps for your project, for sending money and signing smart-contract transactions. Its key is created in and never leaves the platform's key custody. **Pricing: $0.04/day rental + $0.000005 per call**. Signer creation requires $1.20 in allowance (30 days of rent). Non-custodial — see https://run402.com/humans/terms.html#non-custodial-kms-wallets. - `run402 contracts provision-signer --chain [--recovery-address 0x...]` — provision a signer ($0.04/day wallet rent, requires $1.20 in allowance at creation). Prompts for confirmation if the project already has ≥1 active signer. - `run402 contracts get-signer ` — get signer metadata + live native balance + USD value (free) - `run402 contracts list-signers` — list all signers owned by the project, including deleted ones (free) - `run402 contracts set-recovery [--address 0x... | --clear]` — set/clear the optional recovery address used for auto-drain on day-90 deletion (free) - `run402 contracts transfers create --to 0x... --amount [--asset usdc] [--chain ] [--idempotency-key ]` — send a token from the signer with no gas on it: Run402 signs the EIP-3009 authorization with the signer's key and its facilitator settles it and pays the gas (one signing fee, no gas). `--idempotency-key` works as on `call`; a transfer that failed before settlement re-executes under its key, one that died in settlement never does; `status: "unknown"` is finished by the reconciler, poll it. - `run402 contracts transfers get --transfer ` — one transfer (status, tx hash, nonce, failure phase) - `run402 contracts transfers list ` — the signer's transfers, newest first - `run402 contracts set-alert --threshold-wei ` — set the low-balance alert threshold (free; alerts billed via existing email infra) - `run402 contracts call --to 0x... --abi --fn --args [--value-wei ] [--idempotency-key ]` — submit a contract write call (gas at-cost + $0.000005 signing fee). Returns `{ call_id, tx_hash, status }`. - `run402 contracts deploy --bytecode 0x... [--chain ] [--value-wei ] [--idempotency-key ]` — deploy a contract from the signer (gas at-cost + $0.000005 signing fee). `--bytecode` is the full creation calldata (creation bytecode + ABI-encoded constructor args, concatenated client-side via viem/ethers; ≤ 128 KB). Returns `{ call_id, tx_hash, status, contract_address }` — the deterministic CREATE address is known synchronously. run402 does NOT compile Solidity. - `run402 contracts read --chain --to 0x... --abi --fn --args ` — read-only contract call (free, no signing, no billing) - `run402 contracts status ` — get call status, gas used, gas cost USD-micros, receipt, error (free) - `run402 contracts drain --to 0x... --confirm` — drain native balance to a destination address (gas at-cost + $0.000005 signing fee). Works on suspended signers — the safety valve. - `run402 contracts delete --confirm` — schedule the signer's key for deletion (7-day window). Refused if balance ≥ dust — drain first. ### message - `run402 feedback send "" [--project ] [--handle ] [--return-address ]` — `--project` + `message: "promote: yes"` relays a deploy's promotion consent (see "Finish a deploy: hand your human two links" above); `--handle` is the human's X/Twitter handle; `--return-address` (an email, a handle, a URL; at most 256 characters) tells the Run402 team how to contact you back, since there is no inbox to read (≤64 chars), delivered as-is. ### agent - `run402 agent contact --name [--email ] [--webhook ]` - `run402 agent status` - `run402 agent verify-email` - `run402 agent passkey enroll` `contact` returns `email_verification_status`, `passkey_binding_status`, `assurance_level`. New/changed emails start reply challenge and remain `email_pending` until owner replies. `passkey enroll` requires `email_verified`, emails enrollment link, never prints token. ### notifications Owner notifications (audit log, preferences) plus the self-serve Telegram push channel and its routing rules. - `run402 deliveries list [--type ] [--since ] [--limit N] [--after ]` — paginated delivery-attempt audit log. - `run402 deliveries get ` — one audit row. - `run402 contacts preferences` / `preferences set = ...` — read/update cadence, per-class toggles, `webhook_url`, locale, timezone. Cross-wallet effects need `email_verified`; `webhook_url` changes need `operator_passkey`. - `run402 contacts test [--source app|platform] [--type ] [--severity ]` — fires a real synthetic event through the FULL pipeline (email/webhook AND Telegram routing rules), rate-limited to 1/min. Omit the flags for the default sample event (severity `notice`), or set them to exercise a specific rule's filters. The response's `telegram.destinations[]` reports one delivered/failed outcome per matched Telegram binding — empty when no rule matches (not an error) — and `telegram.skipped_rules[]` names each rule that did not send and why: `below_min_severity`, `deferred_to_window` (with `next_window_closes_at`), or `escalation_not_exercised` (a test never pages the ladder). **Telegram channel** (`notification-channel-routing-telegram`): - `run402 contacts connect telegram [--label ]` — creates a PENDING binding, prints `connect_url` (private chat) and `connect_group_url` (group chat) — single-use, 15-minute deep links — then POLLS `contacts list` until the binding flips to `active` or the code expires, printing progress to stderr. Requires `operator_passkey` assurance AND a VERIFIED owner email (bindings are addressed to it). Until the platform's dedicated bot is provisioned on this gateway, this returns `503 TELEGRAM_CHANNEL_NOT_CONFIGURED` with a `next_actions` entry — printed verbatim. - `run402 contacts list` — every channel (email, webhook, and every live Telegram binding) for the authenticated wallet. - `run402 contacts rm ` — requires passkey assurance (`operator_passkey`); a missing/already-revoked/foreign binding id all return the same not-found error (no existence oracle). **Routing rules** — one rule routes to exactly ONE Telegram binding; every match dimension you set (`--project`, `--source`, `--type`, `--class`) is ANDed, and an OMITTED dimension is a wildcard. `--type`/`--class` accept comma-separated lists (matches ANY listed value). **No rules = no Telegram traffic** — Telegram is opt-in per event, per rule; the mandatory email floor (security/recovery/billing_critical/destructive_lifecycle/verification classes) is completely untouched by rules and can never be silenced by one. - `run402 subscriptions add --binding [--project ] [--source app|platform] [--type a,b] [--class a,b] [--min-severity info|notice|warn|critical] [--group-by a,b] [--window ]` — requires passkey assurance (`operator_passkey`); an unusable/foreign `telegram_binding_id` returns the same 404 as a nonexistent one. - `--min-severity` filters on the severity the emitter declared. Without it, a rule that names no `--type` carries `notice` and above (routine traffic stays in the feed) and a rule that names types carries everything of those types. - `--window ` (60-86400) sends ONE message per closed wall-clock window instead of one per event: the total, a line per `--group-by` value with its count (ten, then a summary), three samples, and a feed pointer. `--group-by` needs `--window`; a rule keeps at most 200 group values per window and counts the rest under `(other)`. The feed still holds every event. - `run402 subscriptions add --escalate-to-level --project [--min-severity critical] [...filters]` — instead of a chat, a matching event raises on the organization's escalation ladder (mandatory pages, one-tap ack, deadman climb); a repeat while it is open joins it (`attached_count`) instead of paging again. Needs owner authority on the project's organization (`403 OWNER_REQUIRED`). Bounds: 20 raises per rule per UTC day; refusals are counted on the rule (`escalation_refused_count`). - There is no count or rate condition: the rule filters on severity and reduces with a window, and a field the rule shape does not have is refused (`400 INVALID_RULE_FIELD`). - `run402 subscriptions list` — the person's routing rules. - `run402 subscriptions rm ` — requires passkey assurance (`operator_passkey`). Full rule-model explanation: `run402 contacts --help` / `run402 subscriptions --help`. ### buzz notifications — route project events into a Buzz channel Push selected project events into a Buzz community channel as signed NIP-29 messages. A route is an explicit, owner-declared destination: one ACTIVE community installation, 1–50 named projects (future or transferred-away projects never join automatically), reviewed event filters, one channel. The workflow is **configure → authorize → test → live**, and every command here has zero spend impact. - `run402 buzz notifications scope --org | --listed [--project ...]` — switch the route between the whole organization (`project_scope: org`: every project it owns, present and future, read fresh per event so a transferred project drops out; no list to maintain) and an explicit list. The stored list survives a switch to `--org`; `--listed` on a route with nothing stored needs `--project`. `configure --all-projects` creates a route org-wide from the start. - `run402 buzz notifications projects --add | --remove ` — widen or narrow a listed route's explicit project scope (a PATCH at the route's current revision). A listed route names its projects: a project an agent provisions later is not routed, and its deploys, crashes, and pages do not reach the channel, until it is added — or the route is switched to `--org`. Only events occurring from then on are delivered; nothing is replayed. - `run402 buzz notifications on-call --agent [--name ] | --clear` — set or clear the Buzz agent this route pages. A crash (`error_fingerprints_observed`) or `platform_incident` on a routed project is posted by the project bot as `@ please investigate: …` with a `p` mention of that agent: the `@name` text is what Buzz renders as a mention chip, the tag is what wakes a managed Buzz agent. `--name` supplies the name; without it the gateway reads the agent's Buzz profile (best effort — a silent relay leaves the page tag-only). The failing release's deployer is addressed instead when it holds a public Buzz identity link. `--clear` empties it (the route pages nobody). The command reads the route's current revision and PATCHes at it, then prints the one pubkey the agent must allow: a page is signed by the installation identity (not the project bot, which keeps deploy credits and receipts), and a managed Buzz agent answers only its owner by default, so add that pubkey to the agent's respond-to allowlist (or set respond-to to `anyone`) and restart it. One entry covers every routed project. - `run402 buzz notifications configure --org --installation --name --channel --project [--project ...] [--event-type ...] [--event-class ...] [--include-org-events] [--on-call [--on-call-name ]]` — create the route. `--on-call` names the agent the route pages from the start, `--on-call-name` the `@name` it is addressed by (see `on-call` above). `--include-org-events` also delivers the organization's own events into the channel — the `platform_payment_received` receipt after a Lightning top-up or tier purchase — on top of the named projects' events; without it a route is strictly project-scoped. The response's `authorization` block is either `authorized` (live now) or `pending_buzz_authorization` with the ONE exact, non-secret step, printed loudly to stderr: a Buzz community owner or admin adds the `notification_pubkey` as a relay member (the approving human's own Buzz key lives in Buzz Desktop → Settings → Profile → Identity). Only three reviewed event types are routable (`deploy_activated`, `error_fingerprints_observed`, `platform_incident`); the classes `security` / `billing_critical` / `destructive_lifecycle` / `verification` / `recovery` may NEVER be routed. Omitting a filter routes everything registered; an explicit empty filter is rejected — never a wildcard, never a silent match-nothing. - `run402 buzz notifications status [--org | ]` — the org's routes, or one route's honest `health` (derived from route + credential state, never queue emptiness) with per-status delivery counts and the `revision` an update must echo. - `run402 buzz notifications test [--wait]` — re-checks the Buzz-side membership (activating a `pending_authorization` route when the member-add landed — the test doubles as the authorization poll) and queues ONE signed test message (`202`, queued-not-delivered). `--wait` polls until it settles; on timeout it prints the still-queued delivery and exits 2 — the publisher tick runs ~every 60s, so silence is cadence, not failure. - `run402 buzz notifications deliveries [--limit ] [--cursor ] [--delivery ]` — keyset newest-first history, dead letters included, the signed envelope never. Retries back off 1m/5m/30m/2h/12h to 8 attempts or 48h, then `dead_letter`; retried publishes are byte-identical, so the relay converges on one Nostr event id. - `run402 buzz notifications pause|resume ` — pause stops matching NEW events (nothing is retroactively delivered on resume); resume re-arms and resets the hard-failure counter (ten consecutive hard failures auto-pause with `pause_reason: delivery_failures` and fire the mandatory `buzz_route_auto_paused` owner notification). - `run402 buzz notifications rotate ` — stages the NEXT signing generation; the current key keeps signing until the next pubkey's own Buzz-side membership verifies. No command in this group ever accepts or prints a signing secret — `notification_pubkey` + `signing_generation` are the only credential material on the wire. - `run402 buzz notifications revoke ` — cancel queued deliveries and keep sanitized history readable; the installation's notification credential is destroyed only when this was its LAST live route. Routes deliver NEW events only (the outbox floor is captured at creation), and Buzz is never a deadman channel: mandatory owner notifications keep their human paths (email, Telegram) regardless of route state, and a Buzz delivery acknowledges nothing. Auth: org owner + fresh step-up (a SIWX wallet is inherently fresh); every mutation is idempotent (`--idempotency-key`, auto-generated when omitted). ### webhook-secret - `run402 webhook-secret rotate` — new HMAC signing secret for the notification webhook, returned EXACTLY once (store it immediately). Previous secret remains valid for 24h (dual-secret grace window). Requires passkey assurance (`operator_passkey`; a `run402 login` session satisfies it). ### sign-in (login, logout, whoami, approve) A person signs in; an agent's wallet is a separate identity (`run402 status` shows the wallet's own organization state). There is one **sign-in session**, bound to the person's principal and graded by how it was minted: `loopback` (`run402 login`, full and step-up-able), `device` (`run402 login --device`, read-only), `browser` (the console). A device-grade session reads everything a member may read; every write answers `403 SESSION_READ_ONLY` naming `run402 login`. **Wallet-less writes (write approval):** wallet agents use SIWX. A person without a wallet uses the sign-in session; provisioning (`org.project.create`), deploying (`project.deploy`), and secret writes (`project.secret.write`) also need a passkey-signed **write approval** for one `(action, target)`, sent as `X-Run402-Write-Approval`. Missing or mismatched approval → `WRITE_APPROVAL_REQUIRED` / `WRITE_APPROVAL_SCOPE_MISMATCH` / `WRITE_APPROVAL_BINDING_MISMATCH` (403) or `WRITE_APPROVAL_SESSION_INVALID` (401); the SDK raises `WriteApprovalRequiredError` (`isWriteApprovalRequired()`) with the exact `run402 approve …` command and an `approve_write` next action. Approval is never ambient for MCP/CI/non-TTY; only an interactive TTY auto-opens the browser and retries once. A write approval never satisfies step-up. - `run402 login [--no-open]` — loopback-PKCE (RFC 8252) sign-in. Starts a `127.0.0.1` listener, opens the browser (TTY) for the passkey ceremony, exchanges the code at `POST /agent/v1/control-plane/cli/token`, and caches the `loopback`-grade session at `{base}/control-plane-session.json` (0600; the token is never printed). Signing in again is the step-up for `STEP_UP_REQUIRED`. Stdout `{ logged_in, grade, read_only, principal_id, amr, expires_at, expires_in_seconds, memberships? }` — `memberships[]` includes invites auto-redeemed at sign-in when the lookup succeeds. Browser sign-in flows are SDK/console-side (`r.session.*`). - `run402 login --device [--no-open]` — RFC 8628 device authorization for a terminal with no browser: `POST /agent/v1/control-plane/cli/device` prints a URL and user code to stderr; approve it in the console (which needs a sign-in within the last 5 minutes, else `RECENT_SIGN_IN_REQUIRED`); the CLI polls `…/cli/device/token` and caches a `device`-grade (read-only) session. Errors: `LOGIN_DENIED`, `LOGIN_EXPIRED`, `LOGIN_TIMEOUT`. - `run402 whoami` — `GET /agent/v1/whoami`: the principal, memberships, and `session` (`{ grade, amr, amr_times }`, or `null` when the active wallet reads), plus `write_approvals[]` cached on this machine (`action`, `org_id`, `project_id`, `expires_at`; never the token). With a live cached session it reads as that session; a session the gateway no longer honours is cleared and answers `SESSION_INVALID`. - `run402 logout` — revokes the session (`POST /agent/v1/control-plane/session/revoke`), then deletes it and every cached write approval. Idempotent; the local cache is cleared even when the revoke fails. Stdout `{ revoked, cleared }`. - `run402 approve --action (--org | --project ) [--no-open]` — mint a write approval for one `(action, target)`: `org.project.create` + `--org`, `project.deploy` + `--project`, `project.secret.write` + `--project`. Requires `run402 login` (a device-grade session cannot approve: `SESSION_READ_ONLY`). Opens `POST /agent/v1/control-plane/write-approval/challenges`, the passkey confirm page redirects to the loopback, and `…/write-approval/cli/token` mints the token, cached at `{base}/write-approvals.json` (0600) bound to `(api_origin, sign-in session, action, target)`; 30 minutes idle, 4 hours at most. Approvals for different targets coexist; `login` and `logout` clear them. `projects provision` and `deploy` run it for you on an interactive TTY. Errors: `LOGIN_REQUIRED`, `APPROVE_FAILED`, `APPROVE_BAD_CONFIRM_URL`. - `run402 orgs list` — your orgs joined with the account overview (`GET /agent/v1/me/overview`: tier, lifecycle, lease, quotas, allowance, wallets, advisories); see the orgs slice. The session is principal-scoped and cached once at the base config dir, independent of `--wallet`. No MCP login by design: an MCP host holds no browser, and a person's session must not become the agent's ambient authority; MCP `whoami` reports the grade. ### service Public service-level status. No wallet, no auth, no keystore required — works on a fresh install. Reports on the Run402 service (uptime, capabilities, operator); for your organization state use `run402 status`. - `run402 service status` — public availability report (24h/7d/30d uptime per capability, operator, deployment, schema `run402-status-v1`) - `run402 service health` — liveness check with per-dependency results (postgres, postgrest, s3, cloudfront) and service version ### cache SSR origin-cache inspection + invalidation. Capability `ssr-isr-cache` (paired with `@run402/astro` v1.0+). Cache is scoped per-project; cross-project hosts return `R402_CACHE_INVALIDATION_HOST_FORBIDDEN`. - `run402 cache inspect [--locale ] [--release-id ]` — read the cache row state for a URL. Stdout is JSON: `{ status: "HIT" | "MISS", host, path, locale, releaseId, cachedAt, expiresAt, writtenUnderGeneration, contentSha256, headers }`. `status` is NEVER `BYPASS` — inspect does not issue a request, so it cannot evaluate runtime bypass conditions. Defaults to active release + default locale; pass `--locale` / `--release-id` to inspect non-default rows. (The legacy `--json` flag is removed — JSON is the default.) - `run402 cache invalidate ` — invalidate a single absolute URL. Stdout: `{ deleted, host, path, generation }`. - `run402 cache invalidate --prefix

--host ` — invalidate all rows under a path prefix on a specific host. - `run402 cache invalidate --all --host ` — entire-host purge (catastrophic content changes; nav restructure; layout-wide updates). Every invalidate returns `{ deleted, generation, host, path? }`. `generation` is the post-increment per-(project, host) counter — it gates in-flight MISS renders from overwriting after invalidation completed. ### doctor Health + config diagnostics. Agent-DX entrypoint — agents run this first to verify environment before attempting anything else. - `run402 doctor [--verbose] [--refresh] [--no-scan] [--dir ] [--manifest ] [--scan-dir ] [--project ] [--only ...]` — checks: config dir presence, installed CLI update state (`cli_update`: `latest_known`, `checked_at`, `comparison`, and `install_confidence` (confidence in detecting the installation method, explicitly labeled by `install_confidence_basis: "installation_method_detection"`; independent of whether registry versions match); confidence concerns install detection, not version truth), local wallet (`wallet`) + rail, keystore wallet count, API base reachability, active tier + lifecycle state, notification health snapshot (binding state + per-attempt verification failure detail), **recovery posture** (per vault-owning org: human-owner login + member source-key backup evidence), **vault**, **source scan** (auth-aware-ssr). the `cli_update` cache has a real 24h TTL (kychee-com/run402#561): a plain `doctor` call reuses a cache within that window only for the same installed version, and refreshes a MISSING or EXPIRED cache or one recorded before an installation change with exactly one bounded live check automatically — `--refresh` simply forces that same check even when the cache is still fresh. A failed live check (offline) falls back to the last known-good value rather than reporting a bare unknown, and it is labeled as an estimate, not presented as current: `cli_update.value.cache` always carries `fresh`, `freshness_basis: "observation_age"`, `age_ms`, `refresh_attempted`, `refresh_failed`, and `last_attempt_at`, and a stale fallback's `hint` names the estimate's age and the failure reason. An installed version ahead of the observation has status `unknown`, with no downgrade advice; `--refresh` can retry the registry. A within-TTL observation is not proof that an ahead-of-observation installation is current. Stdout is `{ ok, blocking[], warnings[], checks[] }`. `ok` answers one question — can this agent ship from here — and is `true` exactly when `blocking[]` is empty; advisory findings never affect it or the exit code (0 when `ok`, 1 otherwise), so an unbound passkey or a degraded recovery posture on another org is reported in `warnings[]` while `ok` stays `true`. Every check carries `severity: "blocking" | "advisory" | "info"` (blocking: config dir or wallet missing/error, API unreachable, tier inactive/frozen/past_due/dormant/missing/error, error-severity source-scan findings; advisory: `account_health`, `recovery_posture`, `vault`, `runtime_staleness`, `retiring_routes`, `cli_update` gaps; info: ok/skipped/unknown). `warnings[]` is `[{ check, code?, message, hint? }]`, one entry per gap; `blocking[]` is `[{ check, status, message, hint? }]`; `checks[]` is `[{ name, status, severity, value?, hint?, message? }]`. Agents branch on `ok` and read `warnings[]` for non-blocking gaps. The `tier` check's `status` is a fixed vocabulary (`ok | inactive | frozen | past_due | dormant | purged | missing | unknown | error`, never a tier name) with the tier name and raw lifecycle in `value.tier` / `value.lifecycle`; a wallet whose own organization holds no tier but can reach projects on another organization reports `missing` as advisory (`TIER_MISSING_ON_OWN_ORG`), not blocking. `--only` and `--refresh` produce the same shape. When stale, `cli_update.value.next_actions[]` includes an `upgrade_client` action with `command`, `argv`, install context, confidence, `cwd`, and mutation flags. When owner email verification is `pending`, the doctor surfaces the per-reason hint from `email_verification.last_challenge.hint` along with `attempt_count` / `remaining_attempts` so the person sees what to fix. When the gateway reports `reachability.reachable: false` on `GET /agent/v1/me/status` (recovery-event-reachability — the org resolves to zero verified notification recipients across both the wallet-contact chain and org-membership verified emails), the notification-health check warns that mandatory recovery/security notifications currently reach nobody, including how many were already skipped in the last 90 days, with the `run402 agent contact --email ...` remedy. The **`recovery_posture`** check (vault-recovery-custody; rides the same status read) renders one entry per vault-owning org: `control_plane_configured: false` warns "no human owner with a working control-plane login" with the `run402 orgs invite create --email … --role owner` remedy; `source_backup_configured: false` warns "no human member holds a working source-access key" pointing at console.run402.com/account source enrollment; `custody_legacy_present: true` warns that a member key is still single-credential legacy custody (one passkey, no recovery code). These are EVIDENCE levels — "configured" is what the platform verified, never proof an off-platform passkey or saved code still exists — and the same events arrive as `org_recovery_posture_degraded` / `org_recovery_posture_recovered` feed events. A gateway without the block reports `skipped`, and an account with no vault-owning org reports `ok` with an empty `orgs` list. The source scan selects only the application at `--dir`/cwd or the explicit `--manifest`; without a selected manifest it reports unscoped/skipped. It scans the selected app’s `src` and explicit referenced source files, excludes sibling apps and nested repositories, and shares its gate with SDK `up` and `deploy`. It flags hallucinated SDK auth names (`R402_AUTH_UNKNOWN_EXPORT`), state-changing GET handlers (`R402_AUTH_STATE_CHANGING_GET`), `auth.*` calls in `export const prerender = true` pages (`R402_AUTH_PRERENDERED`), and direct mutation of `internal.sessions.authz_version` (`R402_AUTH_AUTHZ_VERSION_PROHIBITED`), and warns when `adminDb()` (the service key, bypassing RLS) appears in a file that reads the current user (`SERVICE_KEY_IN_USER_REQUEST`; use `db(req)`, or silence with `// run402-allow-admin-db: `). The **`retiring_routes`** check (same status read, `retiring_routes[]`) lists each of your projects' calls to a route being retired in the last 7 days, with the fix: `run402 functions rebuild --all --project ` for deployed functions, `npm i -g run402@latest` for an old CLI or SDK, or the successor URL for your own code; a gateway without the block reports `skipped`. `--no-scan` skips the scan (config-only checks). `--scan-dir ` is an advanced arbitrary scan and reports advisory findings without claiming application deploy refusal. `--project ` (kychee-com/run402#566) targets the vault check at that project, outranking the repo-standing pin/remote/env/active-project default — scoped to the vault check only, since every other check is wallet/machine-wide. Any flag doctor does not recognize is rejected (`UNKNOWN_FLAG`/`BAD_USAGE`), never silently ignored. `run402 deploy` runs the scan as pre-flight and refuses to deploy on any `error`-severity finding (bypass with `RUN402_DEPLOY_SKIP_SCAN=1`). (The legacy `--json` flag is removed — JSON is the default.) **`--only ` (repeatable, kychee-com/run402#566 remaining half).** Runs ONLY the named check(s) and suppresses every other one — INCLUDING the source-tree scan, which on a monorepo can produce thousands of findings that bury a single check's diagnosis (the original dogfood complaint). A skipped check's work never runs at all: `run402 doctor --only vault` costs one vault read, not the full config/tier/account/scan sweep. Composes with `--project`, which still scopes only the vault check. The valid check names are `config_dir`, `cli_update`, `wallet`, `projects`, `api_reachable`, `tier`, `account_health`, `runtime_staleness`, `retiring_routes`, `recovery_posture`, `vault`, `source_scan` (`run402 doctor --help` lists them); an unknown name is `BAD_USAGE` listing the valid set. `--only` is not used together with `--buzz` — buzz mode is its own separate, always-complete check set, and the combination is rejected rather than silently ignoring one of the two flags. ```bash run402 doctor --only vault # just the vault diagnosis — no source scan, no monorepo noise run402 doctor --only vault --project prj_1a2b3c run402 doctor --only config_dir --only wallet --no-scan ``` ### dev Astro dev wrapper. Loads `.env.local`, verifies `RUN402_PROJECT_ID` + `RUN402_SERVICE_KEY`, then spawns `astro dev` with the env inherited. - `run402 dev [--port ] [--host ] [--project ]` SDK calls (`db`, `auth.user`, `cache.invalidate`, `assets.put`) hit the LIVE Run402 project at `https://api.run402.com` — no local database, storage, or key-custody setup required. This is shape-parity with production. Offline emulator mode is deferred to v1.5. ### init astro Project scaffolder. Subroute of `init` (alongside `init` rail setup). - `run402 init astro [

] [--force]` — creates a deployable Astro project with `package.json` (dev/deploy scripts), `astro.config.mjs` (one-line `@run402/astro` preset), `src/pages/{index,[slug]}.astro` (the latter demonstrates the full DB-backed dynamic page pattern with cache directive), `src/layouts/Layout.astro` (title + og + canonical), `src/pages/api/save-page.ts` (admin save endpoint with `db().upsert() → cache.invalidate()`), `.env.example`, `.gitignore`. Refuses non-empty dirs without `--force`. Stdout is JSON `{ dir, files_created, created, next_steps }`; progress lines go to stderr. (The legacy `--json` flag is removed — JSON is the default.) ### logs Fetch function logs by request id. Top-level shortcut — for fine-grained per-function control, `run402 functions logs ` still works. - `run402 logs --request-id [ | --function ] [--project ] [--tail ] [--since ] [--app|--platform|--all]` — fetch the log lines correlated to one request across every function in the project. Reads with the project's cached service key, or with your wallet / session / grant key (`project.read`) when none is cached, so an org member or teammate agent can follow an error fingerprint's logs drill-down without a key. The id is the `x-run402-request-id` header on every function response (`req_…`); `fnrun_…` / `fnatt_…` durable-run ids work too. No function name is needed: the SDK fans the read out to each deployed function and merges the matches oldest-first, each entry tagged with its `function` and `origin`. By default only the function's own output is shown (`--app`); Lambda's runtime lines (`INIT_START`, `START`/`END`/`REPORT RequestId`, billed duration) are `origin: "platform"` and hidden — `hidden.platform` counts them and a `hint` appears when hiding them left nothing, `--platform` shows only them, `--all` shows the raw stream. `--function ` (or a leading positional) narrows the scan; `--function` alone is that function's recent tail. `--tail` (default 100, max 1000) bounds the read per function before the filter. Unknown flags are rejected. Stdout is JSON `{ ok, request_id, project_id, scanned, entries, errors?, origin, hidden?, hint? }`; `ok` is `false` only when a function's read failed (named in `errors[]` with its `code`). (The legacy `--json` flag is removed — JSON is the default.) When an SSR response returns a 5xx, the response headers include `x-run402-error-code: R402_SSR_RUNTIME_ERROR` and `x-run402-request-id: req_...`. Copy the request id and run `run402 logs --request-id req_...` to fetch the full stack trace. Scans every function in the project in parallel and aggregates entries timestamp-ascending unless `--function` narrows the scope. --- ## npm installation script warnings Some npm configurations emit an `install-scripts` warning that `esbuild` is not covered by `allowScripts`. Run402 bundles `tsx` to load TypeScript deploy configurations, and `tsx` depends on `esbuild`, whose postinstall sets up its platform binary. A warning alone does not establish that installation failed: inspect the install exit code and the postinstall result. In the reported npm 11.19.0 case, postinstall exited 0 and the CLI worked. A blocked or failed script is a different condition and may affect TypeScript configuration loading. Keep the user's script policy intact; do not automatically enable scripts globally. Optional `fsevents` failures are separate from this warning.