Claims and providers#
Record a local review#
This sequence assumes a policy already applies
routes/no-privilege-escalation to src/routes/admin.ts. For project setup,
see the README walkthrough.
Inspect the requirement#
$ snob pending src/routes/admin.ts
Read the rule's description and review guidance. The output also shows the
subject a claim must name: the file, its content hash, the rule fingerprint
and any declared context. To get that subject as structured data, run
snob pending src/routes/admin.ts --format json and use the rule's
expected_subject field.
Review the code#
Trace every write to roles or permissions back to an authorization check, following this rule's guidance. Read the supporting files named by its context. Record what you checked and why it satisfies the requirement. Copying the expected subject identifies the content and rule; it provides no evidence that anyone reviewed them.
Save the claim by hand#
There is no command to create a claim. After completing the review, write
snob/claims/admin-esc.claim.toml with status = "satisfied", the subject
you reviewed and your evidence. Replace the example's shortened hashes with
the full values from snob pending, and include [subject.context] only
when it appears in the expected subject. Use your own review notes and
metadata in place of the example values.
# snob/claims/admin-esc.claim.toml
schema = "snob.claim.v3"
status = "satisfied"
reviewer = "reviewer@example.invalid"
reviewed_at = 2026-10-06T12:00:00Z # TOML date-time or string
evidence = """
Every role write in admin.ts goes through requireAdmin.
Discussion: https://example.invalid/review/42
"""
[subject] # from `snob pending`
path = "src/routes/admin.ts"
content_hash = "blake3:…"
rule = "routes/no-privilege-escalation"
rule_fingerprint = "blake3:…"
[subject.context] # only when the expected subject has one
patterns = ["src/auth/**"]
digest = "blake3:…"
For a deletion review, copy the subject with deleted = true and review
the removal in the resulting tree. A review of the file's content does not
approve deleting it. See reviewing a deletion.
Reference the claim#
In the file's review file, replace the rule's "changeme" entry with the
claim reference:
# snob/reviews/src/routes/admin.ts.review.toml
[claims]
"routes/no-privilege-escalation" = "local:admin-esc"
local names the provider, and admin-esc names the claim file without
.claim.toml. Keep entries for the file's other rules. When the trusted
base and checkout have different definitions of the same rule, each needs
its own claim; see review files.
Run the check#
$ snob check
snob fetches the claim and compares its subject with the expected subject.
A satisfied claim counts only when they match field for field. If the
file, rule or context changed during review, the claim is stale and the
changed subject needs review. Other unsatisfied must rules still fail
the check. See freshness and the
outcome table for the remaining states.
Anyone with repository write access can edit a local claim. Local claims suit small teams, tests and mechanical results such as a gate run, but do not authenticate the reviewer. snob checks the record's status and binding; the correctness of the review remains the reviewer's responsibility.
Local claim files#
local:<id> reads snob/claims/<id>.claim.toml; [local] claims_dir
configures the directory. Ids use the same characters as rule ids, so
local:../x is refused. A local claim requires the schema marker shown
above. reviewed_at accepts a TOML date-time or a string.
Errors in a claim file give the line and column but do not quote the file,
so claim contents are not copied into CI logs. Keep internal hostnames,
account ids and log excerpts out of evidence; link to them instead.
External providers#
Any other prefix is served by a command configured in snob.toml:
# snob.toml
[providers.review-db]
command = ["review-db", "snob-provider"]
timeout_ms = 10000
max_output_bytes = 1048576
The command runs once per check or pending, with every reference for
its prefix:
- from its argument list, with no shell. A program containing
/is relative to the project root; a bare name is looked up onPATH; - in the project root, with the inherited environment, so it can use its own credentials;
- under one deadline of
timeout_msfor writing the request, reading the response and exiting. On Unix it runs in its own process group: when it exits, anything it left running is killed, and on timeout the whole group is. A process that leaves the group (setsid) cannot be killed but cannot extend the deadline either. Elsewhere only the command itself is killed; - stdout beyond
max_output_bytesis a protocol error. The first 4 KiB of stderr is included in failure messages.
Review provider configuration like other executable code: both snob check
and snob pending run these commands. Providers fetch stored claims; snob
decides whether each claim covers the current subject.
Claim fields#
| Field | Requirement | Meaning |
|---|---|---|
status |
required | satisfied, rejected, pending or revoked |
subject |
required for satisfied |
what was reviewed, including deleted = true for a deletion; see freshness.md |
reviewer |
optional | shown in reports, never checked |
reviewed_at |
optional | shown in reports, never checked |
evidence |
optional | link to or summary of the review |
extra |
optional | provider-specific table, passed through |
Unknown fields and statuses are rejected as protocol errors, so a field
that looks like evidence cannot be silently ignored.
Protocol snob.provider.v3#
The request on stdin:
{
"protocol": "snob.provider.v3",
"provider": "review-db",
"requests": [
{
"reference": "review-db:abc123",
"id": "abc123",
"uses": [
{
"path": "src/routes/admin.ts",
"rule": "routes/no-privilege-escalation",
"level": "must",
"rule_fingerprint": "blake3:…",
"description": "A user cannot raise their own privileges.",
"review": "Trace every write to roles or permissions back to an authorization check."
}
]
}
]
}
The response on stdout, with exit status 0:
{
"protocol": "snob.provider.v3",
"results": [
{ "reference": "review-db:abc123", "claim": { "status": "satisfied", "subject": { "path": "…" } } },
{ "reference": "review-db:zzz", "error": { "kind": "not_found", "message": "no such claim" } }
]
}
Each result has exactly one of claim (the fields above, as JSON, without
schema) or error (kind is not_found, unavailable or protocol). A
missing or duplicate result, another protocol, malformed JSON, a non-zero
exit, a timeout or too much output makes the affected references
unavailable.
uses says where a reference appears, for lookup and display. A provider
must not build a subject from it: return satisfied only for a stored
review of exactly that rule and subject, and map anything weaker (a partial
read, a note, a review of other content) to another status or an error.
snob checks the returned subject either way.
uses entries for a deletion obligation carry "deleted": true. A
provider must then return only a claim that reviewed the deletion.
Versions#
snob is unreleased, and its formats have changed twice in that time, each time with a version bump so that nothing old is read with a new meaning:
| Now | Before | |
|---|---|---|
| Claim files | snob.claim.v3 |
v2: subject field downstream; v1: no deleted |
| Provider protocol | snob.provider.v3 |
same as claims |
| Reports | snob.report.v3 |
same as claims |
| Rule fingerprints | snob.rule.v2 |
v1: encoded downstream |
v2 added deleted; v3 renamed downstream to
context in subjects, policies and review files, and the rule fingerprint
encoding with it. So every existing claim is stale, and must be reviewed
again rather than relabelled. Older claim files, provider responses and
schema markers are rejected; a file or response that still says
downstream fails with a message naming context. snob accepts exactly
one version of each format and never both. After release, a change to what
a field means will still need a new version.