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 on PATH;
  • in the project root, with the inherited environment, so it can use its own credentials;
  • under one deadline of timeout_ms for 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_bytes is 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.