---
title: "Seal your build and prove what runs"
description: "Sign one record of what your build is, serve it at /.well-known/mainsdk.json, check the files at boot, and review new sensitive capabilities before a release."
section: "guides"
status: "Planned for 0.2"
spec: "seal"
canonical: "https://mainsdk.dev/docs/guides/seal-your-build"
updated: "2026-10-28"
---

# Seal your build and prove what runs

> Sign one record of what your build is, serve it at /.well-known/mainsdk.json, check the files at boot, and review new sensitive capabilities before a release.

Status: Planned for 0.2 · Canonical: https://mainsdk.dev/docs/guides/seal-your-build · Updated: 2026-10-28

Your build will carry one signed record of what it is: a hash of exactly the files that ship, every MainSDK call the code makes and which of them are sensitive, what `main.yaml` grants, its SBOM and its check result. The app serves the record at `/.well-known/mainsdk.json`, checks its own files against it at boot, and anyone can verify it with `mainsdk info <url>`. It takes about 15 minutes.

> **Note:** The seal is in 0.2 (spec `seal`) and runs from the repository. Sign with a local ed25519 key, or keyless in CI with Sigstore (step 5); the CLI verifies Sigstore bundles itself, offline. Sealing, serving and verifying are free.

## You need

- A MainSDK app with a `main.yaml` and a lockfile (`pnpm-lock.yaml` or `package-lock.json`).
- A place outside git for the private key, such as your CI secrets.

## 1. Make a key and pin it

```bash
npx mainsdk seal keygen --out ~/.keys/orders-seal.pem
```

The private key is written with mode 0600 and the public key is printed as `ed25519:<base64>`. Pin it, so only this key's seals count as verified:

```yaml title="main.yaml"
seal:
  mode: warn                      # off | warn | refuse
  include: [.next/standalone, .next/static, public]
  exclude: ["**/*.map"]
  signers:
    - key: ed25519:MCowBQYDK2VwAyEA…
```

Never put the private key in `main.yaml` or in the repository.

## 2. Seal at build time

Run it after the build, in the folder that holds `main.yaml`:

```bash
npx mainsdk seal --key ~/.keys/orders-seal.pem
```

It writes `mainsdk.seal.json` (the served document), `mainsdk.seal.files` (the file manifest the boot check names files from) and `mainsdk.sbom.cdx.json` (CycloneDX 1.6). Ship all three with the build. If the record would hold a secret, an email address or the value of an environment variable, nothing is written and the command fails with `MAIN-SEAL-0003` naming the field.

## 3. Serve it and check at boot

Every adapter serves `mainsdk.seal.json` unchanged at `GET /.well-known/mainsdk.json`; you write no route. At boot the app recomputes the build hash and reports `app.info().seal`. In `warn` mode a mismatch is logged once as `MAIN-SEAL-0001` with the changed paths, after the app is up. In `refuse` mode the app stops before it listens; use it for long-running servers, since it reads every shipped file at each start. Keep the mode on in production: `mainsdk check --env prod` reports `MAIN-SEAL-0006` for `seal.mode: off`.

## 4. Review capabilities before a release

```bash
npx mainsdk seal diff https://orders.acme.dev ./ --key ed25519:MCowBQYDK2VwAyEA… --fail-on sensitive
```

It lists new sensitive kinds, new sensitive calls (payments, AI spend, programs, cross-tenant access and more), widened grants and new destructive actions as `MAIN-SEAL-0008`, before everything else. In CI, `--fail-on sensitive` stops the release until someone has looked.

## Check it worked

Run `npx mainsdk seal verify .`; it prints `verified` and exits 0. Change one shipped file and run it again: it exits 1 with `MAIN-SEAL-0007` and names the file. Against the running app, `npx mainsdk info https://orders.acme.dev --key ed25519:…` prints `verified` with the project, version, commit and capabilities. See [`mainsdk seal`](https://mainsdk.dev/docs/reference/cli#seal).

## 5. Sign keyless in CI with Sigstore

Keyless signing needs your CI's OIDC identity, so the CLI never signs by itself. It writes the statement, your CI signs it, and the CLI checks what comes back:

```bash
npx mainsdk seal --sigstore
cosign attest-blob --statement .mainsdk/seal.statement.json --bundle seal.bundle.json
npx mainsdk seal --sigstore --bundle seal.bundle.json --trust-root ./trusted_root.json
```

Keep a copy of Sigstore's `trusted_root.json` in your repository and pass it as `--trust-root`; the CLI fetches nothing. Pin who may sign in `main.yaml`:

```yaml title="main.yaml"
seal:
  signers:
    - sigstore:
        issuer: https://token.actions.githubusercontent.com
        identity: https://github.com/acme/orders/.github/workflows/release.yml@refs/heads/release
```

```bash
npx mainsdk seal verify . --trust-root ./trusted_root.json
npx mainsdk seal verify . --trust-root ./trusted_root.json --identity "^https://github.com/acme/orders/" --issuer https://token.actions.githubusercontent.com
```

`verified` means the chain, the Rekor entry and the signer all check out. A bundle that is genuine but signed by someone else is `signed` (`MAIN-SEAL-0004`); a bundle that doesn't verify fails with `MAIN-INFO-0002` and the reason. Add `--provenance <bundle>` when sealing to carry an SLSA provenance bundle from a hardened builder. The running app does not check the chain: it reports the signer as `unverified-at-runtime`, so verify from outside with `mainsdk info <url> --trust-root …`.
