---
title: "The check"
description: "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."
section: "concepts"
status: "Planned for 0.2"
spec: "cli-core"
canonical: "https://mainsdk.dev/docs/concepts/the-check"
updated: "2026-10-28"
---

# 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: Planned for 0.2 · Canonical: https://mainsdk.dev/docs/concepts/the-check · Updated: 2026-10-28

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

```text
$ 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 1
```

## What 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](https://mainsdk.dev/docs/concepts/main27) | 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](https://mainsdk.dev/docs/interfaces/secrets). The full list is in [check codes](https://mainsdk.dev/docs/reference/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](https://mainsdk.dev/docs/concepts/policy#observe-warn-enforce-then-roll-out).

## 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](https://mainsdk.dev/docs/concepts/main27) 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](https://mainsdk.dev/docs/concepts/main-yaml).

```yaml title="main.yaml"
exceptions:
  - rule: MAIN-NET-0004
    reason: Partner gateway upgrade booked for November
    owner: platform-team
    expires: 2026-11-30
```

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

```yaml title=".github/workflows/mainsdk.yml"
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 }
```

```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" } ] }] }
```

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.

> **Tip:** Gates in your own CI are free on every plan. Hosted reviewers and PR checks from MainSDK Cloud are what Teams adds. See [Free in the app, paid in the Cloud](https://mainsdk.dev/docs/concepts/free-and-cloud).

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

```text
$ 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-0007
```

## Next

- [Your first check](https://mainsdk.dev/docs/getting-started/first-check)
- [Check codes](https://mainsdk.dev/docs/reference/check-codes)
- [Main 27 and the Main mark](https://mainsdk.dev/docs/concepts/main27)
