Skip to content

Auth, billing, operator, doctor, logs

This slice serves customer operators working within their own projects and organizations. Human operator login, project administration and platform administration are distinct authorities. Platform-admin commands require separate platform credentials; they are not a repair shortcut for a customer permission denial.

Manage project user authentication: passwordless email links/codes, trusted invites, passwords, passkeys, and auth settings.

  • run402 auth magic-link --email <addr> [--delivery link|code|both] [--redirect <url>] [--intent signin|invite|claim|recovery] [--state <value>] [--project <id>] — 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 <token> [--project <id>] — exchange a magic-link token
  • run402 auth verify --challenge-id <opaque-id> --code <six-digits> [--project <id>] — 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 <addr> [--admin <true|false>] [--invite] [--redirect <url>] [--project <id>] — service-key create/update auth user
  • run402 auth invite-user --email <addr> --redirect <url> [--admin <true|false>] [--project <id>] — create/update user and send trusted invite
  • run402 auth set-password --token <bearer> --new <password> [--current <password>] — change, reset, or set password
  • run402 auth settings [--allow-password-set <true|false>] [--preferred <method|null>] [--public-signup <policy>] [--require-admin-passkey <true|false>] [--allowed-email-domains <csv|none>] [--project <id>] — 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 <bearer> --app-origin <origin> [--project <id>] — create WebAuthn registration options
  • run402 auth passkey-register-verify --token <bearer> --challenge <id> --response <json> [--label <text>] [--project <id>] — verify and store passkey registration
  • run402 auth passkey-login-options --app-origin <origin> [--email <addr>] [--project <id>] — create WebAuthn login options
  • run402 auth passkey-login-verify --challenge <id> --response <json> [--project <id>] — verify passkey login and return session tokens
  • run402 auth passkeys --token <bearer> [--project <id>] — list authenticated user’s passkeys
  • run402 auth delete-passkey --token <bearer> --id <passkey_id> [--project <id>] — delete one passkey
  • run402 auth providers [--project <id>] — list available auth providers
  • run402 auth scaffold-roles [--table <name>] [--user-col <col>] [--role-col <col>] [--roles <csv>] [--cache-ttl <secs>] — offline generator: emits a role-table migration + requireRole gate snippet + first-operator 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.

Email orgs + Stripe checkouts; pay by card or scale email beyond tier caps.

  • run402 billing create-email <email> — create an email-based organization (Stripe-only, no wallet). Sends a verification email. Idempotent.
  • run402 billing link-wallet <org_id> <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, organization_api_calls_current, organization_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 <org-id | wallet | email> --product tier --tier <prototype|hobby|team> — subscribe/renew/upgrade a tier 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 <org-id | wallet | email> --product email-pack — buy a $5 email pack (10,000 emails, never expire). Returns a Stripe checkout URL.
  • run402 billing checkout <org-id | wallet | email> --product balance-topup --amount <usd_micros> — add cash credit to the organization. Returns a Stripe checkout URL.
  • run402 billing topup <org-id | wallet | email> --sats <n> [--wait] [--qr <file.png>] [--timeout <seconds>] [--idempotency-key <k>] — top up the cash balance 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:<bolt11>, pay from any Lightning wallet). --qr <file.png> 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 <org_id> <on|off> [--threshold <n>] — auto-repurchase $5 packs when credits drop below threshold. Requires saved Stripe payment method.
  • run402 billing balance <org-id | wallet | email> — balance + email_credits_remaining + tier + lease + auto_recharge state (response includes organization_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 <org-id | wallet | email> [--limit <n>] — ledger history. Keyed by organization id: a wallet/email is resolved to its organization_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 admin-only). link-wallet requires body wallet SIWX/admin; checkout/auto-recharge require linked wallet SIWX/admin. CLI signs from local allowance, so allowance wallet must belong to 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).

KMS signers — provision AWS KMS-backed Ethereum signers per project for signing and broadcasting smart-contract transactions. Private keys never leave KMS. Pricing: $0.04/day rental + $0.000005 per call. Signer creation requires $1.20 in cash credit (30 days of rent). Non-custodial — see https://run402.com/humans/terms.html#non-custodial-kms-wallets.

  • run402 contracts provision-signer --chain <base-mainnet|base-sepolia> [--recovery-address 0x...] — provision a KMS signer ($0.04/day rental, requires $1.20 in cash credit at creation). Prompts for confirmation if the project already has ≥1 active signer.
  • run402 contracts get-signer <signer_id> — 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 <signer_id> [--address 0x... | --clear] — set/clear the optional recovery address used for auto-drain on day-90 deletion (free)
  • run402 contracts set-alert <signer_id> --threshold-wei <n> — set the low-balance alert threshold (free; alerts billed via existing email infra)
  • run402 contracts call <project_id> <signer_id> --to 0x... --abi <json> --fn <name> --args <json> [--value-wei <n>] [--idempotency-key <k>] — submit a contract write call (gas at-cost + $0.000005 KMS sign fee). Returns { call_id, tx_hash, status }.
  • run402 contracts deploy <project_id> <signer_id> --bytecode 0x... [--chain <base-mainnet|base-sepolia>] [--value-wei <n>] [--idempotency-key <k>] — deploy a contract from the KMS signer (gas at-cost + $0.000005 KMS sign 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 <chain> --to 0x... --abi <json> --fn <name> --args <json> — read-only contract call (free, no signing, no billing)
  • run402 contracts status <call_id> — get call status, gas used, gas cost USD-micros, receipt, error (free)
  • run402 contracts drain <signer_id> --to 0x... --confirm — drain native balance to a destination address (gas at-cost + $0.000005 KMS sign fee). Works on suspended signers — the safety valve.
  • run402 contracts delete <signer_id> --confirm — schedule the KMS key for deletion (7-day window). Refused if balance ≥ dust — drain first.
  • run402 feedback send "<text>" [--project <project_id>] [--handle <handle>]--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 (≤64 chars), delivered as-is.
  • run402 agent contact --name <name> [--email <email>] [--webhook <url>]
  • 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.

Operator health notifications (audit log, preferences) plus the self-serve Telegram push channel and its routing rules.

  • run402 deliveries list [--type <event_type>] [--since <iso>] [--limit N] [--after <cursor>] — paginated delivery-attempt audit log.
  • run402 deliveries get <id> — one audit row.
  • run402 contacts preferences / preferences set <key>=<value> ... — 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 <event_type>] — 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, 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).

Telegram channel (notification-channel-routing-telegram):

  • run402 contacts connect telegram [--label <name>] — 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 operator email (bindings are addressed to it). Until the platform’s dedicated bot is provisioned on this deployment, 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 <binding_id> — requires 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 <binding_id> [--project <id>] [--source app|platform] [--type a,b] [--class a,b] — requires operator_passkey; an unusable/foreign telegram_binding_id returns the same 404 as a nonexistent one.
  • run402 subscriptions list — the operator’s routing rules.
  • run402 subscriptions rm <rule_id> — requires operator_passkey.

Full rule-model explanation: run402 notifications channels --help / run402 subscriptions --help.

buzz notifications — route project events into a Buzz channel

Section titled “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 <buzzper_id> --org | --listed [--project <id> ...] — switch the route between the whole organization (project_scope: org: every project it owns, present and future, read fresh per fact 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 <buzzper_id> --add <project_id> | --remove <project_id> — 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 facts occurring from then on are delivered; nothing is replayed.
  • run402 buzz notifications on-call <buzzper_id> --agent <hex-pubkey> [--name <display>] | --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 @<name> 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 <uuid> --installation <buzzci_id> --name <route_name> --channel <nip29-channel-id> --project <id> [--project <id> ...] [--event-type <t> ...] [--event-class <c> ...] [--include-org-events] [--on-call <hex-pubkey> [--on-call-name <display>]] — 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 facts 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 handoff, 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 <uuid> | <buzzper_id>] — 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 <buzzper_id> [--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 <buzzper_id> [--limit <n>] [--cursor <c>] [--delivery <buzzped_id>] — 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 <buzzper_id> — 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 operator notification).
  • run402 buzz notifications rotate <buzzper_id> — 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 <buzzper_id> — 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 operator 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).

  • run402 webhook-secret rotate — new HMAC signing secret for the operator webhook, returned EXACTLY once (store it immediately). Previous secret remains valid for 24h (dual-secret grace window). Requires operator_passkey assurance.

Operator = human email identity, distinct from agent wallet/SIWX. One browser login spans wallets that verified the email; operator overview returns cross-wallet union. Single-wallet org state: run402 status; operator login/approval state: run402 operator status.

Wallet-less writes (operator approval): wallet agents use SIWX. Wallet-less human uses control-plane session (operator login --loopback); high-stakes provision, deploy, secret writes also need passkey-fresh approval for one (action,target) as X-Run402-Write-Auth. Missing approval -> 403 WRITE_AUTH_REQUIRED / SDK OperatorApprovalRequiredError (isOperatorApprovalRequired()) with exact operator approve ... command. Approval is never ambient for MCP/CI/non-TTY; only interactive TTY may auto-open browser and retry.

  • run402 operator login [--no-open] — read session via device auth (RFC 8628). Prints URL + user code to stderr, opens browser on TTY, approve by magic-link or passkey, caches {base}/operator-session.json (0600, base dir shared across wallets). Success { logged_in, email, wallets, wallet_count, expires_at, absolute_expires_at, expires_in_seconds }.
  • run402 operator login --loopback [--no-open] / run402 operator login --step-up — write-capable loopback-PKCE (RFC 8252) login. Starts 127.0.0.1, browser passkey ceremony, mints passkey-fresh session (provenance=loopback_pkce) at {base}/control-plane-session.json (0600; token never printed). --step-up refreshes for STEP_UP_REQUIRED. whoami surfaces it; logout clears it. Stdout includes memberships[], including invites auto-claimed by login when lookup succeeds. Hosted logins are SDK/console-side (r.operator.session.*), not CLI.
  • run402 operator overview — organization view across ALL wallets controlling your email. Sends the cached operator-session bearer to GET /agent/v1/operator/overview. Requires login — returns OPERATOR_LOGIN_REQUIRED (no SIWX fallback) when there is no live session, and clears the cache + returns OPERATOR_SESSION_INVALID on a 401/403 (revoked or expired).
  • run402 operator whoami — local, no network. Prints the cached session (logged_in, email, wallets, expiry) or { logged_in: false, reason: "no_session" | "expired" } with a non-zero exit.
  • run402 operator logout — revokes the session server-side (POST /agent/v1/operator/session/revoke) then clears the local cache. Idempotent; best-effort revoke (always clears locally). Stdout: { revoked, cleared }.
  • run402 operator claim-wallet-org [--org <id>] [--name <label>] — transfer wallet-agent-owned org into human console identity. Wallet authenticator remains agent; agent downgraded owner->developer. Requires write-capable control-plane session + fresh active-wallet signature over server nonce. Success { claimed: true, org_id, display_name, role, already_owned }; multiple owned orgs -> { claimed: false, selectable_orgs: [...] }, retry with --org; stale session -> STEP_UP_REQUIRED; --name labels org.
  • run402 operator approve --action <cap> (--org <id> | --project <id>) [--no-open] — mint passkey approval for one (action,target). Actions: org.project.create + --org, project.deploy + --project, project.secret.write + --project. Requires operator login --loopback; loopback-PKCE + passkey confirm page; caches token bound to (api_origin, control-plane-session, action, target) at {base}/write-auth-session.json (0600; token never printed). Multiple approvals can coexist; cleared on login/step-up/logout. Hidden alias operator write-auth; provision/deploy surface or auto-run it on interactive TTY.
  • run402 operator status — local, no network. Stdout JSON { operator_login: { active, provenance, amr, expires_at } | { active: false }, approvals: [{ action, org_id, project_id, expires_at }] } — your control-plane login state + every live approval.

Session is email-scoped (~30m access TTL, ~12h absolute), cached once at base dir independent of --wallet. Login/overview/logout require gateway device-auth bridge; whoami local. No MCP tools by design: MCP authenticates as agent wallet; human session must not be handed to agent.

run402 source-access is RETIRED — a gateway route namespace (/agent/v1/source-access/*) is not a CLI noun. Both spellings answer a structured COMMAND_MOVED, then are reserved: source-access exportrepos recovery-bundle; source-access statusrepos access (its member_custody block; the org-level advisory is doctor --only recovery_posture). See the repos family above for the current surface.

Public service-level status. No allowance, 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

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 <url> [--locale <code>] [--release-id <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 <url> — invalidate a single absolute URL. Stdout: { deleted, host, path, generation }.
  • run402 cache invalidate --prefix <p> --host <h> — invalidate all rows under a path prefix on a specific host.
  • run402 cache invalidate --all --host <h> — 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.

Health + config diagnostics. Agent-DX entrypoint — agents run this first to verify environment before attempting anything else.

  • run402 doctor [--verbose] [--refresh] [--no-scan] [--dir <app>] [--manifest <path>] [--scan-dir <D>] [--project <id>] [--only <check> ...] — 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), allowance + rail, keystore wallet count, API base reachability, active tier + lifecycle state, operator health snapshot (binding state + per-attempt verification failure detail), recovery posture (per vault-owning org: human-owner login + member source-key backup evidence), gitvault, 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 operator 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 allowance missing/error, API unreachable, tier inactive/frozen/past_due/dormant/missing/error, error-severity source-scan findings; advisory: operator_health, recovery_posture, gitvault, runtime_staleness, 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 operator 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 operator sees what to fix. When the gateway reports operator_reachability.reachable: false (recovery-event-reachability — the org resolves to zero verified notification recipients across both the wallet-contact chain and org-membership verified emails), the operator-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 (gitvault-recovery-custody; rides the same operator-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 org invite create <org_id> --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 facts 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 apply. 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). --no-scan skips the scan (config-only checks). --scan-dir <D> is an advanced arbitrary scan and reports advisory findings without claiming application deploy refusal. --project <id> (kychee-com/run402#566) targets the gitvault check at that project, outranking the repo-standing pin/remote/env/active-project default — scoped to the gitvault 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 apply 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 <check> (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 gitvault costs one gitvault read, not the full config/tier/operator/scan sweep. Composes with --project, which still scopes only the gitvault check. The valid check names are config_dir, cli_update, allowance, projects, api_reachable, tier, operator_health, runtime_staleness, recovery_posture, gitvault, 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.

Terminal window
run402 doctor --only gitvault # just the vault diagnosis — no source scan, no monorepo noise
run402 doctor --only gitvault --project prj_1a2b3c
run402 doctor --only config_dir --only allowance --no-scan

Astro dev wrapper. Loads .env.local, verifies RUN402_PROJECT_ID + RUN402_SERVICE_KEY, then spawns astro dev with the env inherited.

  • run402 dev [--port <n>] [--host <h>] [--project <id>]

SDK calls (db, auth.user, cache.invalidate, assets.put) hit the LIVE Run402 project at https://api.run402.com — no local DB/S3/KMS setup required. This is shape-parity with production. Offline emulator mode is deferred to v1.5.

Project scaffolder. Subroute of init (alongside init rail setup).

  • run402 init astro [<dir>] [--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.)

Fetch function logs by request id. Top-level shortcut — for fine-grained per-function control, run402 functions logs <project> <name> still works.

  • run402 logs --request-id <id> [<function> | --function <name>] [--project <id>] [--tail <n>] [--since <ts>] [--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 / delegate (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 <name> (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.


Some npm configurations emit an install-scripts warning that esbuild is not covered by allowScripts. Run402 bundles tsx to load TypeScript deployment 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.