Notari: how the gate works, and where its boundary is

A human signs the boundary once. The agent then writes whatever it writes, and in CI, outside the agent's reach, Notari measures the diff against that signed boundary and issues a Change Passport: a receipt anyone can re-verify later without trusting Notari's runtime. There is no model anywhere in the verdict, so the verdict cannot be prompt-injected, and every box below corresponds to a command, a file, or a module that exists in the repository today.

notari 0.4.1 · alpha · sources: src/notari/{contract,verify,policy,secrets,passport,audit}.py · docs/SECURITY-MODEL.md

The path a change takes

1Humanonce per task
Sign the boundary
notari begin "add rate limiting"
--scope src/auth/**
--approved-by alice --key approver.pem
--expires-in 7 --repo owner/repo
Writes .notari/contract.json plus a detached .sig: the approved task, the allowed paths, the base commit, and a contract id. --scope is required, so the boundary is a conscious choice; --scope '**' is the explicit "anything the perimeter permits" opt-out, and an unrestricted contract is flagged on the passport.
--repo binds the approval to one repository so a signed contract cannot be replayed elsewhere, and --expires-in gives it a human-set deadline rather than a heuristic.
The standing perimeter of forbidden surfaces is signed separately and once, with notari guard. The approver's private key never has to leave that machine.
2AI agentuntrusted
Writes the diff, opens the PR
Claude Code, Codex, Cursor, Copilot, or a human in a hurry. Notari assumes nothing about intent here, because the check does not depend on it.
What it cannot do from inside this lane:
  1. Edit the workflow that judges it, because pull_request_target runs the trusted workflow from the base branch, not from the pull request.
  2. Forge the contract or the perimeter, because both carry Ed25519 signatures it has no key for.
  3. Weaken a strict verdict with a waiver, because .notari/exceptions.json is ignored entirely under --strict.
3CIoutside the agent's reach
notari verify --strict
Deterministic, in this order, every run:
  1. Build the authoritative changed-path inventory from git diff --name-status -z --find-renames, keeping both ends of a rename and including binary and mode-only changes.
  2. Match every changed path against the signed scope.
  3. Scan each touched file (whole blob from the candidate, not just added lines) for the 26 vendor patterns plus 6 inline-credential shapes in secrets.py.
  4. Classify sensitive surfaces: CI and workflow files, lockfiles, test deletions, and git configuration such as .gitattributes that can control diff visibility.
  5. Apply logged exceptions, which strict mode discards.
  6. Compose the verdict and chain a verification.run event into the audit log.
Same diff, same contract, same perimeter, same keys, same verdict, explainable line by line. The only clock it consults is the expiry a human set.
PASS
In scope, no secrets, no forbidden surface. Exits 0, the build goes green.
NEEDS_REVIEW
Something a human should look at (submodule bump, out-of-scope symlink). The hardened template defaults to block-on-review, so it fails the build unless you opt out.
BLOCK
Out of bounds or a secret in an added line. Exits non-zero, CI fails.

What you are left holding

The Change Passport
verify writes passport.json for tooling and passport.md for the pull request. The markdown footer cites the verification.run audit MAC, so the document traces back to the tamper-evident chain instead of asking you to trust it.
Anyone can re-check it
notari verify-passport passport.json
Re-verifies the verdict and the signature, months later, on a different machine, without trusting Notari's runtime or a screenshot. A forged PASS fails the signature.
The mistake becomes a rule
notari explain
Every finding comes with a fix in plain language, and a repeated mistake gets promoted into CLAUDE.md or AGENTS.md as a short rule, so the next session starts already knowing it. The gate catches; the loop stops the catch from repeating.

How it keeps your work safe

The boundary is signed, not configured
Scope and perimeter carry Ed25519 signatures, so an agent that edits the config it is judged by changes nothing about the verdict.
contract.py · perimeter.py
No model in the decision path
The verdict is compiled rules over a diff, so there is no prompt to inject and no judgment to talk out of its position.
policy.py · verify.py
The record is tamper-evident
Every decision chains into an HMAC-linked audit log, including everything allowed while the gate was paused, so an off-window is still on the record.
audit.py · journal.py
The judge sits outside the defendant
CI runs the workflow from the base branch, so the pull request cannot rewrite the check that is about to read it.
action.yml · docs/secure-workflow.yml
Approvals cannot be replayed
A contract is bound to one repository and carries an expiry, so yesterday's yes cannot be reused on another repo or three months later.
contract.py
Secrets are caught in the whole file
26 vendor patterns plus 6 inline-credential shapes are matched against every touched file, not just added lines, so a rename or a UTF-16 file cannot hide one.
secrets.py
What it does not do, stated plainly because a security claim without limits is marketing
Eleven of twelve documented bypass shapes BLOCK against a freshly signed perimeter, and the twelfth is the known line-split secret limit, which is published rather than hidden. Run them yourself with bash examples/bypass_probes.sh, and read docs/SECURITY-MODEL.md before trusting any one-line claim here.