CLI
Every mainsdk command and flag the specs define, with exit codes, an example, and the milestone each one ships in.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
The mainsdk CLI lives in your repo and works with no account. Run it with npx mainsdk, pnpm mainsdk or bunx mainsdk.
npx mainsdk checkpnpm mainsdk checkbunx mainsdk checkNo command calls MainSDK Cloud before you run mainsdk login. Each command below names the milestone of the spec that defines it, for example "0.2". Commands from milestone 0.6 on need the Cloud or ship after the public beta.
Commands at a glance
| Command | What it does | Milestone |
|---|---|---|
--help, -h | Prints the command list, like mainsdk alone | 0.2 |
init | Writes main.yaml, a project ID, agent rules and the MCP server entry | 0.2 |
add | Adds one interface with a local driver and a test | 0.3 |
doctor | Diagnoses your machine and project offline | 0.3 |
sync | Plans changes to main.yaml in plain words, then opens a pull request | 0.2 |
check | Runs every gate and every overlay combination in use | 0.2 |
privacy labels | Prints the privacy labels (web, manifest, App Store, Play) the code and main.yaml generate | 0.4 |
discover | Lists what your code uses: hosts, folders, server facts and programs | 0.3 |
info | Shows everything about an app, yours or a running one | 0.2 |
fix | Applies the machine fix of a finding as a diff; a fix that widens a grant waits for a person | 0.5 |
next | Says the one next action for an agent in under 300 tokens | 0.5 |
types | Writes .mainsdk/types/main.d.ts from main.yaml | 0.5 |
mcp | Runs an MCP server for coding agents: catalog, info, check, discover, explain, next, fix | 0.5 |
seal | Signs the build record (mainsdk.seal.json), makes seal keys, verifies a seal and diffs the capabilities of two seals | 0.2 |
network export | Writes the allow list as Kubernetes or AWS security-group rules | 0.3 |
upgrade | Moves every MainSDK package to a new release with codemods and a pull request, and checks for security releases | 0.5 |
licence verify, licence show | Checks an app licence file offline and prints its claims, chain and state | 0.4 |
logs | Reads the local event store (--security for security events); --env prod tails production from the Cloud | 0.2 (local); 0.6 (Cloud) |
debug | Raises detail for one user, request or release for up to 60 minutes with no redeploy | 0.6 |
replay | Re-runs a failed request from its trace on fake drivers, with no outbound call | 0.6 |
triage | Groups errors, opens GitHub issues that name the release, commit, file and line, and opens gated fix pull requests | 0.6 |
incident | Switches off a target, sets read-only mode or signs everyone out, with a reason; reads the controls in force | 0.4 |
alerts test | Sends a test alert through every driver of a recipient | 0.4 |
dev | Runs your app offline on a local driver for every interface | 0.2 |
explain | Explains an error code, a trace or the resolved config | 0.1–0.2 |
status | Shows the state of every remote dependency | 0.4 |
errors | Lists error groups in the local event store, newest first | 0.2 |
signals | Prints the signals the app recorded, newest first, with count and age | 0.4 |
neighbours | Shows this app's host bus settings, its socket and the last host signals; rotate-key writes a new host key | 0.4 |
apps | Lists the MainSDK apps on this machine from the registry folder, flags broken seals, stale and duplicate records, and lists declared actions | 0.4 |
context | Shows the shared context: group, purposes, share-with, sinks, and consent and flow counts per purpose, with no person in them | 0.4 |
fleet | Reads Fleet records: push log with inclusion proofs, breach scope, forensic timeline, evidence bundles and deploy admission | 1.4 |
connectors | Pings each configured GRC, privacy, vulnerability and ticket connector without writing, and pushes an assure run | 1.2 |
ops mcp | Runs a stdio MCP server for a running app's ops tools: status, signals, norms, playbooks, plans and approval-only runs | 0.4 |
playbooks | Lists and plans rules, lists runs, and approves, declines or reverts one | 0.4 |
norms | Shows what the app has learned: each metric's bucket, count, p50, p99 and learning state | 0.4 |
eject | Replaces MainSDK calls with plain provider code | 0.3 |
adopt | Brings an existing app onto MainSDK one pull request at a time | 0.5 |
docs | Reads the docs of the installed version, offline, and serves them to agents over MCP | 0.5 |
contract diff | Fails on a breaking contract change not released as a new major | 0.1 |
contract record | Refreshes the recorded getqr.id calls that the offline contract test replays | 0.3 |
login | Device login; adds the project to your Cloud account | 0.6 |
db migrate | Runs your migrations once per region store under a lock, with a lock timeout and retries | 0.3 |
share webhooks | Registers or lists the getqr.id webhook endpoints of the app | 0.4 |
webhooks | Lists inbound deliveries recorded under dev and replays one | 0.4 |
deploy | Writes the platform's files from main.yaml, checks a build before it ships and diffs two environments | 0.4 |
i18n | Finds message keys, writes pseudo-locales, moves messages in and out, and drafts translations | 0.4 |
migrate | Maps a provider's calls to interfaces and opens a pull request | 0.7 |
assure | Checks the app against standards and packs, with owners and fixes | 0.7 |
init
Creates the project. Works offline and with no account.
mainsdk init [--preset minimal|indie|saas|agency] [--from <id>] [--new-id] [--force]| Flag | Meaning |
|---|---|
--preset <name> | Choose the starting main.yaml: minimal (the four required keys), indie (the default), saas or agency |
--from <id> | Start from a description made on mainsdk.dev or by your agent |
--new-id | Replace the project ID, for a fork or a copy |
--force | Run in a repo that already has main.yaml |
--template <name> | Start from a reference architecture: mainsdk/saas-web, mainsdk/api-service, mainsdk/payments-checkout, mainsdk/ai-agent (milestone 0.7) |
What it writes:
main.yamlwithschema: 1, a project ID (a UUIDv7 shown asprj_…),overlays.orderandpolicy.placement: server..mainsdk/for local state, added to.gitignore.- A MainSDK section in
AGENTS.mdandCLAUDE.md, between markers, appended to files you already have. - The MCP server entry for the coding agent it detects.
$ npx mainsdk init
mainsdk init · pinecart
project prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY
wrote main.yaml
wrote AGENTS.md, CLAUDE.md (MainSDK section)
updated .gitignore (.mainsdk/)
mcp registered for Claude Code
Next: edit main.yaml, then run mainsdk syncWith --preset minimal, init writes only schema, project, baseline and overlays.order, in at most ten lines. The default indie preset adds auth, db and files on their local drivers, logging and a deny-by-default network in observe mode; saas and agency build on it. init asks no question when it runs without a terminal. A preset only chooses keys: you can widen or tighten any of them later, and mainsdk explain config says why each default is there. The saas preset adds passkey and email sign-in methods; agency turns on the audit log.
Running init again in the same repo exits 2 unless you pass --new-id or --force.
add
Adds one interface to an existing project with its local driver and one example test, then runs check. It changes nothing outside the files it reports. Planned for milestone 0.3 (spec fast-start).
mainsdk add auth|db|ai|pay|...It works for each interface once its local driver ships: config, secrets, policy and log at milestone 0.2, and auth, db and ai at 0.3. add refuses to weaken a key that is already set and says which command raises the guarantee.
$ npx mainsdk add auth
mainsdk add · auth
updated main.yaml (auth, local driver)
wrote test/auth.test.ts
check no new critical findingThe output above shows the shape; the exact wording is not decided yet.
add recipe
mainsdk add recipe <name> adds a worked pattern to the project: main.yaml keys on local drivers, code in src/recipes/<name>.ts, its test and a note in docs/recipes/<name>.md, together. The recipes are stripe-subscriptions, api-keys-and-plans, invite-teammates, agent-with-approval and memory-and-tools. A target file that exists with other text fails with MAIN-DEV-0002 and nothing is written; --dry-run lists the changes first. Init also writes the agent context: .claude/skills/mainsdk/SKILL.md, .cursor/rules/mainsdk.mdc and a marked section of AGENTS.md, and sync refreshes only the marked text.
doctor
Reports the state of your machine and project in one run: Node, git, lockfile, environment, drivers and network. It runs offline, sends no telemetry and names the fix for every problem. Spec fast-start (milestone 0.3).
mainsdk doctor [--json]With Node 20 and no lockfile, mainsdk doctor --json reports two problems, each with a fix, and exits non-zero. The JSON report is { version: 1, passed, problems: [{ id, message, fix }], notes }. The problems are node (older than 22.18), lockfile, main-yaml, gitignore (.mainsdk/ not ignored) and env (a .env that git would track). notes carry facts that do not fail the run: whether the folder is in git, the declared drivers and the network mode.
sync
Makes the code match main.yaml. It shows the plan first, resolves driver versions into main.lock, applies the change on a new branch, runs check and opens a pull request.
mainsdk sync [--plan] [--no-pr]| Flag | Meaning |
|---|---|
--plan | Print the plan and change nothing |
--no-pr | Leave the branch; don't open a pull request |
--package <path>, --all, --affected --base <ref> | Monorepo scope (milestone 0.7). See monorepos |
With no git remote or no GitHub access, sync leaves the branch and prints the plan.
sync also installs what policy.placement needs: for server in a Next.js app the route app/api/mainsdk/decide/route.ts, for edge the entry mainsdk.edge.ts, and for browser the config mainsdk.browser.ts (with policy.browser.local: true, also mainsdk.policy-engine.ts, the lazily loaded Cedar engine chunk). Change the placement and run sync --plan to see files added and the old placement's files removed. sync never overwrites a file it did not write.
$ npx mainsdk sync
Plan
+ email driver resend (email@1.4) because features lists email
~ main.lock resend 2.3.1 sha256:9f2c…
Branch mainsdk/sync-email · check passed · pull request openedcheck
Runs every registered gate and resolves and tests every overlay combination in use. Gates are explained in the check.
mainsdk check [--env <name>] [--json [--agent]] [--sarif <file>] [--fail-on <severity>]| Flag | Meaning |
|---|---|
--env <name> | Resolve for an environment, for example prod |
--json | Print the versioned JSON result on stdout |
--agent | With --json: output for a repair loop. Every finding carries its fix and file, and a finding still open after the third run carries ask: person and no more fix text. The count of unresolved runs lives in .mainsdk/agent-loop.json and the report's agent.maxTries says how many runs an agent gets (3) |
--sarif <file> | Also write SARIF 2.1.0 for code scanning: one result per finding with its file and line. The --json document is described by packages/cli/schema/check-report.schema.json |
--fail-on <severity> | Fail only at or above this severity |
--explain-baseline | Print the Main 27 pack: its generation, every rule with its area and standards mapping, and the bill of contracts. With --json, generation and contracts are keys of the document |
--maps | Print the standards each finding maps to (ASVS, OWASP Top 10:2025, SOC 2, ISO 27001) under it; --json always carries them. A mapping helps a review and is not certification |
--package <path>, --all, --affected --base <ref> | Monorepo scope (milestone 0.7) |
| Exit code | Meaning |
|---|---|
| 0 | Passed |
| 1 | A gate failed |
| 2 | Usage or configuration error, with a MAIN-CONFIG- code |
The engagement gate (spec engagement) awaits the copy rules before the report is built. Its model pass runs only with check.ai: true and an ai driver; when it was skipped, the text report prints a note: line and the JSON document carries notes. The feeds and imports gates report MAIN-FEED-0010 to 0013 and MAIN-IMPORT-0020.
The config gate loads main.yaml with the selected environment plus each market, segment and target overlay file in the folder, one at a time, and names the overlay file and line of any combination that fails. The text report prints the failing findings first, each with its code and fix.
check --env prod fails with MAIN-CHECK-0026 if any interface resolves to a local driver:
$ npx mainsdk check --env prod
mainsdk check · pinecart · env prod · Main 27
secrets 1 finding
MAIN-CHECK-0026 critical main.env.prod.yaml
fix: Set secrets.driver to a key service in main.env.prod.yaml
config
policy
network
1 gate failed · exit 1// npx mainsdk check --json
{ "version": 1, "project": "prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY", "env": "prod", "passed": false,
"gates": [{ "id": "secrets", "passed": false, "findings": [
{ "code": "MAIN-SECRET-0002", "severity": "critical", "file": "app/lib/pay.ts", "line": 3,
"rule": "secrets.no_literal", "fix": "Move the key to secrets and use secret:STRIPE_KEY" } ] }] }The schema gate validates main.yaml against the JSON Schema the mainsdk package ships (mainsdk/main.schema.json, built from the catalog and the key tables). An unknown top-level key fails the run with MAIN-CONFIG-0030 and names the nearest key. An unknown nested key, or a value of the wrong type (MAIN-CONFIG-0032), is reported as a warning with its path and a fix. mainsdk init writes the schema's path on the first line of main.yaml, so your editor flags the same keys. If the schema is not built, the gate warns with MAIN-CONFIG-0033 and validates nothing.
Every code is listed in check codes.
discover
Reads your source and lists what the app uses outside itself: the hosts it calls, the folders it reads and writes, the server facts it asks for and the programs it runs. Each item says whether main.yaml declares it. It runs offline and changes nothing.
mainsdk discover [--env <name>] [--json] [--out <file>]| Flag | Meaning |
|---|---|
--env <name> | Resolve the declared lists for an environment. The default is dev |
--json | Print the result as JSON |
--out <file> | Write the JSON to a file and print its path |
The JSON manifest is version 2: its calls section lists every call on a MainSDK interface the code makes (for example files.uploadUrl or system.run), with its interface, its sensitive kind or false, and every file and line. A call whose name is computed at run time is listed under dynamic. Use it to fill network.allow and permissions from what the code really does. See declare what your app uses.
The manifest also carries notify.engagement when the app declares it: the guardrail values, whether the strict rules and guardian controls apply, and each journey with its rungs, read from journey() and ladder() in the code.
privacy labels
Prints the privacy labels generated from what the code and main.yaml really do, never written by hand. web and manifest list one line per interface that makes a promise to a person (engagement is the first). app-store and play are the store forms, built from the labelled db.fields, the third-party drivers in main.yaml (listed as processors), the retention rules and, with --manifest, the hosts, system facts and programs a mainsdk discover --out manifest found.
mainsdk privacy labels [--format web|manifest|app-store|play] [--manifest <file>] [--env <name>]| Flag | Meaning |
|---|---|
--format web | One line per source, as text. The default |
--format manifest | JSON: { version: 1, labels: [{ source, text }] } |
--format app-store | The App Store privacy label: data linked to the user by Apple's data types, processors, hosts and retention |
--format play | The Play Data safety answers: data collected by Google's categories, processors, hosts and retention |
--manifest <file> | A manifest from mainsdk discover --out <file>, for the app-store and play forms |
Exit 2 for a format that does not exist or a manifest that cannot be read.
network export
Writes the network.allow list as egress rules for the platform that runs the app, so the platform blocks what the SDK would block.
mainsdk network export --to k8s|aws-sg [--discovered] [--env <name>]| Flag | Meaning |
|---|---|
--to k8s | A Kubernetes NetworkPolicy. Names are resolved to /32 or /128 addresses first |
--to aws-sg | AWS security-group egress rules |
--discovered | Also include the undeclared hosts that discover finds in your code |
--env <name> | Read the allow list for an environment. The default is dev |
Vercel and Cloudflare exports are planned. Any other --to value exits 2.
licence
Checks an app licence the way the app does, offline, and prints what is in it. Neither command calls the network. See App licensing.
mainsdk licence verify [<file> | -] [--json] [--strict] [--at <RFC 3339>] [--env <name>] [--machine <id>] [--root ed25519:... --product <p>]
mainsdk licence show [<file> | -] [--json] [--machine]| Flag | Meaning |
|---|---|
<file> or - | The licence file (JSON or compact mlic1. form), or standard input. Without it, both commands read licence.file or licence.token from main.yaml |
--json | Print { status, reason, scope, licence, chain, revocation, graceEndsAt } |
--strict | verify only: a licence in grace exits 1 |
--at <RFC 3339> | verify only: judge the licence at that time instead of now |
--env <name> | verify only: the environment the licence is checked in. The default is dev |
--machine <id> | verify only: the machine id a machine-bound licence is checked against |
--root ... --product <p> | Verify without a main.yaml: pin this root for this product |
--machine (on show) | Print this machine's id and nothing else |
| Exit code | Meaning |
|---|---|
| 0 | valid, or grace (it prints the days left) |
| 1 | expired, revoked, suspended, tampered or missing, or grace with --strict |
| 2 | Usage error, or no main.yaml and no --root and --product |
show prints the claims, the chain with key ids, the newest revocation list and the state. The claims of a licence that doesn't verify are printed under unverified, so read them as untrusted.
mainsdk licence verify ./main.licence --strict # in CI: fail when the licence is in grace
mainsdk licence show --machine # the id to give your vendor for a machine-bound licenceupgrade
Moves an app to a new MainSDK release. See Upgrades and Upgrade MainSDK.
mainsdk upgrade [--to <version> | --to latest] [--plan] [--json]
mainsdk upgrade --check [--json]| Flag | Meaning |
|---|---|
--to <version> | The target; latest (the default) is the registry's dist-tag. An exact version the CLI already has notes for plans offline |
--plan | Print the plan and write nothing |
--json | Print the plan, or the check result, as JSON |
--check | Exit 1 with MAIN-UPGRADE-0005 when a release marked security is newer than the installed one; cached for 24 hours; offline it reports MAIN-UPGRADE-0004 as info and exits 0 |
--continue --from <version> | The second half, run by the newly installed CLI: key moves, codemods, sync, check, tests, commit and pull request |
| Exit code | Meaning |
|---|---|
| 0 | Done, nothing to upgrade, or the check found no security release |
| 1 | A guard or step failed (MAIN-UPGRADE-0001, -0004, -0006), or --check found a security release |
| 2 | Usage error |
info
Prints everything about an app: name, version and Main mark, project ID, targets, interfaces and drivers, network destinations, data labels and regions, and open exceptions. Pass a URL to read and verify a running app's signed /.well-known/mainsdk.json.
mainsdk info [<url>] [--json]$ npx mainsdk info
Pinecart 2.4.1 · Main 27 prj_01JC8Q4W… (2.4.1+main27)
Targets web
Interfaces auth (github) · ai (bedrock) · db (postgres) · policy (cedar)
Data personal: email · regions: us, eu
Exceptions noneinfo also prints an alerts line (recipients by name and driver, never their keys, the routes and the open alerts) and a security line (sinks, lowest severity sent, detector thresholds and event counts by type).
With a path to a folder holding mainsdk.seal.json (or to the JSON file), info verifies the seal and the shape of its mark against mainsdk-mark@1, and prints verified, signature, mark, state and schema; one changed byte in the record gives verified: false and exit 1. The mark is <version>+main27 with state pass, exception (an open exception is in use), lapsed (an exception expired and its finding is open) or fail.
With a URL, info fetches <url>/.well-known/mainsdk.json over HTTPS only and verifies it as seal verify does. --key <pub> pins the signer; without it a record that verifies is shown as signed, not verified (MAIN-SEAL-0004). Every field shown comes from the signed payload, never from the document around it.
$ npx mainsdk info https://orders.acme.dev --key ed25519:MCowBQYDK2VwAyEA…
verified signer ed25519:4f0c…
project prj_01JC8Q…
app Acme Orders 2.4.1 (owner acme)
build sha256:9b1e… 1834 files, commit 4f2c1e9…, built by github-actionsA record whose signature fails exits 1 with MAIN-INFO-0002 signature does not verify. Unverified fields are never shown as verified.
seal
Signs one record of what the build is (spec seal): a hash of exactly the files that ship, what the code uses, its capability inventory, interfaces, actions, env names, check result, SBOM digest, commit and builder. It runs in the CLI with no network and is free.
mainsdk seal --key <file> [--provenance <bundle>] [--out <dir>] [--env <name>] [--json]
mainsdk seal --sigstore [--out <dir>] [--env <name>]
mainsdk seal --sigstore --bundle <file> --trust-root <file> [--provenance <bundle>] [--allow-email-identity]
mainsdk seal keygen [--out <file>] [--force]
mainsdk seal verify <dir|url> [--key <pub>] [--identity <regexp> --issuer <url>] [--trust-root <file>] [--env <name>] [--json]
mainsdk seal diff <old> <new> [--key <pub>] [--json] [--fail-on sensitive]| Command or flag | Meaning |
|---|---|
seal --key <file> | Sign with a local ed25519 key (PKCS#8 PEM). Writes mainsdk.seal.json, mainsdk.seal.files (the file manifest) and mainsdk.sbom.cdx.json (CycloneDX 1.6) next to main.yaml, or in --out |
seal --sigstore | Writes the statement to .mainsdk/seal.statement.json (and the file manifest and SBOM) for your CI to sign keyless, for example with cosign attest-blob --statement. No seal is written yet: the CLI holds no OIDC credentials and calls neither Fulcio nor Rekor |
--sigstore --bundle <file> | Verifies the CI's Sigstore bundle against --trust-root and against the statement, then writes it into mainsdk.seal.json. An email identity is refused unless --allow-email-identity, because Rekor publishes the certificate |
--provenance <bundle> | Verifies an SLSA provenance bundle whose subject is the build hash and carries it in the seal document as provenance |
--trust-root <file> | The pinned Sigstore trusted_root.json (Fulcio authorities and Rekor keys). Nothing is fetched; a bundle can't be checked without it |
--identity <regexp> --issuer <url> | The signer a bundle must have: the certificate's SAN against the pattern and its OIDC issuer exactly. Without them seal verify uses the sigstore entries of seal.signers |
--env <name> | The environment whose main.yaml is sealed. The default is MAINSDK_ENV, else dev |
seal keygen | Writes an ed25519 private key with mode 0600 (default mainsdk-seal.pem) and prints the public key in the seal.signers form, ed25519:<base64> |
seal verify <dir|url> | Prints verified, signed (MAIN-SEAL-0004) or fails with MAIN-INFO-0002. For a folder it also recomputes the build hash and fails with MAIN-SEAL-0007 naming the files that differ. Exits 0 only when verified |
seal diff <old> <new> | Compares two seals (files, folders or HTTPS URLs, each verified first): calls, sensitive kinds, grants and actions added, removed or changed, sensitive changes first as MAIN-SEAL-0008 |
--fail-on sensitive | seal diff exits 1 when any MAIN-SEAL-0008 is reported |
Before signing, seal refuses a record that would carry a secret, an email address or the value of an environment variable present at build (MAIN-SEAL-0003), and a file it can't hash (MAIN-SEAL-0005). The record holds env var names only, never values, and no commit author. A Sigstore bundle is verified offline by the CLI's own verifier on node:crypto, with no dependency: certificate chain to the pinned trust root, the Rekor signed entry timestamp, the inclusion proof and signed checkpoint, the certificate's validity at the integration time, then the signer. A bundle that fails any step is invalid (MAIN-INFO-0002, with the reason); one that verifies for another signer is signed (MAIN-SEAL-0004). It does not check RFC 3161 timestamps or the certificate's SCT. The app itself never verifies a bundle chain: at run time it checks the files and reports the signer as unverified-at-runtime.
$ npx mainsdk seal keygen --out ~/.keys/orders-seal.pem
ed25519:MCowBQYDK2VwAyEA…
$ npx mainsdk seal --key ~/.keys/orders-seal.pem
sealed sha256:9b1e… 1834 files
$ npx mainsdk seal verify .
verified signer ed25519:4f0c…Every adapter serves mainsdk.seal.json unchanged at GET /.well-known/mainsdk.json; the file manifest is never served, and the SBOM only with seal.publish.sbom: true. At boot the app recomputes the build hash and sets app.info().seal; see the seal keys in main.yaml.
logs
Reads the local event store in .mainsdk/events/ (git-ignored, 7 days).
mainsdk logs [--follow] [--level <lvl>] [--trace <id>] [--security] [--json]--security shows only security events (spec security-events), one line each with type, severity and message. With --json it prints the event itself: OCSF fields, ATT&CK tags, actor and app.
$ npx mainsdk logs --level error --trace 4bf92f3577b34da6a3ce929d0e0e4736
2026-11-03T10:14:02Z error MAIN-DATA-0007 Record belongs to another user
app/api/orders/[id]/route.ts:18 · rule data.owner_scope
fix: Scope the query with db.orders.for(user).Live tail from MainSDK Cloud is free. It reads the project's production records from its region, asks for a fresh passkey first, and with --follow asks again every 2 seconds, so each record shows within 5 seconds of being written:
mainsdk logs --follow --env prod [--user <id>] [--trace <id>] [--level error] [--json]See debugging.
debug
Raises detail for one user, request or release for up to 60 minutes with no redeploy (spec debug-loop; Teams). The app reads the active sessions and keeps below-level records only for that target; the session switches itself off and its start and end are audited.
mainsdk debug start --user <id> | --request <trace-id> | --release <version> [--for 30m]
mainsdk debug stop <session-id>--for 90m is cut to 60 minutes and the answer says so. Give exactly one target, or the Cloud answers MAIN-DEBUG-0002. On Free the Cloud answers HTTP 402 with a link to Teams (MAIN-DEBUG-0001, exit 3) and the app is unchanged. Exit codes: 0 done, 1 the Cloud refused, 2 usage or no login, 3 needs Teams.
replay
Re-runs a failed request from its trace on the test app with a fake driver for every interface (Teams). A call that tries to reach the network is blocked and counted, so a replay never touches a real provider.
mainsdk replay <trace-id> [--handlers <file>] [--file <trace.json>] [--json]The handlers file, mainsdk.replay.ts by default, exports a function for each action by name. Exit 0 means the same error code came back; 1 means it did not. See debugging.
triage
Groups the project's errors, finds the release and commit that introduced each from the Main mark, and opens a GitHub issue that names the file and line, the trace and the steps from customer reports (spec debug-loop).
mainsdk triage connect github --installation <id> [--repo owner/name]
mainsdk triage run
mainsdk triage fix <issue> --head <branch>run opens one issue for each error group that has none (hosted triage, Teams). fix opens a draft pull request from the branch you pushed and sets the mainsdk/check status to pending; it turns green only when pnpm mainsdk check --json reports "status": "pass", and the repository's own gates still apply.
dev
Runs your app on local drivers: a local driver for every interface in use, no provider calls and no account.
mainsdk dev <entry> [--port <n>] [--env dev]
mainsdk dev [-- <command>]With an entry file, dev serves its default fetch function on 127.0.0.1 (port 3000 unless --port says otherwise) and prints one line for each request. The token Bearer dev_<name> signs the request in as the test user <name>, from this machine only and only on the local auth driver, so curl -H "authorization: Bearer dev_ada" ... needs no sign-in flow. A dev_ token from another address is a bad token. --env prod is refused with MAIN-DEV-0008; a missing entry, a missing fetch export or a port in use is MAIN-DEV-0009.
With --cockpit, dev also serves a page on 127.0.0.1 (the next port up) that lists each request with its status, events and decisions and a replay button that sends it again on local drivers. It answers this machine only.
npx mainsdk dev app.ts
npx mainsdk dev -- next devWith -- <command>, dev runs your command with every interface on a local driver.
Local drivers include files for config, .mainsdk/secrets.json for secrets, Cedar for policy, the terminal for log, SQLite for db, a local inbox for email, test cards for pay and an in-process queue for jobs. check --env prod refuses them with MAIN-CHECK-0026.
fix
Applies the machine fix of a check finding (spec agent-experience). check --json --agent carries patch, yaml and widens on each finding; fix builds the same change and writes it. A finding with no mechanical fix says ask: person and why.
mainsdk fix <MAIN-CODE>|--all [--dry-run] [--allow-widen] [--json]--dry-run prints the unified diff and writes nothing. A fix that widens a grant, such as a host for network.allow, is planned but not applied: MAIN-DEV-0001, until a person reviews it and runs it with --allow-widen. Every applied fix is recorded in .mainsdk/agent-changes.jsonl. The rules with a builder are MAIN-SECRET-0002, MAIN-FILES-0010 and MAIN-NET-0007.
next
Prints the one thing an agent should do now, as kind (fix, edit, test, declare, ask-person or done), the command and a sentence, in at most 300 tokens. It looks at check findings first, then unreviewed widening changes, stale types (MAIN-DEV-0004) and missing tests. A finding still open after three runs is ask-person.
mainsdk next [--env <env>] [--json]types
Writes .mainsdk/types/main.d.ts, an interface Declared listing the hosts, events, flags, agents, secrets, features and actions main.yaml declares. next reports MAIN-DEV-0004 while the file is older than main.yaml.
mainsdk types [--json]mcp
Runs an MCP server over stdio for a coding agent. The tools are catalog.lookup (answered from the catalog packed with this CLI), project.info, check.run, discover.run, explain and next.run; each answer is cut to 800 tokens unless the call passes detail: "full". fix.apply is offered only with --allow-write; without it the call fails with MAIN-DEV-0003. Not the same as ops mcp, which talks to a running app.
mainsdk mcp [--allow-write]explain
Explains an error code, a trace or the resolved config. Works offline from the local registry and event store.
mainsdk explain <code | trace_id> [--json]
mainsdk explain config [--market <m>] [--segment <s>] [--env <e>] [--json]explain config also takes --target <t> (milestone 1.1) and --package <p> (monorepos, milestone 0.7). Every value comes with the file and line it came from:
$ npx mainsdk explain config --market in --segment enterprise
payments.driver = razorpay (main.in.yaml:1)
payments.methods = ["cards"] (main.segment.enterprise.yaml:2)With --json it prints config (the same object resolveConfig returns), sources (file and line by JSON pointer), values (one row per leaf with path, value, file and line) and the exceptions in use. A flag such as --plan pro selects a team dimension with its own main.plan.pro.yaml.
incident
Sets, clears and reads the incident controls (spec incident-controls). Every change goes through the config driver, needs --reason, writes an incident.control audit record and prints when it will have reached every instance. A control only tightens what main.yaml allows.
mainsdk incident on kill <target>... --reason "<why>" [--expires 4h] [--env prod]
mainsdk incident on read-only --reason "<why>" [--expires 4h]
mainsdk incident on sign-out [--tenant tnt_…] --reason "<why>"
mainsdk incident off kill <target> | off read-only | off sign-out [--tenant tnt_…] --reason "<why>"
mainsdk incident status [--env prod] [--json]A target is action:<name>, route:<path>, interface:<name>, driver:<id>, agent:<name> or tool:<name>. Without --reason, or with a config driver that cannot write, the command fails with MAIN-INCIDENT-0005 and prints the key and value to set by hand.
| Exit | Meaning |
|---|---|
| 0 | Done, or status printed |
| 2 | Bad usage, no reason, or the driver cannot write |
alerts test
Sends a test alert, marked as a test, through every driver of one recipient and prints each result (spec alerts).
mainsdk alerts test <recipient> [--env prod] [--json]| Exit | Meaning |
|---|---|
| 0 | Every driver sent it |
| 1 | At least one driver failed |
| 2 | Bad usage or an invalid alerts section |
status
Lists every remote dependency, MainSDK services and each configured driver, with its state and since when.
mainsdk status [--json]$ npx mainsdk status
mainsdk-cloud down since 10:02Z telemetry buffered (4,120 queued, 0 dropped)
tuf stale since 09:40Z last good: baseline 27.0.3
db:postgres oknorms
Shows each configured metric's current bucket with its value count, median, p99 and whether it is still learning (spec norms). It reads this project's model; mainsdk info shows the same under norms.
mainsdk norms [--json] [--env <name>]$ npx mainsdk norms
norms hour-of-week in Asia/Kolkata, one value per 1m
requests all n 0 p50 0.000 p99 0.000 learninganalytics
Asks the app's own analytics store a question and prints the rows, with the number of hidden small groups (spec analytics). It reads totals across tenants, so it is for the operator, and it needs builtin in analytics.to.
mainsdk analytics --query <count|active|feature|retention|funnel|metric> [--event <e>] [--window day|week|month] [--by <a,b>] [--from <date>] [--to <date>] [--steps <a,b>] [--within <duration>] [--json]$ npx mainsdk analytics --query active --window month --by country
country people
IN 412
US 187
(2 groups hidden: fewer than 10 people)See track events.
errors
Lists the error groups in the local event store, newest first, with count, first and last seen and release.
mainsdk errors [--since <duration>] [--release <r>] [--json]signals
Prints the signals the local app recorded in .mainsdk/events/, newest first, with severity, subject, scope, count and age (spec signals).
mainsdk signals [--min <severity>] [--name <glob>] [--json] [--follow]$ npx mainsdk signals --min medium
4m high app.import_failed import:nightly instance x3playbooks
Lists the rules, plans one without applying it, lists runs, and acts on a run in a running dev app through .mainsdk/control/ (spec playbooks). A revert needs --reason.
mainsdk playbooks [list] [--json]
mainsdk playbooks plan <rule> [--signal <id>]
mainsdk playbooks runs [--state <state>] [--json]
mainsdk playbooks approve <run> | decline <run> [--reason "..."]
mainsdk playbooks revert <run> --reason "..."
mainsdk playbooks unquarantine <rule>neighbours
Shows this app's host bus settings, its socket name and the last host signals recorded in .mainsdk/events/ (spec neighbours). rotate-key writes a new host key beside the apps folder.
mainsdk neighbours [--json] [--apps <dir>]
mainsdk neighbours rotate-key [--apps <dir>]apps
Lists every MainSDK app recorded in the machine's registry folder (spec app-registry): project id, app, owner, version, seal state, boot check, last seen and destinations. It reads files only and makes no connection. Flags: MAIN-INFO-0002 (broken seal), MAIN-SEAL-0001 (boot check mismatch), MAIN-APPS-0003 (stale), MAIN-APPS-0002 (duplicate project id) and MAIN-APPS-0006 (one side lists a peer that does not list it back). It exits 0, or 1 with --strict when any flag is raised.
mainsdk apps [--json] [--strict] [--apps <dir>]
mainsdk apps --actions [--risk read|write|destructive] [--json]
mainsdk apps --prune--actions lists the actions the sealed apps declare, with app, intent and risk. --prune deletes records stopped or stale for more than 30 days. The folder is --apps, then MAINSDK_APPS_DIR, then ~/.mainsdk/apps.
context
Shows the shared context of this app (spec context): the group, the identity mode, the sinks and, for each purpose, who it shares with, how long it is kept, how many people said yes and how many flows went out and came in. The counts are plain numbers the running app keeps in the run folder. It prints no context id, user id or attribute value. mainsdk info shows a one-line summary and ops.status carries the same document.
mainsdk context [--json] [--env <env>]fleet
Reads the Fleet records an app owner or a CISO needs (specs fleet-control and fleet-prove). Every command reads files only and changes nothing. log lists the pushes that reached an app with the policy hash, signers, time, blast radius and an inclusion proof it verifies; a tampered log fails with exit 1. scope names the people, labels and regions a key, user or app touched in a time window. timeline prints the hash-chained forensic timeline and the position where it breaks. evidence writes a ZIP bundle for one standard and period (MAIN-FLEET-0011 for an unknown standard). admit refuses a release that lacks a required deploy mark (MAIN-FLEET-0007). mainsdk check also fails a build that includes a blocked package (MAIN-FLEET-0006).
mainsdk fleet log --app <id> [--json]
mainsdk fleet scope [--key <id>|--user <id>|--app <id>] --from <t1> --to <t2> [--json]
mainsdk fleet timeline [--app <id>] [--from <t>] [--to <t>] [--json]
mainsdk fleet evidence --standard <soc2|iso27001|dpdp|gdpr> --period <from>..<to> --out <file.zip>
mainsdk fleet admit [--env <env>] [--marks <a,b>] [--json]The state folder is --state, then MAINSDK_FLEET_DIR, then .mainsdk/fleet. See Read your Fleet records.
The pilot commands are for MainSDK staff and need --staff <id> or MAINSDK_STAFF (MAIN-PILOT-0006 otherwise); their state is under <state>/pilots. verify prints the TXT record that proves an organisation's domain and exits 1 until it resolves (MAIN-PILOT-0001). tenant create makes a ten_ tenant in a region. pilot enrol puts a partner's project in its tenant with signed proof and an audit line (MAIN-PILOT-0003 for another organisation's project). pilot add writes the pilot register, pilot role sets the tenant's delegated key role, and pilots lists partners and the count toward the gate of 10.
mainsdk fleet verify <domain> [--org <id>] [--json]
mainsdk fleet tenant create --org <id> --region us|eu|in [--json]
mainsdk fleet pilot enrol --tenant <ten_…> --project <prj_…> [--json]
mainsdk fleet pilot add --org <id> --started <date> [--name <name>] [--paid] [--tenant <ten_…>]
mainsdk fleet pilot role --tenant <ten_…> --key <id>=<public key> [--key …] [--threshold <n>]
mainsdk fleet pilots [--json]The inventory commands (specs fleet-discover and fleet-observe) read a snapshot, <state>/visibility/<tenant>.json, and change nothing. apps lists every app with owner, environment, version, mark and last release, and coverage as verified over total. surface exports the attack surface as k8s-networkpolicy, vercel-firewall or aws-sg (MAIN-FLEET-0015 for another name). map lists data flows to providers, narrowed by --label and --region-outside. broken lists apps failing an SLO now. sbom find lists the apps and releases running a package version (MAIN-FLEET-0016 when the query is not name@version). vulns lists reachable issues ranked by KEV and EPSS. See See every app you run.
mainsdk fleet apps --tenant <ten_…> [--json]
mainsdk fleet surface --tenant <ten_…> --export <k8s-networkpolicy|vercel-firewall|aws-sg>
mainsdk fleet map --tenant <ten_…> [--label <label>] [--region-outside <a,b>] [--json]
mainsdk fleet broken --tenant <ten_…> [--json]
mainsdk fleet sbom find <name@version> --tenant <ten_…> [--json]
mainsdk fleet vulns --tenant <ten_…> [--reachable] [--kev] [--json]ops mcp
Runs a stdio MCP server for an agent such as Claude Code (spec ops-mcp). It finds the running app's ops socket and token file by the project id in main.yaml, presents the token and copies JSON-RPC lines both ways. The tools are ops.status, ops.signals, ops.norms, ops.playbooks, ops.reactions, ops.plan, ops.explain, ops.run and ops.revert; the last two only file a request that waits for a person (mainsdk playbooks approve). With no running app it exits 1 with MAIN-OPS-0005. mainsdk init writes a mainsdk-ops entry in the project MCP file of Claude Code, Cursor and Gemini CLI.
mainsdk ops mcp [--app <dir>] [--apps <dir>]eject
Writes plain provider code in place of MainSDK calls. Offline, plan first.
mainsdk eject <interface> [--driver <name>] [--dry-run] [--json]
mainsdk eject --all [--dry-run] [--json]--dry-run prints every file and interface and changes nothing. Recipes cover email (Resend, SES), pay (Stripe, Razorpay), auth (Clerk, Better Auth) and db (node-postgres, Neon, raw SQL only). A file it has no recipe for stays as it was and is named in the report; the mainsdk dependency, main.yaml and the gates go only when no MainSDK import is left. MAIN-EJECT-0001 means no recipe for that interface or driver; MAIN-EJECT-0002 means nothing imports it.
It rewrites call sites to the configured provider's SDK, moves secret: references to environment variable reads, rewrites tests that used fakes, and writes EJECT-REPORT.md listing each guarantee you lose. See eject.
adopt
Brings an existing Next.js or Node app onto MainSDK. Runs locally, sends nothing, and is safe to re-run.
mainsdk adopt runs the next step
mainsdk adopt scan 1. writes main.yaml as the app is; no code change
mainsdk adopt report [--json] 2. gap report against Main 27: secrets, access, data, then the rest
mainsdk adopt lock 3. records key flows as locked flows
mainsdk adopt next [--module <m>] 4. one module per branch and pull request
mainsdk adopt verify after each module: only allowed files changed, no new findings, flows intact; commits it
mainsdk adopt finish [--json] 5. launch report: baseline reached, or what remains
mainsdk adopt skill [--target claude|cursor|agents] writes the agent skill that drives these steps
mainsdk adopt mcp stdio MCP server with the six adopt.* tools, for an agent to drive the stepsadopt next refuses to run before flows are locked: MAIN-ADOPT-0001 lock flows first. mainsdk adopt access --from openfga generates typed permissions from an OpenFGA model (access spec, milestone 0.3). See adopt an existing app.
docs
Reads the docs bundled with the installed MainSDK version, so an agent sees the pages that match the code it runs.
mainsdk docs --list every bundled page
mainsdk docs <page> prints one page as markdown, e.g. guides/file-uploads
mainsdk docs mcp stdio MCP server with docs.list and docs.readA source checkout builds the bundle with python3 tools/build_docs.py --bundle packages/cli/docs. An install with no bundle stops with MAIN-DOCS-0001; an unknown page stops with MAIN-DOCS-0002 and lists the closest pages.
contract diff
Compares every contract with the last release.
mainsdk contract diff [--base <git-ref | release>] [--json]| Exit code | Meaning |
|---|---|
| 0 | No change, or only additive changes in a minor |
| 1 | A breaking change not marked as a new major |
Breaking means a removed or renamed field, a narrowed type, a new required input, a changed default or a removed error code. From 1.0, --since 1.0.0 fails on any breaking 1.x change.
mainsdk contract check (milestone 0.4) fails an interface with no threat model: MAIN-CONTRACT-0021.
contract record
Spec files-share (AC31). Writes packages/core/contracts/vendor/getqr/recordings/<operationId>.json, one request and its answer for each of the 20 getqr.id operations. The contract test packages/sdk/test/contract-getqr-replay.test.ts replays them offline in tools/check fast: each request goes through httpTransport, and what it sent and what came back are checked against the schemas of the pinned openapi.yaml. Run it from a source checkout.
mainsdk contract record --vendor getqr [--key secret:<NAME>] [--examples] [--json]| Flag | Meaning |
|---|---|
--vendor getqr | The vendored contract to record. Only getqr has a recording plan |
--key secret:<NAME> | The environment variable that holds a gq_test_ sandbox key. Default secret:GETQR_SANDBOX_KEY. Pass the reference, never the key |
--examples | Build from the contract's examples even when a key is set |
With a gq_test_ key the command calls each operation once against sandbox.api.getqr.id. Without one it builds the recordings from the 38 examples in the pinned file, with no network. Either way each recording is checked against the pinned schemas first, and nothing is written if one fails (MAIN-CONTRACT-0031). A gq_live_ key, or a key typed on the command line, is refused with exit 2; the key is never printed, logged or written, and a webhook signing secret in an answer is stored as whsec_redacted. It records nothing while openapi.yaml differs from its pin (MAIN-CONTRACT-0030). The sandbox run leaves one webhook endpoint behind, because the contract has no delete for it. This does not replace the stage contract test (AC17) that calls the sandbox itself.
| Exit code | Meaning |
|---|---|
| 0 | Recordings written |
| 1 | The pin does not match, or an answer breaks the pinned schema; nothing written |
| 2 | Unknown vendor, a live key, or a key written on the command line |
login, logout and keys
Milestone 0.6, with MainSDK Cloud.
mainsdk login [--region us|eu|in] [--new-id] [--upload|--no-upload]
mainsdk logout
mainsdk keys create --env dev|stage|prod [--json]
mainsdk init --new-idlogin uses the device flow (RFC 8628): a short code in the terminal, approved in the browser. It claims the project ID from main.yaml; no key is typed or pasted. If the ID is claimed under another git remote (MAIN-CLOUD-0009), it offers a new one: accepting (or --new-id) writes a fresh UUIDv7 to main.yaml. If .mainsdk/ holds events, it offers to upload the last 7 days (--upload accepts, --no-upload skips; with no terminal it asks nothing and sends nothing). The session is kept through a keychain interface; the default today is a file at ~/.mainsdk/session.json, mode 0600 (not an OS keychain), which logout deletes. logout stops all sending at once; every check keeps working locally. keys create issues one ingest key per environment: msk_test_… for dev and stage, msk_live_… for prod (a passkey sign-in, else MAIN-CLOUD-0010). The key is shown once and is never logged or stored. init --new-id gives a copy or fork a fresh project ID without logging in. Nothing runs on its own and nothing is sent before login. Exit codes: 0 done, 1 the Cloud refused, 2 usage or no login. See environment variables.
usage and billing
Spec usage-billing. Both need mainsdk login.
mainsdk usage [--json]
mainsdk billing card [--cap <usd>]usage prints this month's events, the percent of the allowance, the days of grace left and the monthly cap, and warns once at 80%. billing card prints the card page link for the project and starts it with --cap; the card and the cap are entered on that page, so no card data passes through the CLI. When the Cloud answers HTTP 402 (MAIN-CLOUD-0402), the command prints the code and the upgrade link and exits 3; your app is unaffected. Exit codes: 0 done, 1 the Cloud refused, 2 usage or no login, 3 needs a card or Teams.
db migrate
Milestone 0.3. Runs the app's own migrations, one region store at a time, under one Postgres advisory lock, with a lock timeout so a statement waits briefly and retries instead of queueing every request behind it. MainSDK is not a migration tool: Prisma and Drizzle run as your own commands (prisma migrate deploy, drizzle-kit migrate), and a folder of numbered .sql files is applied one file at a time.
mainsdk db migrate [--plan] [--json] [--env <name>]$ npx mainsdk db migrate --plan
plan 2 migrations, 3 statements, tool: prisma (npx prisma migrate deploy)
pending eu: 20261027_add_index, 20261027_backfill
$ npx mainsdk db migrate
lock advisory lock mainsdk:migrate:eu held · lock_timeout 5s · retries 5
applied 20261027_add_index (1.8s) · 20261027_backfill (0.4s)Settings come from db.migrations in main.yaml (tool, path, lock-timeout, retries, base, allow). A statement that times out waiting for a lock after the last retry stops the run with MAIN-DATA-0044, and nothing after it is applied. A run that waited for another instance and found nothing left to do ends with MAIN-DATA-0045. It never prints a connection string or password. Exit code 0 when done or nothing to do, 1 when a migration failed, 2 for a usage error. mainsdk check reports unsafe statements in new migrations before they run; see Data and records.
share
Spec files-share. Registers or lists the webhook endpoints that getqr.id calls for the app's events. Only share webhooks exists.
mainsdk share webhooks add <url> [--events a,b] [--env <name>] [--json]
mainsdk share webhooks list [--env <name>] [--json]$ npx mainsdk share webhooks add https://app.example.com/hooks/getqr --events share.opened,upload.scanned
Registered whk_01 for share.opened, upload.scanned
Secret (shown once, not saved anywhere): whsec_…
Store it as webhooks.inbound.getqr.secret: secret:GETQR_WEBHOOK_SECRET--eventstakes a comma list. Without it the command usesshare.webhooks.eventsinmain.yaml, then every event type (share.created,share.opened,share.expired,share.removed,share.reported,upload.scanned,placement.scanned,chat.started, and the rest thesharepage lists). An unknown event is refused.- The signing secret is printed once and written nowhere. Put it in your secret store.
listprints id, status, URL and events. Exit code 0 when done, 2 for a usage error.
webhooks
Spec webhooks-jobs. Works only under mainsdk dev: it reads the inbound deliveries the running dev server recorded and sends one again, signed with the dev replay key.
mainsdk webhooks list [--json]
mainsdk webhooks replay <id> [--same-id] [--json]$ npx mainsdk webhooks list
dlv_01 2026-10-27T10:02:11Z stripe verified
$ npx mainsdk webhooks replay dlv_01
Replayed dlv_01 as replay_8f3c…: HTTP 200replaysends a new delivery id unless--same-idrepeats the provider's id, which tests your idempotency. A delivery with no provider id refuses--same-id.- Outside
dev, with no dev server running, or for an id that was never recorded, both commands stop withMAIN-HOOK-0012and exit 2.replayexits 0 on a 2xx answer and 1 otherwise.
deploy
Spec deploy. Writes Vercel, Cloudflare, Fly and Docker files from main.yaml; it never pushes a deploy, holds a platform token or calls the network (ADR-0072).
mainsdk deploy config [--target vercel|cloudflare|fly|docker|node] [--env <env>] [--check] [--json]
mainsdk deploy preflight --env <env> [--json]
mainsdk deploy diff --from <env> --to <env> [--fail-on widen] [--json]$ npx mainsdk deploy config --env prod
written vercel.json
written deploy/env.prod.example
$ npx mainsdk deploy diff --from staging --to prod --fail-on widen
grant network.allow: (none) -> api.stripe.com widens
MAIN-DEPLOY-0012 main.env.prod.yaml:1 prod widens staging: grant network.allow api.stripe.com
fix: Review the change, then approve it, or remove it from the prod overlayconfigowns only the lines between# mainsdk:generated:beginand# mainsdk:generated:endand, invercel.json, the keysregions,cronsandbuildCommand.--checkwrites nothing and exits 1 on a stale or hand-edited block (MAIN-DEPLOY-0010);mainsdk checkreports the same. A Next.js app on Docker or Fly withoutoutput: "standalone"stops withMAIN-DEPLOY-0005.preflightruns in the platform build: required env var names present (MAIN-DEPLOY-0003, names only), then the region and plan-limit rules, thenmainsdk check --env <env>. It exits 1 on any failure.difflists changed config, required env names, schedules, regions and grants between two environments. With--fail-on widenit exits 1 withMAIN-DEPLOY-0012for each change that widens the target.
i18n
Spec i18n. Works on locales/*.yaml (or i18n.dir). Nothing here calls MainSDK Cloud.
mainsdk i18n extract [--json]
mainsdk i18n pseudo
mainsdk i18n export --format xliff|xcstrings|android|icu-json [--locale <locale>] [--out <file>]
mainsdk i18n import <file> [--format xliff|xcstrings|android|icu-json] [--locale <locale>]
mainsdk i18n draft [--locale <locale>] [--env <name>]extractlists the keys used in code and those missing from the default locale. Exit code 1 when any are missing.pseudowrites the pseudo-locales from the default locale's messages.exportwrites one interchange file (stdout without--out).importreads one, takes the format from the file name unless--formatis given, and stores nothing if a message failsMAIN-I18N-0003.draftasks the app'saiinterface for missing messages and holds them inlocales/.drafts/for a person to approve. It never approves anything.
migrate
Milestone 0.7.
mainsdk migrate <from> [--report-only] [--json]
<from>: supabase | firebase | clerk | auth0 | authjs | better-auth | stripe | rawIt reports three lists, each with file and line: calls that map to an interface, calls that stay behind raw(), and items that need a person.
$ npx mainsdk migrate stripe --report-only
mapped app/api/checkout/route.ts:12 stripe.checkout.sessions.create() → pay.checkout()
raw lib/billing.ts:40 stripe.terminal.readers.list() no pay capability: terminal
person app/api/webhooks/stripe.ts webhook handler reads raw body; review the event mappingconnectors
Sends MainSDK's evidence and findings to the tools your security team uses (spec connectors). connectors test pings each connector in main.yaml and writes nothing, and exits 1 when one fails (MAIN-CONNECTOR-0002; MAIN-CONNECTOR-0001 for an unusable setting; MAIN-SECRET-0002 for a literal auth). assure --push <grc|privacy|vulns|tickets>, --tickets and --owner push an assure run after it finishes. Every call is one line in .mainsdk/connectors-audit.jsonl, with no credential in it.
mainsdk connectors test [--json]
mainsdk assure --standard soc2 --push grc --tickets --owner web-teamassure
Milestone 0.7. Running it and seeing results is free.
mainsdk assure [--standard <ids>] [--pack <name>] [--diff] [--against <run>] [--release <name>]
[--fail-on critical|high|medium] [--fix] [--key <pem>]
[--format terminal|sarif|cyclonedx|oscal|pdf] [--out <file>] [--json]
[--tickets github|jira] [<url>]
mainsdk assure verify <bundle> [--key <public key>] [--json]$ npx mainsdk assure --standard pci-dss-4.0.1
FAIL pci-dss-4.0.1:6.4.3 (AST-02) owner web-team, fix by 2026-11-26 (high) every script on a payment page is approved: cdn.example-chat.com
FAIL pci-dss-4.0.1:8.4.1 (IAC-06) owner platform, fix by 2026-11-26 (high) auth.admin.require does not require phishing-resistant MFA
FAIL pci-dss-4.0.1:10.5.1 (MON-10) owner unowned, fix by 2027-01-25 (medium) logs are kept 3 months; 12 are needed
pass pci-dss-4.0.1:3.2.1 (DCH-01) db.fields has no field labelled pan
SAQ A scope: eligible (the pay driver uses hosted fields; no card number is stored)
This pack makes controls checkable; it is not a certification.- Controls come from the Secure Controls Framework subset in
packages/cli/data/assure/, crosswalked to 13 standards, each pinned to its version (pci-dss-4.0.1:8.4.1). Every standard is shown as beta until two reviewers, one a qualified assessor, are recorded in the mapping file. - Each failure has one owner (CODEOWNERS, then the
ownersmap inmain.yaml, elseunowned), a severity and a deadline: critical 7 days, high 30, medium 90. - An exception in
main.yaml(rule: <standard>:<control>,reason,approver,expires) shows the control as excepted until its expiry and fails it again after; every exception is in the evidence. --release <name>records a run;--diffshows only failures new since the last release (or--against <run>), and--fail-onexits 1 at or above that level. With neither, any failure exits 1.--fixopens one pull request for mechanical fixes (retention, the admin MFA setting, change control, key rotation) and lists the judgement calls. The MCP toolassure.fixlists the same two sets.- Every run writes a signed evidence bundle to
.mainsdk/assure/latest.bundle(results, SBOM, VEX, policy test results, restore drills, exceptions).assure verifychecks it; it readsverifiedwhen the signer is pinned underseal.signersandsignedotherwise, and a changed byte isMAIN-ASSURE-0004. - Pointed at a URL it runs read-only from
/.well-known/mainsdk.json: it judges what the record carries and lists the rest as not evaluated; a tampered record exits 2 withMAIN-ASSURE-0003 signature invalid. --push grcsends results to a GRC connector (milestone 1.2).--fleet <tenant> --assignruns across a Fleet tenant (milestone 1.4).
api
Write the OpenAPI document and a typed client for your routes, or diff them against a base.
mainsdk api openapi [--out <file>]
mainsdk api client --out <file.ts> [--react [--react-out <file.ts>]]
mainsdk api diff [--base <git-ref> | --against <file>] [--json]--out: the file to write--react: also write React hooks for the client next to it; needs react 18 or 19 inpackage.json, and writes nothing without it--react-out: where to write the hooks, default<out>-react.ts--base: the git ref to compare with--json: print the diff as JSON, same content
Exit: 0, done; 1, breaking changes found; 2, usage.
policy
Try a Cedar or Rego policy on a request: the decision, the lines that decided it, its tests and what a change would flip.
mainsdk policy play [policies/] --request <file> [--entities <file>] [--tests <file>] [--replay <file>] [--engine cedar|rego] [--json]--request: a JSON file with the subject, action, resource and context--entities: a JSON file with the entities the policy reads--tests: a tests file; by default the*.test.jsonfiles in the folder--replay: recorded decisions as JSON lines, to list every decision the policy would flip--engine:cedarorrego, taken from the file names when left out--json: print the result as JSON, the same result the browser playground shows
Exit: 0, every test passed; 1, a test failed; 2, usage or a file could not be read (MAIN-PLAY-0005).
canary
Plant fake records and keys that raise an alert when something reads them, and see their state.
mainsdk canary plant [--env <name>] [--model <name>]
mainsdk canary status [--env <name>] [--json]--env: the environment to use, default dev--model: plant for one model only--json: print the status as JSON, same content
Exit: 0, done; 2, usage, or canaries is not set in main.yaml.
conformance
Export the golden test vectors, or run another implementation against them.
mainsdk conformance export [--out <dir>] [--check]
mainsdk conformance run --impl "<command>" [--only <set>] [--json]--out <dir>: the folder to write the vectors to--only <set>: run one set of vectors--json: print the report as JSON, same content
Exit: 0, passed; 1, a vector failed; 2, usage.
driver
Test a driver against an interface contract, or print the matrix of drivers and interfaces.
mainsdk driver test <path> [--interface <id>@<version>] [--json]
mainsdk driver matrix [--json] [--out <file>]
mainsdk driver publish <path> [--implements <interface>@<range>] [--publisher <name>] [--json]
mainsdk driver search <interface> [--region <r>] [--capability <a,b>] [--json]
mainsdk driver info <driver> [--json]--interface <id>@<version>: the contract to test against--out <file>: write the matrix to a file--implements <claim>: the contract rangepublishruns conformance for, such asemail@^1.2--region <r>,--capability <list>:searchlists only drivers that serve the region and have every capability--json: print the result as JSON, same content
driver publish writes .mainsdk/marketplace/submission.json; MainSDK reruns the reference harness before listing. driver search and driver info read the listings from the verified TUF cache or .mainsdk/marketplace/.
Exit: 0, passed; 1, a case failed; 2, usage, or every claimed version fails, or the driver is not listed.
drill
Restore a point in time into a scratch environment, time it against backup.rto, and sign the result as evidence. drill restore is the only drill command.
mainsdk drill restore [--at <time>] [--to scratch] [--json]--at <time>: the restore point, RFC 3339 (default now)--to scratch: where to restore; only scratch is accepted, never production--json: print the result as JSON, same content
Exit: 0, the drill passed; 1, it failed RPO, RTO, verification or teardown; 2, usage or config.
pack
Check an industry pack folder and write its signed-delivery bundle with pack build.
mainsdk pack build <folder> [--out <file>]--out <file>: the bundle file (default<id>.json)
Exit: 0, the pack is valid; 1, it weakens the baseline or is not valid. mainsdk assure --standard <id> evaluates the controls a pack maps to a standard.
cbom
Write the cryptographic bill of materials (CycloneDX CBOM) for the app and list the algorithms a quantum computer breaks.
mainsdk cbom [--env <name>] [--out <file>] [--json]--env <name>: the environment to read (defaultdev)--out <file>: write the CBOM to a file, for Fleet or an auditor--json: print only the CBOM, with no summary line
Exit: 0, read. See post-quantum and FIPS.
eval
Run the eval sets of your AI features and fail when one gets worse.
mainsdk eval [feature] [--json] [--update-baseline] [--max-cost <$>]
mainsdk eval samples [feature] [--export <file>]--update-baseline: write the baseline, only when the run passes--max-cost <$>: stop before the run would pass this spend--json: print the result as JSON, same content
Exit: 0, passed; 1, below a threshold or worse than the baseline; 2, invalid set or over --max-cost.
moderate
Export statements of reasons and the transparency report of a tenant.
mainsdk moderate statements --tenant <id> --from <date> --to <date> [--format dsa-tdb] [--out <file>]
mainsdk moderate transparency --tenant <id> --from <date> --to <date> [--json]--tenant <id>: the tenant whose region and data are read--from <date>, --to <date>: the period; a bare end date includes its day--format <f>: dsa-tdb (the only one)--out <file>: write the statements to this file--json: print the transparency report as JSON
Exit: 0, done; 2, usage or config error.
preview
Run a branch as a preview with its own seeded data, and take it down when the branch is done.
mainsdk preview up [--branch <b>] [--no-seed] [--json]
mainsdk preview down [--branch <b>]
mainsdk preview list [--json]
mainsdk preview prune [--json]--branch <b>: the branch to use, default the current one--no-seed: start with an empty database--json: print the result as JSON, same content
Exit: 0, done; 2, usage, or no git branch.
seed
Fill the local database with realistic fake records, so a new screen has something to show.
mainsdk seed [--number <n>] [--tenants <n>] [--reset] [--no-ai] [--init-local] [--env <name>] [--json]--number <n>: how many records to add--tenants <n>: spread them over this many tenants--reset: remove earlier seed records first--no-ai: use templates, not a model, to write records--json: print the summary as JSON, same content
Exit: 0, done; 2, usage, or a prod environment.
telemetry
Show where telemetry goes and whether MainSDK Cloud may receive it.
mainsdk telemetry [status] [--json]--json: print the state as JSON, same content
Exit: 0, done; 2, usage or config error.
help
Prints the command list, or the usage, flags, examples and exit codes of one command.
mainsdk help [command]
mainsdk --help
mainsdk -hmainsdk --help and mainsdk -h print the same command list as mainsdk with no arguments.
On an unknown command it names the nearest one. mainsdk <command> --help prints the same page.
More commands
| Command | What it does | Spec | Milestone |
|---|---|---|---|
mainsdk test policy [--recorded <dir>] [--json] (test policy) | Policy unit tests and replay of recorded decisions | policy | 0.2 |
mainsdk test access | Policy and model tests for access | access | 0.3 |
mainsdk run <action> | Runs an action from the terminal | mcp-actions | 0.3 |
mainsdk check (deps gate) | Reads the lockfile, the registry and OSV.dev; reports MAIN-DEPS-0001 to -0008; see Dependency checks | dependency-check | 0.5 |
mainsdk network export --to vercel|k8s|aws-sg|cloudflare | Writes the network allowlist as platform rules. Today k8s and aws-sg are built; Vercel and Cloudflare are planned | network | 0.3 |
mainsdk api openapi [--out <file>] [--include <glob>…] | Writes the OpenAPI 3.1 document for the app's actions | app-api | 0.3 |
mainsdk api client --out <file.ts> [--include <glob>…] | Writes the typed client, one file with no imports | app-api | 0.3 |
mainsdk api diff [--base <git-ref> | --against <file>] [--json] | Lists breaking changes against the committed snapshot | app-api | 0.3 |
mainsdk seed [--number <n>] [--tenants <n>] [--reset] [--no-ai] [--env <name>] [--json] | Seeds a local or preview database with the same synthetic rows for the same number | seed-preview | 0.4 |
mainsdk preview up|down|list|prune [--branch <git branch>] [--json] | Creates, removes, lists and prunes one preview database per git branch | seed-preview | 0.4 |
mainsdk analytics --query active --window <day|week|month> [--by <dim>] [--json] | Runs a built-in analytics query from the terminal; replaces mainsdk stats | analytics | 0.3 |
mainsdk privacy labels --format web|app-store|play|manifest | Generates privacy labels from data labels and drivers | privacy-lifecycle | 0.3 |
mainsdk driver test <path> [--interface <id@ver>] [--json] | Runs the conformance suite on a driver | testing-kit | 0.4 |
mainsdk debug start --user <id> | --request <trace> | --release <v> [--for <dur>], mainsdk debug stop <session> | Debug sessions, 60 minutes at most (Teams) | debug-loop | 0.6 |
mainsdk replay <trace-id> | Re-runs a request locally on fake drivers | debug-loop | 0.6 |
mainsdk triage connect github | Installs the GitHub app for triage | debug-loop | 0.6 |
mainsdk usage, mainsdk billing card | Events used, grace left, card and monthly limit | usage-billing | 0.6 |
mainsdk policy play [<dir>] [--request <file>] [--entities <file>] | Policy playground in the terminal | policy-playground | 0.7 |
mainsdk org create, mainsdk org invite <email> --role <role> | Teams organisations | teams-plan | 0.7 |
mainsdk project transfer <prj> --to <org> [--keep-maintainer] [--bill-addons-to client] | Agency handover | teams-plan | 0.7 |
mainsdk verify person|org --domain <d> --github <org> | Verified developer checks | verified-developers | 0.7 |
mainsdk driver publish, driver search <interface> [--region] [--capability], driver info <name> | Publish and find drivers in the marketplace | driver-marketplace | 1.2 |
mainsdk connectors test | Dry run against each configured connector | connectors | 1.2 |
mainsdk drill restore --at <time> --to scratch | Restore drill with signed evidence | backup-restore-evidence | 1.2 |
mainsdk fleet … | Fleet tenant, inventory, evidence and response | fleet-* | 1.2–1.4 |
Named in the product requirements but in no spec yet: mainsdk swap <interface> <from> <to> (switch a driver after its tests and shadow mode; see swap a driver) and mainsdk report (launch report and trust page). mainsdk upgrade (codemods between baseline generations) comes with Main 28. There is no mainsdk deploy: you deploy wherever you do today, for example on Vercel.
Full exit-code tables exist for check and contract diff. Other commands name an exit code only for the cases shown on this page.
Next
Open questions
Was this page helpful?