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 saysaccepted:true, preservesmessage/warnings, and returns opaquechallenge_idfor code/both without claiming delivery.run402 auth verify --token <token> [--project <id>]— exchange a magic-link tokenrun402 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 userrun402 auth invite-user --email <addr> --redirect <url> [--admin <true|false>] [--project <id>]— create/update user and send trusted inviterun402 auth set-password --token <bearer> --new <password> [--current <password>]— change, reset, or set passwordrun402 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-domainsis a comma-separated list restricting hosted Google sign-in to those domains (enforced at token issuance);noneclears it; empty = unrestricted.run402 auth passkey-register-options --token <bearer> --app-origin <origin> [--project <id>]— create WebAuthn registration optionsrun402 auth passkey-register-verify --token <bearer> --challenge <id> --response <json> [--label <text>] [--project <id>]— verify and store passkey registrationrun402 auth passkey-login-options --app-origin <origin> [--email <addr>] [--project <id>]— create WebAuthn login optionsrun402 auth passkey-login-verify --challenge <id> --response <json> [--project <id>]— verify passkey login and return session tokensrun402 auth passkeys --token <bearer> [--project <id>]— list authenticated user’s passkeysrun402 auth delete-passkey --token <bearer> --id <passkey_id> [--project <id>]— delete one passkeyrun402 auth providers [--project <id>]— list available auth providersrun402 auth scaffold-roles [--table <name>] [--user-col <col>] [--role-col <col>] [--roles <csv>] [--cache-ttl <secs>]— offline generator: emits a role-table migration +requireRolegate snippet + first-operator bootstrap (JSON out; no project/network). Pipe throughjq(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
Section titled “billing”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 apool_implicationsblock (organizationtier,projects_in_pool_count,organization_api_calls_current,organization_storage_bytes_current,tier_limits,over_limit) — inspectover_limitbefore 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 — userun402 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 fornsats (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 (uppercaseLIGHTNING: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).--waitblocks untilpaid,paid_late, orexpired(exit 2 on expired/timeout; a late payment still credits) and prints the receipt. No Stripe, no node; the gateway answersLIGHTNING_TOPUP_NOT_CONFIGURED(503) when no provider is wired — usebilling checkoutthen.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 includesorganization_id). A wallet/email is resolved to its organization viaGET /orgs/v1/lookup?wallet=|?email=; an organization id (UUID) readsGET /orgs/v1/:org_id/billingdirectly.run402 billing history <org-id | wallet | email> [--limit <n>]— ledger history. Keyed by organization id: a wallet/email is resolved to itsorganization_idfirst, thenGET /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).
contracts
Section titled “contracts”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).--bytecodeis 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.
message
Section titled “message”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);--handleis the human’s X/Twitter handle (≤64 chars), delivered as-is.
run402 agent contact --name <name> [--email <email>] [--webhook <url>]run402 agent statusrun402 agent verify-emailrun402 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
Section titled “notifications”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 needemail_verified;webhook_urlchanges needoperator_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’stelegram.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, printsconnect_url(private chat) andconnect_group_url(group chat) — single-use, 15-minute deep links — then POLLScontacts listuntil the binding flips toactiveor the code expires, printing progress to stderr. Requiresoperator_passkeyassurance AND a VERIFIED operator email (bindings are addressed to it). Until the platform’s dedicated bot is provisioned on this deployment, this returns503 TELEGRAM_CHANNEL_NOT_CONFIGUREDwith anext_actionsentry — printed verbatim.run402 contacts list— every channel (email, webhook, and every live Telegram binding) for the authenticated wallet.run402 contacts rm <binding_id>— requiresoperator_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]— requiresoperator_passkey; an unusable/foreigntelegram_binding_idreturns the same 404 as a nonexistent one.run402 subscriptions list— the operator’s routing rules.run402 subscriptions rm <rule_id>— requiresoperator_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;--listedon a route with nothing stored needs--project.configure --all-projectscreates 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) orplatform_incidenton a routed project is posted by the project bot as@<name> please investigate: …with apmention of that agent: the@nametext is what Buzz renders as a mention chip, the tag is what wakes a managed Buzz agent.--namesupplies 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.--clearempties 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 toanyone) 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-callnames the agent the route pages from the start,--on-call-namethe@nameit is addressed by (seeon-callabove).--include-org-eventsalso delivers the organization’s own facts into the channel — theplatform_payment_receivedreceipt 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’sauthorizationblock is eitherauthorized(live now) orpending_buzz_authorizationwith the ONE exact, non-secret handoff, printed loudly to stderr: a Buzz community owner or admin adds thenotification_pubkeyas 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 classessecurity/billing_critical/destructive_lifecycle/verification/recoverymay 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 honesthealth(derived from route + credential state, never queue emptiness) with per-status delivery counts and therevisionan update must echo.run402 buzz notifications test <buzzper_id> [--wait]— re-checks the Buzz-side membership (activating apending_authorizationroute when the member-add landed — the test doubles as the authorization poll) and queues ONE signed test message (202, queued-not-delivered).--waitpolls 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, thendead_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 withpause_reason: delivery_failuresand fire the mandatorybuzz_route_auto_pausedoperator 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_generationare 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).
webhook-secret
Section titled “webhook-secret”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). Requiresoperator_passkeyassurance.
operator
Section titled “operator”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. Starts127.0.0.1, browser passkey ceremony, mints passkey-fresh session (provenance=loopback_pkce) at{base}/control-plane-session.json(0600; token never printed).--step-uprefreshes forSTEP_UP_REQUIRED.whoamisurfaces it;logoutclears it. Stdout includesmemberships[], 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 toGET /agent/v1/operator/overview. Requires login — returnsOPERATOR_LOGIN_REQUIRED(no SIWX fallback) when there is no live session, and clears the cache + returnsOPERATOR_SESSION_INVALIDon 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;--namelabels 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. Requiresoperator 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 aliasoperator write-auth;provision/deploysurface 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.
source-access (retired)
Section titled “source-access (retired)”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 export → repos recovery-bundle; source-access status → repos access (its member_custody block; the org-level advisory is doctor --only recovery_posture). See the repos family above for the current surface.
service
Section titled “service”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, schemarun402-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 }.statusis NEVERBYPASS— inspect does not issue a request, so it cannot evaluate runtime bypass conditions. Defaults to active release + default locale; pass--locale/--release-idto inspect non-default rows. (The legacy--jsonflag 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.
doctor
Section titled “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 <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, andinstall_confidence(confidence in detecting the installation method, explicitly labeled byinstall_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). thecli_updatecache has a real 24h TTL (kychee-com/run402#561): a plaindoctorcall 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 —--refreshsimply 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.cachealways carriesfresh,freshness_basis: "observation_age",age_ms,refresh_attempted,refresh_failed, andlast_attempt_at, and a stale fallback’shintnames the estimate’s age and the failure reason. An installed version ahead of the observation has statusunknown, with no downgrade advice;--refreshcan retry the registry. A within-TTL observation is not proof that an ahead-of-observation installation is current. Stdout is{ ok, blocking[], warnings[], checks[] }.okanswers one question — can this agent ship from here — and istrueexactly whenblocking[]is empty; advisory findings never affect it or the exit code (0 whenok, 1 otherwise), so an unbound operator passkey or a degraded recovery posture on another org is reported inwarnings[]whileokstaystrue. Every check carriesseverity: "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_updategaps; 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 onokand readwarnings[]for non-blocking gaps. Thetiercheck’sstatusis a fixed vocabulary (ok | inactive | frozen | past_due | dormant | purged | missing | unknown | error, never a tier name) with the tier name and raw lifecycle invalue.tier/value.lifecycle; a wallet whose own organization holds no tier but can reach projects on another organization reportsmissingas advisory (TIER_MISSING_ON_OWN_ORG), not blocking.--onlyand--refreshproduce the same shape. When stale,cli_update.value.next_actions[]includes anupgrade_clientaction withcommand,argv, install context, confidence,cwd, and mutation flags. When operator email verification ispending, the doctor surfaces the per-reason hint fromemail_verification.last_challenge.hintalong withattempt_count/remaining_attemptsso the operator sees what to fix. When the gateway reportsoperator_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 therun402 agent contact --email ...remedy. Therecovery_posturecheck (gitvault-recovery-custody; rides the same operator-status read) renders one entry per vault-owning org:control_plane_configured: falsewarns “no human owner with a working control-plane login” with therun402 org invite create <org_id> --email … --role ownerremedy;source_backup_configured: falsewarns “no human member holds a working source-access key” pointing at console.run402.com/account source enrollment;custody_legacy_present: truewarns 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 asorg_recovery_posture_degraded/org_recovery_posture_recoveredfeed events. A gateway without the block reportsskipped, and an account with no vault-owning org reportsokwith an emptyorgslist. 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’ssrcand explicit referenced source files, excludes sibling apps and nested repositories, and shares its gate with SDKupanddeploy apply. It flags hallucinated SDK auth names (R402_AUTH_UNKNOWN_EXPORT), state-changing GET handlers (R402_AUTH_STATE_CHANGING_GET),auth.*calls inexport const prerender = truepages (R402_AUTH_PRERENDERED), and direct mutation ofinternal.sessions.authz_version(R402_AUTH_AUTHZ_VERSION_PROHIBITED).--no-scanskips 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 applyruns the scan as pre-flight and refuses to deploy on anyerror-severity finding (bypass withRUN402_DEPLOY_SKIP_SCAN=1). (The legacy--jsonflag 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.
run402 doctor --only gitvault # just the vault diagnosis — no source scan, no monorepo noiserun402 doctor --only gitvault --project prj_1a2b3crun402 doctor --only config_dir --only allowance --no-scanAstro 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.
init astro
Section titled “init astro”Project scaffolder. Subroute of init (alongside init rail setup).
run402 init astro [<dir>] [--force]— creates a deployable Astro project withpackage.json(dev/deploy scripts),astro.config.mjs(one-line@run402/astropreset),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 withdb().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--jsonflag 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 thex-run402-request-idheader 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 itsfunctionandorigin. By default only the function’s own output is shown (--app); Lambda’s runtime lines (INIT_START,START/END/REPORT RequestId, billed duration) areorigin: "platform"and hidden —hidden.platformcounts them and ahintappears when hiding them left nothing,--platformshows only them,--allshows the raw stream.--function <name>(or a leading positional) narrows the scan;--functionalone 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? };okisfalseonly when a function’s read failed (named inerrors[]with itscode). (The legacy--jsonflag 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
Section titled “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 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.