Core commands
Command Reference
Section titled “Command Reference”run402 up [repo-or-path] [--name <name>] [--project <project_id>] [--manifest <path>] [--dir <path>] [--tier <prototype|hobby|team>] [-y|--yes] [--check|--print-spec|--print-manifest|--plan|--require-plan <plan_id>] [--verify] [--propagation-budget-s <seconds>] [--no-propagation-wait] [--quiet]— SDK-owned recursive app deploy. Validatesrun402.deploy.json/app.json, requires explicit--manifestfor executable.ts/.jsconfigs, ensures missing Cloud prerequisites when approved, resolves/creates/links a project, then applies the manifest. Output includessteps[]; success has no top-levelstatus. Use--checkfor local-only validation,--print-specfor normalizedReleaseSpec,--planfor a gateway-reviewed non-deploying plan, and--require-planfor exact reviewed apply. App HTTP verification reports fresh edge misses aspropagation_pending, waits up to--propagation-budget-s(default 120), and--no-propagation-waitreturns the pending state immediately. Add--verifyto wait for gateway/edge release coherence after the deploy and attachedge_coherence; non-coherence exits 2.run402 up verify [repo-or-path] [--project <project_id>] [--manifest <path>] [--dir <path>] [--propagation-budget-s <seconds>] [--no-propagation-wait] [--quiet]— rerun manifest HTTP verification (app manifestverify.http[]or deploy-manifest top-levelverify) without deploying, uploading, creating a project, or mutating resources. Use it afterpropagation_pendingor before declaring a consumer copy healthy.
run402 init— set up with x402 (Base Sepolia). Creates the local wallet, requests faucet, checks tier, lists projects. A blocked or unconfirmed faucet is reported infunding; next actions carry the server cooldown or balance-check recovery instead of recommending immediate deploy. Identity setup can succeed while funding remains blocked.run402 init --api-base <url>— configure the active profile to target a Run402 Core/API base. For Core, this does not create a wallet, request faucet funds, or require a Cloud tier.run402 init mpp— set up with MPP: pathUSD on Tempo, faucet-funded on the Moderato testnet. Same steps, different payment rail; a purchase pays the first Tempo chain its 402 offers that the wallet can cover.run402 init lightning— the Lightning wallet: the same Base wallet (the x402 fallback) plus a budgeted Lightning wallet the platform mints on Run402’s own Hub (POST /agent/v1/lightning-wallet; custodyrun402_hub, a starter amount funded by the platform). The one-time pairing is stored inwallet.jsonbeside the Base key and never printed;railbecomeslightning, so tier purchases and image generation answer a Lightning challenge first and fall back to x402. Image generation pays the x402exactscheme on Lightning when Run402 offers it, and MPP Lightning otherwise. The summary carrieslightning: { wallet_id, status, lightning_address, budget_sats, starter_sats, balance_sats, custody, outcome };outcome: "minting"means rerun in a few seconds,"unavailable"means no Hub on this gateway (x402 keeps paying).run402 init <rail> --switch-rail— switch the persisted rail; re-running with the same rail is idempotent.
run402 pay <url> [--method <M>] [--body <json-or-text>] [--max-usd <amount>] [--max-sats <n>] [--lightning-pairing <file|->] [--idempotency-key <key>] [--require-receipt] calls an arbitrary x402- or MPP-priced HTTP endpoint through the SDK buyer. On the mpp rail a 402 carrying an MPP Tempo charge (WWW-Authenticate: Payment) is paid in pathUSD on the first offered Tempo chain the wallet covers, bounded by --max-usd before signing; the printed payment carries protocol: "mpp", method: "tempo", the eip155:4217 / eip155:42431 network, the Tempo transaction hash, and evidence.protocol_receipt (the seller’s Payment-Receipt), with offer.status and merchant_receipt.status "unavailable". Over MPP --idempotency-key is correlation only: repeat the identical command from the same client to resolve an unknown outcome, which re-presents the same credential and is never charged twice. A 402 that offers nothing the wallet’s rail pays fails PAYMENT_NETWORK_UNSUPPORTED before signing with details.offers[] and a run402 init <rail> --switch-rail next action naming the rail that pays it. On the lightning rail a 402 that names an lnbtc entry on the wallet’s network is paid in sats from the agent’s Lightning wallet, bounded by --max-sats (invoice plus routing headroom) or the rail’s ceiling; an lnbtc entry on another network is refused with LIGHTNING_NETWORK_MISMATCH and nothing is paid. --lightning-pairing <file|-> pays with a Nostr Wallet Connect pairing read from a file or stdin (never the command line, never printed) on whatever network its node is on, which is how a testnet desk or an operator’s own node is paid. A host’s priced routes, with the methods that buy each and the networks it takes (eip155:…, lnbtc:…), are listed at https://<host>/.well-known/x402, with mppResources and paymentCapabilities for the routes that also take an MPP Tempo charge; a route priced on Lightning or Tempo alone is listed only there, never in the x402 Bazaar. --require-receipt cannot be satisfied by a Lightning entry (LIGHTNING_RECEIPT_UNAVAILABLE). The request body goes in --body and nowhere else: --json is the CLI-wide output-format flag, takes no value, and is a no-op here because pay always prints JSON. Writing --json '<payload>' does not send the payload — it fails BAD_USAGE with a hint naming --body. The default ceiling is $0.10; --max-usd accepts up to six decimal places and is converted exactly to USD micros. --require-receipt requires a verified wallet-rooted offer before payment and a matching receipt afterward. JSON output is the complete x402-commerce-result.v1 envelope with settlement, movement/replay, delivery, offer, merchant-receipt, signer-relationship, policy, and portable evidence. Unpriced URLs return payment: null. A seller that settles and then answers a non-2xx with a valid PAYMENT-RESPONSE receipt prints a normal result: http_status is the seller’s status, body is the seller’s body, and payment.funds_moved: true with payment.delivery.status: "failed". For PAYMENT_INTENT_PENDING on a trusted Run402 host, wait for Retry-After and repeat the identical command with the same payer, request, and key. Never substitute a fresh key. Custom/arbitrary hosts and other funds_moved: "unknown" outcomes remain ambiguous and require reconciliation.
run402 pay https://seller.example/translate --method POST \ --body '{"text":"hello"}' --max-usd 0.05 \ --idempotency-key translation:1 --require-receiptA route priced in sats, paid with a pairing on its network (the pairing on stdin):
run402 pay https://kychange.com/api/usdc/10k --method POST \ --body '{"destination":"0x0000000000000000000000000000000000000001"}' \ --max-sats 10500 --lightning-pairing - < ./payer.nwcstatus
Section titled “status”run402 status — show full organization state in one shot (wallet, rail, balances, tier, projects, active project). Read-only, JSON output. Includes a wallet: { local_label, server_label, address } object naming the active named wallet (local_label is the local selector, server_label the server-synced display name or null), a top-level rail, and a balances: { on_chain_usd_micros, on_chain_by_network, on_chain_token, allowance_usd_micros, held_usd_micros } object: on_chain_usd_micros is the wallet’s on-chain balance summed over the networks its rail pays on, on_chain_by_network each of those networks apart (base_mainnet_usd_micros and base_sepolia_usd_micros on x402, tempo_mainnet_pathusd_micros and tempo_moderato_pathusd_micros on mpp; null where a read failed), and allowance_usd_micros the organization’s allowance. The on-chain token tracks the rail (USDC on x402, pathUSD on mpp); the allowance is rail-independent. Remote reads carry remote_status entries for tier, billing and projects (available or unavailable, with safe error code/category). projects_source distinguishes server inventory from local_cache. Null remote values after failed reads are unknown, not evidence that initialization or funding is needed.
wallets
Section titled “wallets”Manage multiple named wallets (profiles) on one machine. Keys never leave the machine (non-custodial). The default wallet lives at the config-dir root; named wallets live under {config_dir}/profiles/<name>/.
run402 wallets list— JSON array of{ local_label, server_label, address, address_short, rail, active }. Reads non-secretmeta.json; never loads private keys.run402 wallets current— the resolved active wallet{ local_label, source, source_detail, address, server_label, configured, created, rail, faucet_used, path, warnings }; with no local wallet yet,configured: falseand aninitialize_walletnext action.faucet_usedrecords faucet use, not pay-readiness.source∈ flag|env|binding|config|default.warningssurfaces env-vs-binding conflicts and local-name-vs-server-label drift.run402 wallets new <name> [--mpp | --rail <x402|mpp|lightning>]— create a new wallet (generates a key);wallets new defaultcreates the root wallet without funding it.{ local_label, address, rail, created: true, next_actions }. Alightningwallet is minted on the platform byrun402 --wallet <name> init lightning.run402 wallets fund— request testnet funds from the faucet into the active wallet (Base Sepolia USDC, or Tempo pathUSD on thempprail) and wait briefly for them to land.run402 wallets balance— the active wallet’s on-chain balances (onchain:base_mainnet_usd_micros,base_sepolia_usd_micros,tempo_mainnet_pathusd_micros,tempo_moderato_pathusd_micros; null where a read failed) plus its organization’sallowance_usd_micros(null with no organization).run402 wallets lightning [status|mint|revoke]— the active wallet’s Lightning wallet.statusreads the platform record plus a best-effort balance and remaining budget over NWC ({ rail, lightning: { wallet_id, status, custody, lightning_address, budget_sats, starter_sats, has_pairing, balance_sats, budget_remaining_sats } });mintisinit lightning’s wallet step alone;revokedeletes the sub-wallet on Run402’s Hub, forgets the pairing, and returns the rail to x402. The pairing secret is never printed.run402 wallets use <name>— set the global default wallet (config.jsonactive_wallet).{ name, active: true }.run402 wallets rename <old> <new>— rename a wallet; renamingdefaultmigrates its root files intoprofiles/<new>/.{ from, to, renamed: true }.run402 wallets bind [<name>]— write./.run402.jsonbinding this directory to a wallet (defaults to the active one). Safe to commit (holds only a name).{ wallet, file, bound: true, safe_to_commit: true }.run402 wallets unbind— remove./.run402.json.{ file, unbound }.run402 wallets import <name> --key <path|->— adopt an existing 0x-prefixed 64-hex private key (file path or-for stdin) as a named wallet.{ local_label, address, imported: true, signer: "local" }.run402 wallets import <name> --kms <key-arn> [--aws-profile <profile>]— adopt an AWS KMS key (ECC_SECG_P256K1,SIGN_VERIFY) by reference: reads its public key, derives the address, and writes a profile that holds the ARN and no secret. Every command then signs through KMS under--wallet <name>(SIWX, x402, MPP on Tempo, transactions); the key never leaves KMS.{ local_label, address, imported: true, signer: "aws_kms", key_arn }. Errors:KMS_KEY_UNSUITABLE(wrong key type),KMS_ACCESS_DENIED(the AWS identity is not on the key policy; names the caller),KMS_SIGNER_UNAVAILABLE(expired session:aws sso login --profile <p>),SIGNER_ADDRESS_MISMATCH(the key at the ARN is not the imported one).run402 wallets send <amount> --to <address> [--from <smart-account>] [--network base|base-sepolia|tempo|tempo-moderato] [--confirm]— send USDC (Base) or pathUSD (Tempo; the fee is paid in pathUSD too) from the active wallet, key-backed or KMS-backed.--fromsends from a CoinbaseSmartWallet the wallet owns (Base only; refused withSMART_ACCOUNT_NOT_OWNERotherwise). Without--confirmit runs every check and prints a preview ({ preview: true, network, asset, amount_usd_micros, from, to, sender, balance_usd_micros, estimated_gas, hint }) without signing; with it, it reports{ tx_hash, from, to, amount_usd_micros, network, asset, explorer_url }once the transaction is mined, orSEND_REVERTEDwith the hash. A short balance isSEND_INSUFFICIENT_BALANCEnaming the asset and shortfall. The MCP sandbox refuses it (VALUE_TRANSFER_REQUIRES_CLI).run402 wallets rm <name> --yes— delete a wallet and its keys. Requires--yes(agent-first: no interactive prompt). Refuses to removedefault. Removing a KMS-backed wallet deletes the local profile only; the KMS key is untouched.{ name, removed: true }.- Server-side display label:
new/rename/importpush the wallet’s name to a server-side label (signed by the wallet — proof of control) so the same name shows cross-machine and in the console (WEB). Best-effort and on by default;RUN402_WALLET_LABEL_SYNC=0opts out (fully offline wallet ops). The local folder name is the source of truth; the label is a mirror, andwallets currentflags any drift. - Selection for ANY command:
--wallet <name>(alias--profile) >RUN402_WALLET> nearest./.run402.json/.run402.local.json>wallets usedefault >default. A conflicting env + binding errors withWALLET_SELECTION_CONFLICT(resolve via--wallet,unset RUN402_WALLET, orwallets unbind). Selecting a non-existent wallet errors withWALLET_NOT_FOUND.
Tier and quotas are per organization (not per project) — set is organization-wide, status.pool_usage is the pooled total across every project in the organization. set refetches status after the call and includes it as status_after.
run402 tier statusrun402 tier set <prototype|hobby|team>
credentials
Section titled “credentials”Two surfaces under one command. credentials <verb> acts on PROJECT CREDENTIALS (rows on the gateway); credentials project-keys <verb> acts on the LOCAL key cache on this machine. A project credential (r402_…) is named, listable, expiring and individually revocable, and several may be live per kind at once — that overlap is how you rotate with no downtime. It replaces the derived anon_key/service_key, which come from the platform signing key, never expire, and cannot be revoked one at a time.
Secrets are returned EXACTLY ONCE, from issue, rotate and token. Full JSON goes to stdout so it can be piped (| jq -r .secret); warnings go to stderr. Never write one of these responses to a result cache or tmp file.
run402 credentials status [--project <project_id>]— am I still on the retiring key? Returnsstate: "legacy"|"rotatable",rotatable_credentials,credentials[], andretirement.gated_on[].retirement.deadlineis ALWAYS null and that is deliberate: retirement is gated on conditions (every project migrated, 30 consecutive days of zero legacy-key use, explicit owner approval), never a date — do not plan against one. Needs onlyproject.read, so automation can check its own posture.run402 credentials issue --kind <anon|service> --name <name> [--project <project_id>] [--expires <iso8601>] [--import]— mint one; secret printed ONCE.--namemust be unique among LIVE credentials; re-using a live name returns409 CREDENTIAL_NAME_TAKEN, and that collision IS the idempotency story (a retried create never silently mints a second credential).--expiresmust be in the future and within one year.--importadditionally writes the minted secret into this machine’s local key cache (what deploys and data-plane commands read) — the cold-restart re-key path in one command per kind instead of issue-then-project-keys import. A first--kind anon --importon a machine with no cached entry still needs a service key first (the same rule asproject-keys import), and that refusal comes BEFORE the mint, so a show-once secret is never burned on a usage error.run402 credentials list [--project <project_id>] [--include-revoked]— metadata only; never a secret or a secret hash.run402 credentials rotate <credential_id> [--project <project_id>]— mint a replacement and revoke the old one in one transaction, keeping the name and recordingreplacement_of. New secret printed ONCE. For a rotation with NO downtime window, prefer issuing a second credential, deploying it, then revoking the first; userotatewhen the old secret is already compromised.run402 credentials revoke <credential_id> [--project <project_id>] [--reason <text>]— immediate, and frees the name for reuse.run402 credentials token [--project <project_id>] [--kind <anon|service>]— mint a SHORT-LIVED token (defaults toservice). This is the cold-restart recovery path and the ONE credential call an agent can make with no human present: a grant key is accepted. No step-up, because there is nobody to prompt; what it returns expires, so it cannot become a durable root.
Authority split: issue/rotate/revoke require owner membership on the project’s owning org PLUS a fresh step-up, and a grant key can NEVER satisfy them — otherwise a scoped agent credential could escalate itself into a permanent root. Authenticate with a wallet (SIWX) or a sign-in session (run402 login). token is the deliberate exception.
run402 credentials project-keys list— LOCAL CACHE read. Lists cached project-key entries withsource: "local_cache",cache_path,wallet/profile, key presence, prefixes, fingerprints, and timestamps. Never prints full keys.run402 credentials project-keys status --project <project_id>— LOCAL CACHE read for one project id.configured: falsemeans this selected wallet/profile lacks cached keys; it does not mean the server project is missing.run402 credentials project-keys import --project <project_id> --service-key-stdin— import a service key from stdin. Optional anon key comes from--anon-key-env <env>.run402 credentials project-keys import --project <project_id> --service-key-env <env>— import a service key from an environment variable. Do not pass service keys as argv values.run402 credentials project-keys import --project <project_id> --anon-key-env <env>— anon-only rotation. Import writes the whole cache entry, so the FIRST import for a project must supply a service key; once an entry exists,--anon-key-envalone rotates the anon key and keeps the cached service key. Rotating the anon key therefore never requires exporting the service key with--revealand passing it back through a shell. Passing--anon-key-envwith no cached service key fails withBAD_USAGEnaming that flag.run402 credentials project-keys export --project <project_id> --reveal— print cached secret key material. Requires--reveal.run402 credentials project-keys delete --project <project_id>— delete one local cache entry without deleting or changing the server project.
projects
Section titled “projects”run402 projects quoterun402 projects list [--org <org_id>] [--all]— SERVER read of the named, domain-aware inventory (NOT the local project-key cache). Membership-scoped by default: every project owned by an org your wallet is an active member of, each row{ project_id, name, site_url, custom_domains, org_id, status, active }(activefrom local state).--org <org_id>filters to one org (authorize-before-reveal: non-member/guessed id -> 403, non-UUID -> 400).--allreads every project you can reach across all your orgs (GET /agent/v1/me/projects) — as your sign-in session afterrun402 login, else as the current wallet — and echoesscope. Barerun402 projects listis the cold-start path (no login needed). Tier/lifecycle live on the organization — userun402 status/run402 tier status.run402 projects rename <project_id> --name <label>— rename a project (fix an auto-generated name). Needs orgadmin+ (or aproject:writegrant) on the owning org; authorize-before-reveal (unauthorized/guessed id -> 403, never a not-found oracle). Works even if the project isn’t in the local project-key cache. Server-validated name (1-200 chars, no control characters).run402 projects provision [--name <name>] [--org <org_id>]—--orgprovisions into an EXISTING org (you needdeveloper+ on it); omit for the cold-start path (the wallet’s organization). Tier is org-governed — a client-supplied--tieris ignored when targeting an org. Against a configured Core target, creates a local Core project without Cloud payment and saves it as the active project.run402 projects use <project_id>— SERVER validation, then stores only the active project id in profile state. It does not require a matching local project-key cache entry.run402 projects current— LOCAL STATE read of the active project pointer with profile-state provenance and validation status.run402 projects get <project_id>— SERVER read of one project’s authoritative view:{ project_id, public_id, name, org_id, tier, effective_status, organization_lifecycle_state, site_url, custom_domains[], last_deploy, mailbox[], usage{api_calls, storage_bytes, api_calls_limit, storage_bytes_limit, storage_limit}, created_at }. Caller-authed (SIWX/control-plane, no project keys) and works even if the project isn’t in the local project-key cache; authorize-before-reveal (unauthorized/guessed id -> 403, never a not-found oracle). Returns NO keys — userun402 credentials project-keys ...for local credential-cache inspection/export.run402 projects sql <project_id> "<sql>" [--file <path>] [--params '<json_array>'] [--read-only]—POST /projects/v1/:project_id/sqlwith the cached service key, or, with no cached key, as you (an org member with the viewer role reads, read-only and enforced by PostgreSQL; developer and above can write;--read-onlyforces read-only for any caller); prints{ rows, row_count, fields, statements, warnings }.warnings[]never changes the result:SCHEMA_CHANGE_OUTSIDE_MIGRATIONmeans a schema change on a project with a live release belongs inspec.database.migrations(it will be refused outside migrations in a later release);MULTI_STATEMENT_TEXT_BODYpoints at--batch.run402 projects sql <project_id> --batch <file.json> [--transaction all|each]— runs a JSON array of{ "sql", "params"? }statements (one statement each, up to 100).all(default) rolls back every statement on the first failure (SQL_BATCH_STATEMENT_FAILED,details.index);eachcommits each on its own, prints everyresults[i].status, and exits 1 when any failed.run402 projects rest <project_id> <table> "<query>"run402 projects costs <project_id> [--window <24h|7d|30d|90d>]— staff-only per-project finance: revenue, direct cost, margin, and cost breakdown. Uses the configured wallet for admin auth;RUN402_ADMIN_COOKIE='run402_admin=...'is an optional browser-session override.run402 projects promote-user <project_id> <email>— promote a user to project_admin rolerun402 projects demote-user <project_id> <email>— demote a user from project_admin rolerun402 projects <usage|schema> <project_id>run402 projects delete <project_id> --confirm— cascade deletes all project resources: Lambda functions, subdomains, S3 site files, releases, secrets, and published releases. The schema slot is dropped and recreated. This is irreversible.--confirmis required.run402 projects validate-expose [id] <manifest_json>— validate an auth/expose manifest without applying itrun402 projects validate-expose [id] --file manifest.json [--migration-file setup.sql]— validate file input with optional migration-reference SQLrun402 projects apply-expose <project_id> <manifest_json>— apply a declarative authorization manifestrun402 projects apply-expose <project_id> --file manifest.json— apply from a JSON filerun402 projects get-expose <project_id>— print the current manifest (source: applied | introspected)
Provisioning automatically sets the new project as the active project. Other commands that take <project_id> default to the active project when omitted.
SQL supports DDL + queries, returns JSON. REST uses PostgREST syntax (select=, eq., order=, limit=).
User auth: password + Google OAuth. See “User Auth” section below.
admin (staff only)
Section titled “admin (staff only)”run402 admin lease-perpetual <org_id> --enable | --disable— toggle the organization-level escape hatch. When enabled, the organization never advances pastactiveregardless of lease expiry; every project on the organization is pinned. Enabling on a grace-state organization (past_due / frozen / dormant) reactivates inline (reactivated: truein the response).run402 admin archive <project_id> [--reason "..."]— staff moderation. Setsprojects.archived_at = NOW()on a single project; sibling projects on the same organization keep serving. No-op when already archived (returnsnote: "already archived").run402 admin reactivate <project_id>— un-archive a project (flipsarchived_atback to NULL). It does not touch organization-level lifecycle. To reactivate a grace-state organization, runrun402 tier set <tier>(the tier flow runs the lifecycle advance inline) or enablerun402 admin lease-perpetual <org_id> --enable.
All admin subcommands require a staff wallet (or a staff OAuth session). Project owners with a non-admin wallet receive 403 admin_required.
rehearsal, snapshots, branches
Section titled “rehearsal, snapshots, branches”run402 deploy rehearse [<plan_id>] [--manifest <path>] [--project <project_id>] [--teardown keep|on_pass|always] [--json]— ADVANCED: rehearse without committing. With no<plan_id>it discovers the manifest in the current directory the wayupdoes (or takes--manifest), creates a reviewed plan, uploads its bytes, and rehearses it. With a<plan_id>whose bytes were never uploaded (a--plan-only plan), the gateway answers409 REHEARSAL_CONTENT_MISSINGand the CLI uses the manifest to upload and retry; if the project changed since that plan a fresh plan is rehearsed and the result says so underreplanned. Source project data and the original plan stay untouched; a project with no live release rehearses on an empty branch. The result names the plan rehearsed and its bound commit command.run402 snapshots create [project_id] [--label <text>] [--metadata <json>] [--json]— capture a manual internal restore point.--label(1–120 characters) and--metadata(a flat JSON object, up to 4 KB) are stored with the snapshot, outside the project’s database, so they survive restores; never put secrets in metadata. Every snapshot reportslabel,metadata,created_by(credential_kind,principal_id), andrestore_of(set on thepre_restoresnapshot a restore takes).run402 snapshots list [project_id] [--kind manual|pre_migration|pre_restore|scheduled] [--limit <n>] [--after <cursor>] [--json]— keyset-paginated snapshot list.run402 snapshots get [project_id] <snapshot_id> [--json]— inspect one snapshot and its next actions.run402 snapshots restore [project_id] <snapshot_id> [--release keep|snapshot] [--include-auth] [--json]— plan a restore and printrestore_plan.confirm.tokenplus aconfirm_command; no mutation.--release snapshotalso puts back the release that was live when the snapshot was taken (live release pointer, static site, routes, subdomains) in the same transaction as the data;restore_plan.releasesays whether that release isrestorable, lists infunctions_to_redeploythe functions it re-deploys to their capture-time code right after the flip, and names inwarnings(FUNCTION_VERSION_MISMATCH) any function whose capture-time source is no longer stored (it keeps its current code).run402 snapshots restore [project_id] <snapshot_id> --confirm <token> [--release keep|snapshot] [--include-auth] [--json]— execute the atomic offline-materialize-then-flip restore and wait for its result (it runs in the background on the gateway; the CLI polls). Pass the same--releaseand--include-authas the plan, or the confirm is refused409 STALE_RESTORE_CONFIRMATION. Auth users/passkeys are restored only with--include-auth; sessions and tokens are never restored. One restore runs per project at a time (409 SNAPSHOT_RESTORE_IN_PROGRESS). The result nameslive_release_id; undo with thepre_restoresnapshot it reports.run402 snapshots restore-status [project_id] <snapshot_id> <restore_id> [--json]— read one restore:status(running/ready/failed),release_mode,live_release_id,error, and, when ready, the fullresult.run402 snapshots delete [project_id] <snapshot_id> [--json]— delete a snapshot and release its CAS references.run402 branches create [project_id] [--from-snapshot <snapshot_id>] [--name <label>] [--email-mode sandbox|off] [--enable-cron] [--ttl-days <1..30>] [--json]— create a contained branch project. Email defaults to sandboxed, scheduled functions default off, and TTL defaults to 7 days. A parent with no live release yields an EMPTY branch (branch_url: null, no cloned release) whose host comes up on its first activation.run402 branches list [project_id] [--json]— list active branches for the parent project.run402 branches renew [project_id] <branch_project_id> [--ttl-days <1..30>] [--json]— extend a branch TTL.run402 branches delete [project_id] <branch_project_id> [--json]— delete a branch project and purge its resources.
deploy
Section titled “deploy”run402 deploy [--manifest app.json] [--project <project_id>] [--check|--print-spec|--print-manifest|--plan|--require-plan <plan_id>] [--no-rehearse] [--json] [--quiet|--final-only] [--allow-warning <code> ...] [--allow-warnings] [--allow-dirty] [-v|--verbose]— the deploy itself (the unified apply primitive withassetsslice support);run402 updoes any missing setup and then runs this same deploy. Without--manifest/--spec/--dir/ piped stdin it discoversrun402.json,run402.deploy.json,app.jsonor an executablerun402.deploy.{ts,mts,js,mjs}in the current directory and failsMANIFEST_NOT_FOUNDwhen none is there; it accepts JSON data manifests and explicit executable typed configs through the same--manifestflag. Rehearsal against a contained branch is automatic when there is a live release to protect (--no-rehearseskips it);--jsonis accepted as a no-op because success output is always JSON. Against avault_policy: requiredproject, the deploy-lane capture applies the SAME dirty-tree rulerepos capturedoes: a dirty work tree refusesSNAPSHOT_DIRTY_TREEbefore any capture, and--allow-dirtycaptures it as-is with the samemodified_captured/untracked_captureddisclosure (see therepossection above and Observability). The final result always carries astatsblock;-v/--verbosealso prints a one-line stderr summary.run402 deploy resume <operation_id> [--project <project_id>] [--quiet|--final-only]— re-run a paused operation forward, or follow it when it is still runningrun402 deploy status <operation_id> [--project <project_id>]— fetch one operation’s snapshot (status,release_id,urls,error, rehearsal report)run402 deploy promote <release_id> [--project <project_id>] [--allow-warning <code>] [--allow-warnings]— pointer-swap (re-point live release without re-running the apply pipeline)run402 deploy list [--project <project_id>] [--limit <n>]— list recent deploy operationsrun402 deploy events <operation_id> [--project <project_id>]— fetch the recorded event stream for an operationrun402 deploy verify <operation_id>|--operation <operation_id> [--project <project_id>] [--wait] [--timeout <seconds>] [--json]— verify gateway/edge release coherence; exits 2 when a valid report remains not coherentrun402 deploy releases get <release_id> [--project <project_id>] [--site-limit <n>]— fetch release inventoryrun402 deploy releases active [--project <project_id>] [--site-limit <n>]— fetch current-live release inventoryrun402 deploy releases diff --from <empty|active|release_id> --to <active|release_id> [--project <project_id>] [--limit <n>]— diff release targetsrun402 deploy resolve (<url> | --url <url> | --host <host> [--path /x]) [--project <project_id>] [--method GET]— explain what a public URL would serve against the live release; a URL cannot be combined with--host/--path
Requires active tier and a provisioned project on Run402 Cloud. Against a configured Core target, uses the active Core project and does not require Cloud tier/wallet setup. Deploys to an existing project: runs migrations, applies the authorization manifest (from a manifest.json entry in files[]), deploys functions, deploys static site, and claims subdomain when the target supports that slice. Secret values are write-only: set them with printf %s "$OPENAI_API_KEY" | run402 secrets set <project_id> OPENAI_API_KEY --stdin or --file <path> before deploy, then use value-free secrets.require in deploy manifests. deploy stops before upload/commit on confirmation-required warnings unless each warning is covered by repeatable --allow-warning <code> or the broad reviewed --allow-warnings; --require-plan absorbs the warning approval already bound into the reviewed plan and rejects warning flags. The manifest must include project_id, or use --project, RUN402_PROJECT_ID, or an app-local workspace link. Omitting intent fails with UP_PROJECT_REQUIRED; it never falls back to the active project.
Inside GitHub Actions, deploy automatically uses OIDC credentials when GITHUB_ACTIONS=true, ACTIONS_ID_TOKEN_REQUEST_URL, and ACTIONS_ID_TOKEN_REQUEST_TOKEN are present. In that mode, project intent comes from --project, RUN402_PROJECT_ID, the app-local workspace link, or the manifest; conflicting selectors are rejected.
run402 live --tables <a,b> [--project <project_id>] [--cursor <cursor>] [--as anon|service]— stream change hints for live tables as NDJSON (Server-Sent Events under the hood; reconnects on its own)run402 live --tables <a,b> --once [--cursor <cursor>] [--wait <1..25>]— one held read: hints since the cursor, or hold up to--waitseconds for the first one
run402 ci link github [--project <project_id>] [--manifest <path>] [--repo <owner/repo>] [--branch <name> | --environment <name>] [--repository-id <repository_id>] [--workflow <path> | --no-workflow] [--expires-at <iso>] [--route-scope <pattern> ...] [--force]— create a GitHub Actions OIDC deploy binding and write a workflow (--no-workflow: the binding only)run402 ci list [--project <project_id>]— list CI bindings for a projectrun402 ci revoke <binding_id>— revoke a binding
link github requires a local wallet because it signs the delegation. The generated workflow does not require a wallet file or service key in GitHub; it uses GitHub’s OIDC token with id-token: write. Use repeatable --route-scope only when CI should deploy route declarations; no scopes means no CI route authority.
transfer (unified project transfer — wallet + email + owned-org recipient)
Section titled “transfer (unified project transfer — wallet + email + owned-org recipient)”One noun, three recipient kinds: wallet = two-party SIWX; email = accepted by a principal whose verified email matches, into an org it owns or a new one; both complete on the ONE accept. Owned org = --to-org <org_id> immediate same-actor org move. Same /projects/v1/:project_id/transfers; preview/list/cancel are kind-agnostic; rows carry recipient_kind.
run402 transfer init (--to <wallet|email> | --to-org <org_id>) [--project <project_id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>] [--retain-member developer]— owner/admin initiate.--toroutes by kind.--to-org: caller must actively own source+destination org; success completes immediately with accepted result + project keys.--retain-member developer: email recipients only, onlydeveloper, rejected withBAD_FLAGon wallet/org rails; recipient must accept it at accept; omit = full severance.--billing-policy/--kysignedwallet-only; email/org reject. Codes:INVALID_RETAIN_ROLE,RETAIN_SUBJECT_REQUIRED.run402 transfer preview <transfer_id>— fetch the preview document (any party; kind-agnostic). Lists custom domains, subdomains, function names, secret NAMES (never values), CI bindings that will be revoked on completion, mailbox summary, billing implications, and — on email transfers — theretain_memberoffer.run402 transfer list [--incoming | --outgoing] [--limit N] [--offset N]—--incoming(default) shows transfers OFFERED TO you;--outgoingshows transfers you initiated. Pending rows are unioned and each entry carriesrecipient_kindandpreview_path.run402 transfer accept <transfer_id> [--org <org_id>] [--accept-retained-member]— the one completion, whatever the row’s address. Wallet-addressed: your wallet must equalto_wallet; the response hassecret_names_inherited[]. Email-addressed: your verified email must match;--org <org_id>names the receiving org you own, omit it to create a new org;--accept-retained-memberaccepts the retained developer offer from the preview (omit = severance) and the result carriesretained_member_principal_id|null. Either way the accept atomically flips ownership, revokes previous owner CI bindings, stampssecrets_rotation_advised, and returns the new owneranon_key+service_key; SDK/CLI persist keys and set the project active. The returnedservice_keysupersedes every earlier one: the previous owner’s key answers401 CREDENTIAL_REVOKED, and the project’s functions are re-keyed within a minute. Theanon_keydoes not change. Rotate inherited secrets withrun402 secrets set. A credential of the wrong kind for the row gets409 WRONG_COMPLETION_FOR_TRANSFER_KINDwith anaccept_transfernext action naming the right one. A project with a KyGit vault hands over its source first: the firstacceptpublishes this machine’s keystore identity, names you as the principal that receives the source, and exitsTRANSFER_VAULT_HANDOVER_PENDING(next action: the sender’srun402 transfer handover <transfer_id>); runacceptagain once the sender has. The accept moves the vault with the project (its storage and downloads count against your org; handoff/invite keys minted before the transfer stop working) and returnsvaultplus aretire_previous_owner_writersnext action.run402 transfer handover <transfer_id>— the SENDER’s step for a project with a KyGit vault: admits the principal the recipient named (its own published signing key, as a vault writer) and wraps the vault’s current key for it. Your key must be a writer of the vault. Safe to re-run; prints{transfer_id, repo_id, writer, envelope, vault_handover}. The recipient can accept oncevault_handover.stateiscomplete.run402 transfer cancel <transfer_id> [--reason <text>]— cancel a pending transfer of any kind (any authorized party). If the recipient’s key was already admitted to the vault, the response carries aretire_previous_owner_writersnext action.
Pending transfer: 72h TTL; owner-side mutations return 409 PROJECT_HAS_PENDING_TRANSFER with details.transfer_id + cancel next_actions[]. Data-plane and payment routes keep serving; transfer cancel unblocked. After accept, rotate inherited secrets; secrets_rotation_advised clears after every inherited name is rewritten.
KyGit vault: moves with the project after the handover above. Nothing forces the previous owner out of it; when ready, run run402 repos access retire-previous-owner (owner + step-up), which protects only what is written afterwards. A project whose vault keeps its source in your own bucket (BYO) is not transferable (409 TRANSFER_VAULT_BYO_UNSUPPORTED). A --to-org move requires your own key to already hold the source (TRANSFER_VAULT_HANDOVER_PENDING with run402 repos access sync otherwise).
Does not transfer: tier lease (stays with original org; no proration), KMS signers, GitHub repo ownership, on-chain balances. Wallet transfers only support --billing-policy migrate; if recipient lacks active org, accept -> 409 RECIPIENT_ORGANIZATION_NOT_ACTIVE. Email/owned-org always migrate; do not pass --billing-policy.