---
title: "CLI"
description: "Every mainsdk command and flag the specs define, with exit codes, an example, and the milestone each one ships in."
section: "reference"
status: "Planned for 0.2"
spec: "cli-core"
canonical: "https://mainsdk.dev/docs/reference/cli"
updated: "2026-10-28"
---

# CLI

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

Status: Planned for 0.2 · Canonical: https://mainsdk.dev/docs/reference/cli · 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`.

npm:

```bash
npx mainsdk check
```

pnpm:

```bash
pnpm mainsdk check
```

bun:

```bash
bunx 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

| Command | What it does | Milestone |
| --- | --- | --- |
| [`--help`, `-h`](#help) | Prints the command list, like `mainsdk` alone | 0.2 |
| [`init`](#init) | Writes `main.yaml`, a project ID, agent rules and the MCP server entry | 0.2 |
| [`add`](#add) | Adds one interface with a local driver and a test | 0.3 |
| [`doctor`](#doctor) | Diagnoses your machine and project offline | 0.3 |
| [`sync`](#sync) | Plans changes to `main.yaml` in plain words, then opens a pull request | 0.2 |
| [`check`](#check) | Runs every gate and every overlay combination in use | 0.2 |
| [`privacy labels`](#privacy-labels) | Prints the privacy labels (web, manifest, App Store, Play) the code and `main.yaml` generate | 0.4 |
| [`discover`](#discover) | Lists what your code uses: hosts, folders, server facts and programs | 0.3 |
| [`info`](#info) | Shows everything about an app, yours or a running one | 0.2 |
| [`fix`](#fix) | Applies the machine fix of a finding as a diff; a fix that widens a grant waits for a person | 0.5 |
| [`next`](#next) | Says the one next action for an agent in under 300 tokens | 0.5 |
| [`types`](#types) | Writes `.mainsdk/types/main.d.ts` from `main.yaml` | 0.5 |
| [`mcp`](#mcp) | Runs an MCP server for coding agents: catalog, info, check, discover, explain, next, fix | 0.5 |
| [`seal`](#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`](#network-export) | Writes the allow list as Kubernetes or AWS security-group rules | 0.3 |
| [`upgrade`](#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`](#licence) | Checks an app licence file offline and prints its claims, chain and state | 0.4 |
| [`logs`](#logs) | Reads the local event store (`--security` for security events); `--env prod` tails production from the Cloud | 0.2 (local); 0.6 (Cloud) |
| [`debug`](#debug) | Raises detail for one user, request or release for up to 60 minutes with no redeploy | 0.6 |
| [`replay`](#replay) | Re-runs a failed request from its trace on fake drivers, with no outbound call | 0.6 |
| [`triage`](#triage) | Groups errors, opens GitHub issues that name the release, commit, file and line, and opens gated fix pull requests | 0.6 |
| [`incident`](#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`](#alerts-test) | Sends a test alert through every driver of a recipient | 0.4 |
| [`dev`](#dev) | Runs your app offline on a local driver for every interface | 0.2 |
| [`explain`](#explain) | Explains an error code, a trace or the resolved config | 0.1–0.2 |
| [`status`](#status) | Shows the state of every remote dependency | 0.4 |
| [`errors`](#errors) | Lists error groups in the local event store, newest first | 0.2 |
| [`signals`](#signals) | Prints the signals the app recorded, newest first, with count and age | 0.4 |
| [`neighbours`](#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`](#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`](#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`](#fleet) | Reads Fleet records: push log with inclusion proofs, breach scope, forensic timeline, evidence bundles and deploy admission | 1.4 |
| [`connectors`](#connectors) | Pings each configured GRC, privacy, vulnerability and ticket connector without writing, and pushes an assure run | 1.2 |
| [`ops mcp`](#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`](#playbooks) | Lists and plans rules, lists runs, and approves, declines or reverts one | 0.4 |
| [`norms`](#norms) | Shows what the app has learned: each metric's bucket, count, p50, p99 and learning state | 0.4 |
| [`eject`](#eject) | Replaces MainSDK calls with plain provider code | 0.3 |
| [`adopt`](#adopt) | Brings an existing app onto MainSDK one pull request at a time | 0.5 |
| [`docs`](#docs) | Reads the docs of the installed version, offline, and serves them to agents over MCP | 0.5 |
| [`contract diff`](#contract-diff) | Fails on a breaking contract change not released as a new major | 0.1 |
| [`contract record`](#contract-record) | Refreshes the recorded getqr.id calls that the offline contract test replays | 0.3 |
| [`login`](#login-logout-and-keys) | Device login; adds the project to your Cloud account | 0.6 |
| [`db migrate`](#db-migrate) | Runs your migrations once per region store under a lock, with a lock timeout and retries | 0.3 |
| [`share webhooks`](#share) | Registers or lists the getqr.id webhook endpoints of the app | 0.4 |
| [`webhooks`](#webhooks) | Lists inbound deliveries recorded under `dev` and replays one | 0.4 |
| [`deploy`](#deploy) | Writes the platform's files from `main.yaml`, checks a build before it ships and diffs two environments | 0.4 |
| [`i18n`](#i18n) | Finds message keys, writes pseudo-locales, moves messages in and out, and drafts translations | 0.4 |
| [`migrate`](#migrate) | Maps a provider's calls to interfaces and opens a pull request | 0.7 |
| [`assure`](#assure) | Checks the app against standards and packs, with owners and fixes | 0.7 |

## init

Creates the project. Works offline and with no account.

```text
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.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 [TBD].

### 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]
```

| 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](https://mainsdk.dev/docs/guides/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](https://mainsdk.dev/docs/concepts/the-check).

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

```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](https://mainsdk.dev/docs/reference/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>]
```

| 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](https://mainsdk.dev/docs/guides/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>]
```

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

```text
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](https://mainsdk.dev/docs/interfaces/licence).

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

```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](https://mainsdk.dev/docs/interfaces/upgrade) and [Upgrade MainSDK](https://mainsdk.dev/docs/guides/upgrade-mainsdk).

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

```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`](#seal) 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 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`.

```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](https://mainsdk.dev/docs/reference/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](https://mainsdk.dev/docs/guides/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](https://mainsdk.dev/docs/guides/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`](#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.

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

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

```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](https://mainsdk.dev/docs/guides/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](https://mainsdk.dev/docs/guides/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](https://mainsdk.dev/docs/guides/see-your-whole-estate).

```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](https://mainsdk.dev/docs/guides/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](https://mainsdk.dev/docs/getting-started/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 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](https://mainsdk.dev/docs/specs/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]
```

| 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 `example`s 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.

```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](https://mainsdk.dev/docs/reference/environment).

## usage and billing

Spec [usage-billing](https://mainsdk.dev/docs/specs/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](https://mainsdk.dev/docs/interfaces/db).

## share

Spec [files-share](https://mainsdk.dev/docs/specs/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](https://mainsdk.dev/docs/specs/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](https://mainsdk.dev/docs/specs/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](https://mainsdk.dev/docs/adr/0072-mainsdk-writes-and-checks-deploy-config-and-never-deploys-or-hosts-the-app)).

```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](https://mainsdk.dev/docs/specs/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](https://mainsdk.dev/docs/guides/prepare-for-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

| 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](https://mainsdk.dev/docs/concepts/actions-and-mcp) 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](https://mainsdk.dev/docs/interfaces/deps) | 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](https://mainsdk.dev/docs/guides/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](https://mainsdk.dev/docs/guides/deploy-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

- [main.yaml keys](https://mainsdk.dev/docs/reference/main-yaml)
- [Check codes](https://mainsdk.dev/docs/reference/check-codes)
- [Your first check](https://mainsdk.dev/docs/getting-started/first-check)
