docs

Exposure budget

The exposure matrix says who may do what in your API right now. The exposure budget writes it down: calm audit --accept saves the matrix to security.snapshot.json, you commit it, and from then on any access wider than the file is an error, in CI and in your editor's terminal, until a person accepts it again and says why.

Terminal
npx calm audit --accept                                   # the first snapshot
npx calm audit --accept --reason "Products are public for the storefront"
npx calm audit --ci                                       # fails when access is wider than the snapshot
npx calm doctor --ci                                      # so does doctor

Accepting#

Terminal
npx calm audit --accept
Text
✔ security.snapshot.json written: 2 resources, 4 personas, accepted by Ada Lovelace. Commit it.

Accepting boots the app the way calm routes does, without a database, reads the exposure matrix and writes it to security.snapshot.json at the project root. Commit the file.

When access has widened since the snapshot, accepting needs a reason, which is recorded next to your name:

Text
$ npx calm audit --accept
✖ Access widens in 2 places: say why with --reason "..." (it is recorded next to your name)

$ npx calm audit --accept --reason "Visitors can browse posts before they sign up"
! Accepted 2 wider cells:
    post list anonymous: no -> yes
    post.title read anonymous: no -> yes
✔ security.snapshot.json written: 2 resources, 4 personas, accepted by Ada Lovelace. Commit it.

Accepting again when nothing changed writes nothing and adds nothing to the log, so the log stays a record of real decisions:

Text
✔ security.snapshot.json already matches who may do what: nothing to accept.

Access that only got narrower is accepted without a reason.

OptionMeaning
--acceptWrite the current matrix to security.snapshot.json
--reason <why>Why access changed. Required when it widened since the last snapshot.
--as "Name <email>"Accept as this person instead of your git user
--json{ file, unchanged, wider, accepted }

The person is your git user: GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL, else git config user.name and user.email, unless you pass --as. Like signing a review, accepting refuses to run in an AI agent's shell:

Text
✖ Accepting wider access is for a person, and this shell belongs to an AI agent (claude-code). Run it in your own terminal.

security.snapshot.json#

The file is the plain-text answer to "who can see what", for you, a reviewer or an auditor:

JSON
{
  "$comment": "Who may do what in this API. Generated by calm audit --accept; CI fails when access widens beyond it. Commit it.",
  "version": 1,
  "updatedAt": "2026-10-13T09:40:02.118Z",
  "matrix": {
    "personas": ["anonymous", "user", "owner", "admin"],
    "resources": [
      {
        "name": "post",
        "path": "/posts",
        "actions": {
          "list": { "anonymous": "allow", "user": "allow", "owner": "allow", "admin": "allow" },
          "read": { "anonymous": "allow", "user": "allow", "owner": "allow", "admin": "allow" },
          "create": { "anonymous": "deny", "user": "allow", "owner": "allow", "admin": "allow" },
          "update": { "anonymous": "deny", "user": "deny", "owner": "allow", "admin": "allow" },
          "delete": { "anonymous": "deny", "user": "deny", "owner": "allow", "admin": "allow" }
        },
        "fields": {
          "title": {
            "read": { "anonymous": "yes", "user": "yes", "owner": "yes", "admin": "yes" },
            "write": { "anonymous": "yes", "user": "yes", "owner": "yes", "admin": "yes" }
          },
          "slug": {
            "read": { "anonymous": "yes", "user": "yes", "owner": "yes", "admin": "yes" },
            "write": { "anonymous": "never", "user": "never", "owner": "never", "admin": "never" }
          }
        }
      }
    ]
  },
  "accepted": [
    { "by": { "name": "Ada Lovelace", "email": "ada@example.com" }, "at": "2026-10-12T16:02:44.310Z", "reason": "first snapshot", "widened": [] },
    {
      "by": { "name": "Ada Lovelace", "email": "ada@example.com" },
      "at": "2026-10-13T09:40:02.118Z",
      "reason": "Visitors can browse posts before they sign up",
      "widened": ["post list anonymous: no -> yes", "post.title read anonymous: no -> yes"]
    }
  ]
}

(Shortened: a real file has every resource and every field.)

  • matrix.personas: anonymous, a signed-in user, the owner, each custom role, admin.
  • actions: per action and persona, allow, own (only their own records), deny, custom (a policy function) or off (the action is turned off).
  • fields: per field, who may read and write it (yes, no, never...), and pii: true for personal data.
  • accepted: who accepted, when, why, and which cells it widened. The last 50 entries are kept. The first snapshot gets the reason first snapshot when you give none.

What counts as wider#

Each cell (post list anonymous, user.email read user) is compared with the snapshot. A cell is wider when it allows more than before: deny or no to own, own to allow or yes, a cell that did not exist (a new resource, field or role) that allows anything, and a cell that becomes custom (a policy function, whose effect cannot be read from the file). Each one is reported as a line like post list anonymous: no -> yes or invoice.total read user: new -> yes.

Narrower cells are counted but never fail anything.

Where it is checked#

As soon as security.snapshot.json exists:

  • calm doctor reports wider access as an error, so calm doctor --ci fails:

    Text
      ✖ error  Access widened beyond security.snapshot.json in 2 places  security.snapshot.json
               post list anonymous: no -> yes
               post.title read anonymous: no -> yes
               If it is intended: npx calm audit --accept --reason "why". Otherwise fix the policy or field rule.
  • calm audit ends with the comparison, and calm audit --ci fails on wider access as well as on failed checks. Without a snapshot it says No exposure budget yet. Write one with calm audit --accept, commit security.snapshot.json, and CI will fail when access widens. Within it: ✔ Within the exposure budget (security.snapshot.json). calm audit --json has the result under budget.

  • The devtools Doctor panel shows the same error while you work.

A file that is not valid JSON is an error too.

In CI#

Terminal
npx calm doctor --ci
MONGODB_TEST_URI=mongodb://127.0.0.1:27017 npx calm audit --ci

calm doctor --ci needs no database. When a pull request widens access on purpose, a person runs calm audit --accept --reason "..." and commits security.snapshot.json with the change; the reason and their name stay in the file's history.

The exposure budget, calm review --ci and calm diff --ci catch wider access from three sides: a committed list of who may do what, the last signed review, and a git ref.

Last updated