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.
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 doctorAccepting#
npx calm audit --accept✔ 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:
$ 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:
✔ security.snapshot.json already matches who may do what: nothing to accept.Access that only got narrower is accepted without a reason.
| Option | Meaning |
|---|---|
--accept | Write 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:
✖ 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:
{
"$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) oroff(the action is turned off).fields: per field, who mayreadandwriteit (yes,no,never...), andpii: truefor personal data.accepted: who accepted, when, why, and which cells it widened. The last 50 entries are kept. The first snapshot gets the reasonfirst snapshotwhen 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 doctorreports wider access as an error, socalm doctor --cifails: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 auditends with the comparison, andcalm audit --cifails on wider access as well as on failed checks. Without a snapshot it saysNo 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 --jsonhas the result underbudget. -
The devtools Doctor panel shows the same error while you work.
A file that is not valid JSON is an error too.
In CI#
npx calm doctor --ci
MONGODB_TEST_URI=mongodb://127.0.0.1:27017 npx calm audit --cicalm 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.