The check
What mainsdk check verifies, the four layers of code safety, exceptions, exit codes, and how to run it in CI with JSON and SARIF output.
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.
mainsdk check is the gate between your agent and main. It runs every registered gate against your code and main.yaml, blocks drift from what main.yaml promises, and blocks anything below the baseline. Every finding carries a stable code, the file and line, and the fix, so your agent can act on it without you.
The check is free, runs on your machine and in your own CI, and works offline.
$ npx mainsdk check
mainsdk check · pinecart · env dev · Main 27
secrets 1 finding
MAIN-SECRET-0002 critical app/lib/pay.ts:3
fix: Move the key to secrets and use secret:STRIPE_KEY
config policy network data
1 gate failed · exit 1What it verifies
| Area | Examples of what fails |
|---|---|
| Secrets | A key in source, a committed .env, a secret in the browser bundle, a literal in a secret field of main.yaml |
| Chokepoints | A raw database client, raw fetch, axios or node:net outside MainSDK, a storage SDK used directly instead of files |
| Data | A query across regions, an unlabelled personal field, a pan field, SQL built from strings |
| Access | Relations your access driver can't evaluate, an engine version with a known flaw |
| Network | A call to a host not in network.allow or a driver manifest |
| Sign-in | Tokens in local storage, an admin role without phishing-resistant sign-in |
| Privacy | A data label with no retention rule |
| Production safety | A local driver (SQLite, local secrets, local keys) resolved for prod |
| Messaging | email.marketing with no unsubscribe link, open or click tracking left on, a real email driver in prod with no bounce webhook, a local SMS or WhatsApp channel in prod, an invalid email or notify section |
| Escape hatches | Every raw() use, listed so you know where you bypass guarantees |
| Main 27 | Every baseline rule not covered by an open exception |
| Config | Schema errors, overlays that weaken a parent, every overlay combination in use |
Each interface registers its own gates and check codes; its page lists them, for example secrets. The full list is in check codes.
Layers of code safety
MainSDK checks every change on four layers, and each catches what the one before it cannot:
| Layer | What it checks | When it runs |
|---|---|---|
| Rules | Deterministic checks: secrets, chokepoints, types, Main 27 rules, bundled linters and dependency checks | As the agent writes, in milliseconds |
| Intent | AI reviewers ask whether the change does what was asked. Free on your own agent; hosted reviewers are a paid Cloud run | As the agent writes |
| Outcomes | Locked flows in a headless browser at desktop and phone widths, a second user trying to read your data, accessibility | On every change before main |
| Proof | Signed builds, SBOM, provenance and the Main mark | On every change before main |
Reviewers for intent use a different model or prompt from the coding agent, so the agent never grades its own work.
Adopting without breaking production
New runtime rules, such as network permissions and policies, can start in observe (log only), move to warn, then enforce, so turning MainSDK on never blocks live traffic by surprise. The modes are explained in Policy as code.
Exceptions
A finding you accept goes in main.yaml with a rule, a reason, an owner and an expiry date. The check passes with the exception recorded, the Main mark shows "Main 27 · 1 exception", and the day after it expires the check fails again. The same list grants a layer the right to weaken an inherited setting; see main.yaml.
exceptions:
- rule: MAIN-NET-0004
reason: Partner gateway upgrade booked for November
owner: platform-team
expires: 2026-11-30Exit codes
| Code | Meaning |
|---|---|
0 | Passed |
1 | A gate failed |
2 | Usage or configuration error, with a MAIN-CONFIG- code |
In CI
The check writes versioned JSON and SARIF 2.1.0, so code scanning shows findings inline on the pull request.
name: mainsdk
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
permissions: { contents: read, security-events: write }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npx mainsdk check --env prod --sarif mainsdk.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: mainsdk.sarif }// 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" } ] }] }Run with --env prod in CI so local drivers that are fine on your machine fail before production (MAIN-CHECK-0026). --fail-on <severity> sets the lowest severity that fails the run.
Reading a code
Codes look like MAIN-<AREA>-<NNNN>. A code is assigned once and never changes meaning. Explain any code or trace offline (the format of errors your app returns is in Errors):
$ npx mainsdk explain MAIN-DATA-0007
MAIN-DATA-0007 Record belongs to another user (403)
cause orders.get was called for an order owned by a different user.
rule data.owner_scope
fix Scope the query with db.orders.for(session.user).
docs https://mainsdk.dev/errors/MAIN-DATA-0007Next
Was this page helpful?