Get started#

Review policies with receipts.

snob fails your check when a file has not been reviewed against your rules since the file or the rule last changed.

Install#

Download the archive for Linux or macOS from the Releases page, or build from a checkout with Rust 1.88 or newer:

$ cargo install --locked --path .

docs/install.md lists the platforms and how to verify a download.

Try it#

In an empty directory with one source file:

// src/routes/admin.ts
export function updateRole(user, role) {
  requireAdmin(user);
  return db.users.update(user.id, { role });
}
$ snob init
created snob.toml

Add a policy:

# snob/policies/routes.policy.toml
match = ["src/routes/**/*.ts"]

[[must]]
id = "authz"
description = "Every handler that writes data checks the caller's role first."
review = "Follow each write back to a role check on the same request."

init again adds a placeholder for every rule a matching file has no entry for, and check fails until someone reviews:

$ snob init
created snob/reviews/src/routes/admin.ts.review.toml
$ snob check          # exit 1
  FAIL must   routes/authz: unclaimed: reference is still `changeme`
$ snob pending src/routes/admin.ts
      bind to:     content blake3:1a48d620c9be1e611fefef233763f7cddc5fdb60e1852f248f6139c1b5705b3a
                   rule    blake3:50c72e574724f634b29736349b9088bdc7376529cc24619cf2961745c94390a3

pending shows the rule's description, the review guidance and the subject a claim has to name. After reviewing, the reviewer records a claim with that subject. snob cannot write claims yet, so this one is written by hand:

# snob/claims/admin-authz.claim.toml
schema = "snob.claim.v3"
status = "satisfied"
reviewer = "reviewer@example.invalid"
reviewed_at = 2026-10-06T12:00:00Z
evidence = "updateRole calls requireAdmin before the write."

[subject]
path = "src/routes/admin.ts"
content_hash = "blake3:1a48d620c9be1e611fefef233763f7cddc5fdb60e1852f248f6139c1b5705b3a"
rule = "routes/authz"
rule_fingerprint = "blake3:50c72e574724f634b29736349b9088bdc7376529cc24619cf2961745c94390a3"

and points the review file at it:

# snob/reviews/src/routes/admin.ts.review.toml
[claims]
"routes/authz" = "local:admin-authz"
$ snob check
1 file (0 without a review file), 1 rule: 1 passed, 0 exempt, 0 warned, 0 failed
$ echo "// tidy" >> src/routes/admin.ts
$ snob check          # exit 1
  FAIL must   routes/authz: stale: file content changed since review [local:admin-authz]

The claim is bound to the content it reviewed, so any edit makes it stale, and so does rewording the rule. Reverting the edit makes it fresh again.

How it works#

  • Policies (snob/policies/*.policy.toml) match files with globs and list rules at three levels. must rules fail the check, should rules warn, and consider rules are guidance that never needs a claim.
  • Review files (snob/reviews/<path>.review.toml) map each rule that applies to a file to a claim reference such as local:admin-authz, or to "changeme" while unreviewed. They never repeat policy text.
  • Claims are records of a review, fetched by providers: local: reads .claim.toml files from snob/claims/, and a configured command can serve other prefixes from an external review system.
  • Subjects tie a claim to one file's exact bytes, one rule's exact definition and, where declared, the files it depends on. A new file, an edit, a reworded rule or a changed dependency all need review again.

Adopting it in an existing repository#

You do not have to review everything first. Commit the policies and review files with their changeme placeholders, then pin that commit:

# snob.toml
[ratchet]
since = "0123456789abcdef0123456789abcdef01234567"   # the adoption commit, in full

Obligations that existed unchanged at that commit pass as exempt (snob pending lists them); anything new or changed needs a review. Pinning the commit edits snob.toml, so if a policy covers snob.toml, its review is the first one due. Details: docs/freshness.md.

In CI#

Check out full history and judge every change by its target branch:

$ snob check --trusted-rev origin/main

The target branch's snob.toml, policies and ratchet pin then apply, so a change cannot move its own baseline, ignore its own files, swap the provider command, or remove or weaken a rule it is judged by. A file the change deletes needs a review of the deletion. Without --trusted-rev, all of that comes from the checkout under review and is not binding.

Trust model#

snob does not review anything and does not decide who may approve. It is meant for code that changes faster than people re-read it, such as agent-written code, where some properties (authorization, error handling, a release process) can only be judged by reading.

A satisfied claim records that someone said a review happened against a specific subject. snob checks that the record exists, is positive and is bound to the current bytes and rule. It does not check that the review was right, and a local: claim does not show who wrote it: anyone who can edit the repository can edit the claim. Authenticated records have to come from an external provider. snob adds no identity system of its own: a local claim is as trustworthy as the repository review that merged it, and an external provider is as trustworthy as its own signer checks.

Limitations#

There is no command to create claims yet; reviewers write them by hand. Subjects are whole files. Process cleanup for external providers is Unix-only, and Windows is untested. More in docs/design.md.