MainSDK
Search⌘K
Docs

CLI

Every mainsdk command and flag the specs define, with exit codes, an example, and the milestone each one ships in.

Planned for 0.2

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.
Stability levels
Updated 2026-10-28

The mainsdk CLI lives in your repo and works with no account. Run it with npx mainsdk, pnpm mainsdk or bunx mainsdk.

npx mainsdk check

No 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

CommandWhat it doesMilestone
--help, -hPrints the command list, like mainsdk alone0.2
initWrites main.yaml, a project ID, agent rules and the MCP server entry0.2
addAdds one interface with a local driver and a test0.3
doctorDiagnoses your machine and project offline0.3
syncPlans changes to main.yaml in plain words, then opens a pull request0.2
checkRuns every gate and every overlay combination in use0.2
privacy labelsPrints the privacy labels (web, manifest, App Store, Play) the code and main.yaml generate0.4
discoverLists what your code uses: hosts, folders, server facts and programs0.3
infoShows everything about an app, yours or a running one0.2
fixApplies the machine fix of a finding as a diff; a fix that widens a grant waits for a person0.5
nextSays the one next action for an agent in under 300 tokens0.5
typesWrites .mainsdk/types/main.d.ts from main.yaml0.5
mcpRuns an MCP server for coding agents: catalog, info, check, discover, explain, next, fix0.5
sealSigns the build record (mainsdk.seal.json), makes seal keys, verifies a seal and diffs the capabilities of two seals0.2
network exportWrites the allow list as Kubernetes or AWS security-group rules0.3
upgradeMoves every MainSDK package to a new release with codemods and a pull request, and checks for security releases0.5
licence verify, licence showChecks an app licence file offline and prints its claims, chain and state0.4
logsReads the local event store (--security for security events); --env prod tails production from the Cloud0.2 (local); 0.6 (Cloud)
debugRaises detail for one user, request or release for up to 60 minutes with no redeploy0.6
replayRe-runs a failed request from its trace on fake drivers, with no outbound call0.6
triageGroups errors, opens GitHub issues that name the release, commit, file and line, and opens gated fix pull requests0.6
incidentSwitches off a target, sets read-only mode or signs everyone out, with a reason; reads the controls in force0.4
alerts testSends a test alert through every driver of a recipient0.4
devRuns your app offline on a local driver for every interface0.2
explainExplains an error code, a trace or the resolved config0.1–0.2
statusShows the state of every remote dependency0.4
errorsLists error groups in the local event store, newest first0.2
signalsPrints the signals the app recorded, newest first, with count and age0.4
neighboursShows this app's host bus settings, its socket and the last host signals; rotate-key writes a new host key0.4
appsLists the MainSDK apps on this machine from the registry folder, flags broken seals, stale and duplicate records, and lists declared actions0.4
contextShows the shared context: group, purposes, share-with, sinks, and consent and flow counts per purpose, with no person in them0.4
fleetReads Fleet records: push log with inclusion proofs, breach scope, forensic timeline, evidence bundles and deploy admission1.4
connectorsPings each configured GRC, privacy, vulnerability and ticket connector without writing, and pushes an assure run1.2
ops mcpRuns a stdio MCP server for a running app's ops tools: status, signals, norms, playbooks, plans and approval-only runs0.4
playbooksLists and plans rules, lists runs, and approves, declines or reverts one0.4
normsShows what the app has learned: each metric's bucket, count, p50, p99 and learning state0.4
ejectReplaces MainSDK calls with plain provider code0.3
adoptBrings an existing app onto MainSDK one pull request at a time0.5
docsReads the docs of the installed version, offline, and serves them to agents over MCP0.5
contract diffFails on a breaking contract change not released as a new major0.1
contract recordRefreshes the recorded getqr.id calls that the offline contract test replays0.3
loginDevice login; adds the project to your Cloud account0.6
db migrateRuns your migrations once per region store under a lock, with a lock timeout and retries0.3
share webhooksRegisters or lists the getqr.id webhook endpoints of the app0.4
webhooksLists inbound deliveries recorded under dev and replays one0.4
deployWrites the platform's files from main.yaml, checks a build before it ships and diffs two environments0.4
i18nFinds message keys, writes pseudo-locales, moves messages in and out, and drafts translations0.4
migrateMaps a provider's calls to interfaces and opens a pull request0.7
assureChecks the app against standards and packs, with owners and fixes0.7

init

Creates the project. Works offline and with no account.

text
mainsdk init [--preset minimal|indie|saas|agency] [--from <id>] [--new-id] [--force]
FlagMeaning
--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-idReplace the project ID, for a fork or a copy
--forceRun 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.yaml with schema: 1, a project ID (a UUIDv7 shown as prj_…), overlays.order and policy.placement: server.
  • .mainsdk/ for local state, added to .gitignore.
  • A MainSDK section in AGENTS.md and CLAUDE.md, between markers, appended to files you already have.
  • The MCP server entry for the coding agent it detects.
text
$ 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 sync

With --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).

text
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.

text
$ npx mainsdk add auth
mainsdk add · auth
  updated   main.yaml (auth, local driver)
  wrote     test/auth.test.ts
  check     no new critical finding

The 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).

text
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.

text
mainsdk sync [--plan] [--no-pr]
FlagMeaning
--planPrint the plan and change nothing
--no-prLeave 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.

text
$ 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 opened

check

Runs every registered gate and resolves and tests every overlay combination in use. Gates are explained in the check.

text
mainsdk check [--env <name>] [--json [--agent]] [--sarif <file>] [--fail-on <severity>]
FlagMeaning
--env <name>Resolve for an environment, for example prod
--jsonPrint the versioned JSON result on stdout
--agentWith --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-baselinePrint 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
--mapsPrint 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 codeMeaning
0Passed
1A gate failed
2Usage 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:

text
$ 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
jsonc
// 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.

text
mainsdk discover [--env <name>] [--json] [--out <file>]
FlagMeaning
--env <name>Resolve the declared lists for an environment. The default is dev
--jsonPrint 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.

text
mainsdk privacy labels [--format web|manifest|app-store|play] [--manifest <file>] [--env <name>]
FlagMeaning
--format webOne line per source, as text. The default
--format manifestJSON: { version: 1, labels: [{ source, text }] }
--format app-storeThe App Store privacy label: data linked to the user by Apple's data types, processors, hosts and retention
--format playThe 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.

text
mainsdk network export --to k8s|aws-sg [--discovered] [--env <name>]
FlagMeaning
--to k8sA Kubernetes NetworkPolicy. Names are resolved to /32 or /128 addresses first
--to aws-sgAWS security-group egress rules
--discoveredAlso 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.

text
mainsdk licence verify [<file> | -] [--json] [--strict] [--at <RFC 3339>] [--env <name>] [--machine <id>] [--root ed25519:... --product <p>]
mainsdk licence show [<file> | -] [--json] [--machine]
FlagMeaning
<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
--jsonPrint { status, reason, scope, licence, chain, revocation, graceEndsAt }
--strictverify 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 codeMeaning
0valid, or grace (it prints the days left)
1expired, revoked, suspended, tampered or missing, or grace with --strict
2Usage 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.

bash
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 licence

upgrade

Moves an app to a new MainSDK release. See Upgrades and Upgrade MainSDK.

text
mainsdk upgrade [--to <version> | --to latest] [--plan] [--json]
mainsdk upgrade --check [--json]
FlagMeaning
--to <version>The target; latest (the default) is the registry's dist-tag. An exact version the CLI already has notes for plans offline
--planPrint the plan and write nothing
--jsonPrint the plan, or the check result, as JSON
--checkExit 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 codeMeaning
0Done, nothing to upgrade, or the check found no security release
1A guard or step failed (MAIN-UPGRADE-0001, -0004, -0006), or --check found a security release
2Usage 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.

text
mainsdk info [<url>] [--json]
text
$ 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   none

info 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.

text
$ 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-actions

A 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.

text
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 flagMeaning
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 --sigstoreWrites 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 keygenWrites 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 sensitiveseal 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.

text
$ 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).

text
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.

text
$ 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:

text
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.

text
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.

text
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).

text
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.

text
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.

bash
npx mainsdk dev app.ts
npx mainsdk dev -- next dev

With -- <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.

text
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.

text
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.

text
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.

text
mainsdk mcp [--allow-write]

explain

Explains an error code, a trace or the resolved config. Works offline from the local registry and event store.

text
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:

text
$ 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.

text
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.

ExitMeaning
0Done, or status printed
2Bad 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).

text
mainsdk alerts test <recipient> [--env prod] [--json]
ExitMeaning
0Every driver sent it
1At least one driver failed
2Bad usage or an invalid alerts section

status

Lists every remote dependency, MainSDK services and each configured driver, with its state and since when.

text
mainsdk status [--json]
text
$ 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     ok

norms

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.

text
mainsdk norms [--json] [--env <name>]
text
$ npx mainsdk norms
norms     hour-of-week in Asia/Kolkata, one value per 1m
requests        all     n 0      p50 0.000 p99 0.000  learning

analytics

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.

text
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]
text
$ 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.

text
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).

text
mainsdk signals [--min <severity>] [--name <glob>] [--json] [--follow]
text
$ npx mainsdk signals --min medium
4m    high     app.import_failed        import:nightly           instance x3

playbooks

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.

text
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.

text
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.

text
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.

text
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).

text
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.

text
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.

text
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.

text
mainsdk ops mcp [--app <dir>] [--apps <dir>]

eject

Writes plain provider code in place of MainSDK calls. Offline, plan first.

text
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.

text
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 steps

adopt 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.

text
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.read

A 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.

text
mainsdk contract diff [--base <git-ref | release>] [--json]
Exit codeMeaning
0No change, or only additive changes in a minor
1A 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.

text
mainsdk contract record --vendor getqr [--key secret:<NAME>] [--examples] [--json]
FlagMeaning
--vendor getqrThe 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
--examplesBuild 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 codeMeaning
0Recordings written
1The pin does not match, or an answer breaks the pinned schema; nothing written
2Unknown vendor, a live key, or a key written on the command line

login, logout and keys

Milestone 0.6, with MainSDK Cloud.

text
mainsdk login [--region us|eu|in] [--new-id] [--upload|--no-upload]
mainsdk logout
mainsdk keys create --env dev|stage|prod [--json]
mainsdk init --new-id

login 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.

text
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.

text
mainsdk db migrate [--plan] [--json] [--env <name>]
text
$ 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.

text
mainsdk share webhooks add <url> [--events a,b] [--env <name>] [--json]
mainsdk share webhooks list [--env <name>] [--json]
text
$ 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
  • --events takes a comma list. Without it the command uses share.webhooks.events in main.yaml, then every event type (share.created, share.opened, share.expired, share.removed, share.reported, upload.scanned, placement.scanned, chat.started, and the rest the share page lists). An unknown event is refused.
  • The signing secret is printed once and written nowhere. Put it in your secret store.
  • list prints 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.

text
mainsdk webhooks list [--json]
mainsdk webhooks replay <id> [--same-id] [--json]
text
$ 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 200
  • replay sends a new delivery id unless --same-id repeats 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 with MAIN-HOOK-0012 and exit 2. replay exits 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).

text
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]
text
$ 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 overlay
  • config owns only the lines between # mainsdk:generated:begin and # mainsdk:generated:end and, in vercel.json, the keys regions, crons and buildCommand. --check writes nothing and exits 1 on a stale or hand-edited block (MAIN-DEPLOY-0010); mainsdk check reports the same. A Next.js app on Docker or Fly without output: "standalone" stops with MAIN-DEPLOY-0005.
  • preflight runs in the platform build: required env var names present (MAIN-DEPLOY-0003, names only), then the region and plan-limit rules, then mainsdk check --env <env>. It exits 1 on any failure.
  • diff lists changed config, required env names, schedules, regions and grants between two environments. With --fail-on widen it exits 1 with MAIN-DEPLOY-0012 for each change that widens the target.

i18n

Spec i18n. Works on locales/*.yaml (or i18n.dir). Nothing here calls MainSDK Cloud.

text
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>]
  • extract lists the keys used in code and those missing from the default locale. Exit code 1 when any are missing.
  • pseudo writes the pseudo-locales from the default locale's messages.
  • export writes one interchange file (stdout without --out). import reads one, takes the format from the file name unless --format is given, and stores nothing if a message fails MAIN-I18N-0003.
  • draft asks the app's ai interface for missing messages and holds them in locales/.drafts/ for a person to approve. It never approves anything.

migrate

Milestone 0.7.

text
mainsdk migrate <from> [--report-only] [--json]
  <from>: supabase | firebase | clerk | auth0 | authjs | better-auth | stripe | raw

It 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.

text
$ 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 mapping

connectors

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.

text
mainsdk connectors test [--json]
mainsdk assure --standard soc2 --push grc --tickets --owner web-team

assure

Milestone 0.7. Running it and seeing results is free.

text
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]
text
$ 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 owners map in main.yaml, else unowned), 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; --diff shows only failures new since the last release (or --against <run>), and --fail-on exits 1 at or above that level. With neither, any failure exits 1.
  • --fix opens one pull request for mechanical fixes (retention, the admin MFA setting, change control, key rotation) and lists the judgement calls. The MCP tool assure.fix lists 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 verify checks it; it reads verified when the signer is pinned under seal.signers and signed otherwise, and a changed byte is MAIN-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 with MAIN-ASSURE-0003 signature invalid.
  • --push grc sends results to a GRC connector (milestone 1.2). --fleet <tenant> --assign runs 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.

text
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 in package.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.

text
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.json files in the folder
  • --replay: recorded decisions as JSON lines, to list every decision the policy would flip
  • --engine: cedar or rego, 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.

text
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.

text
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.

text
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 range publish runs conformance for, such as email@^1.2
  • --region <r>, --capability <list>: search lists 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.

text
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.

text
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.

text
mainsdk cbom [--env <name>] [--out <file>] [--json]
  • --env <name>: the environment to read (default dev)
  • --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.

text
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.

text
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.

text
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.

text
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.

text
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.

text
mainsdk help [command]
mainsdk --help
mainsdk -h

mainsdk --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

CommandWhat it doesSpecMilestone
mainsdk test policy [--recorded <dir>] [--json] (test policy)Policy unit tests and replay of recorded decisionspolicy0.2
mainsdk test accessPolicy and model tests for accessaccess0.3
mainsdk run <action>Runs an action from the terminalmcp-actions0.3
mainsdk check (deps gate)Reads the lockfile, the registry and OSV.dev; reports MAIN-DEPS-0001 to -0008; see Dependency checksdependency-check0.5
mainsdk network export --to vercel|k8s|aws-sg|cloudflareWrites the network allowlist as platform rules. Today k8s and aws-sg are built; Vercel and Cloudflare are plannednetwork0.3
mainsdk api openapi [--out <file>] [--include <glob>…]Writes the OpenAPI 3.1 document for the app's actionsapp-api0.3
mainsdk api client --out <file.ts> [--include <glob>…]Writes the typed client, one file with no importsapp-api0.3
mainsdk api diff [--base <git-ref> | --against <file>] [--json]Lists breaking changes against the committed snapshotapp-api0.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 numberseed-preview0.4
mainsdk preview up|down|list|prune [--branch <git branch>] [--json]Creates, removes, lists and prunes one preview database per git branchseed-preview0.4
mainsdk analytics --query active --window <day|week|month> [--by <dim>] [--json]Runs a built-in analytics query from the terminal; replaces mainsdk statsanalytics0.3
mainsdk privacy labels --format web|app-store|play|manifestGenerates privacy labels from data labels and driversprivacy-lifecycle0.3
mainsdk driver test <path> [--interface <id@ver>] [--json]Runs the conformance suite on a drivertesting-kit0.4
mainsdk debug start --user <id> | --request <trace> | --release <v> [--for <dur>], mainsdk debug stop <session>Debug sessions, 60 minutes at most (Teams)debug-loop0.6
mainsdk replay <trace-id>Re-runs a request locally on fake driversdebug-loop0.6
mainsdk triage connect githubInstalls the GitHub app for triagedebug-loop0.6
mainsdk usage, mainsdk billing cardEvents used, grace left, card and monthly limitusage-billing0.6
mainsdk policy play [<dir>] [--request <file>] [--entities <file>]Policy playground in the terminalpolicy-playground0.7
mainsdk org create, mainsdk org invite <email> --role <role>Teams organisationsteams-plan0.7
mainsdk project transfer <prj> --to <org> [--keep-maintainer] [--bill-addons-to client]Agency handoverteams-plan0.7
mainsdk verify person|org --domain <d> --github <org>Verified developer checksverified-developers0.7
mainsdk driver publish, driver search <interface> [--region] [--capability], driver info <name>Publish and find drivers in the marketplacedriver-marketplace1.2
mainsdk connectors testDry run against each configured connectorconnectors1.2
mainsdk drill restore --at <time> --to scratchRestore drill with signed evidencebackup-restore-evidence1.2
mainsdk fleet …Fleet tenant, inventory, evidence and responsefleet-*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?

↑↓ moveEnter openEsc close

Not decided yet