Skip to main content

Sagrada Linter

A linter for the belief state of agent instruction files (CLAUDE.md, AGENTS.md, .cursorrules, …): it reads their git history and reports rules that were retracted in one commit and are back in the file today.

CI License: Apache 2.0 pre-commit

$ git clone https://github.com/Cruxia-Labs/sagrada-specimen && cd sagrada-specimen
$ uvx sagrada-linter read .
EXAMEN — sagrada-specimen
reading 1 rule file across 16 commits. nothing leaves this machine.

o RESTORED WITH INTENT  tone
    "Keep error messages plain and unfunny"
    killed   376ce593  2026-03-02
    returned 2ba05a7d  2026-03-28  — with the decision on the books:
    in-file · "restored 2026-04-02: the style guide never shipped"
    not a zombie. an intentional restoration, recorded.

+ WALKING  deploy_gate
    "Always run migrations manually before deploy"
    killed   e48323d1  2026-02-14
    revived  0f6c89af  2026-05-19
    active again in CLAUDE.md:2 today · walking 73 days

sagrada-specimen: 1 walking · 1 restored with intent

(captured 2026-07-31 — the day count moves; the commits do not)

That is the whole idea. The specimen is a scripted repository with known findings; run the same command on a repository you own (use a full clone — a shallow --depth 1 clone has no history to read, and the tool says so rather than printing a clean result):

uvx sagrada-linter read .

Runs locally over your git history. No install, no signup, no API key, nothing leaves your machine. On a clean history it says so plainly — and a clean result is a result:

$ uvx sagrada-linter read .
EXAMEN — clean-repo
reading 1 rule file across 1 commits. nothing leaves this machine.

NO REVIVED RULES FOUND
every retraction in 1 rule file is still resting.

What it does (and what it doesn't)

A snapshot checker reads rules as they are right now. Sagrada reads how they changed — and flags one pattern that quietly breaks agents: a retracted rule that came back. A merge, a stale branch, a second file nobody updated; agents read whatever text survived.

It's deterministic. Every result is a real retract→re-add in your git history, located by diffing consecutive versions of the file — no fuzzy matching, no model, no guessing. "The same rule" means the same normalized structured line; a reworded rule does not count.

What it will not catch (the edges, stated up front):

  • A rule re-added with completely different wording — that reads as a rewrite, not a zombie.
  • An intentional reversal you actually meant. Declare it — sagrada-linter restore FILE LINE --reason "…" writes a sagrada:allow marker on the line — and the reading shows the decision instead of an accusation (that's the RESTORED WITH INTENT entry above).
  • Semantic contradictions between two different rules ("always X" vs "never X"). That is a fuzzier problem; it is not part of the deterministic check and never fails your build.
  • Imperative free-prose rules with no structure (e.g. - Use type annotations). The deterministic floor anchors on structured rules (key: value, - term — definition); it refuses to guess at prose rather than risk a false positive. (Measured: see BENCHMARKS.md.)

Not git log | grep. Grep finds a string; it can't tell that a rule was retracted and then re-asserted across commits, pair the before/after, or tell a rewrite from a revival.

The gate — guard

The reading reports; the gate enforces. sagrada-linter guard writes a lock of every dead rule; guard --check then fails (exit 2) if an undeclared resurrection is present — with the rule's kill history in the failure text. guard --workflow installs the GitHub Actions workflow so the check runs on every PR.

This repository's specimen runs the gate on itself: its PR tab has real pull requests that try to bring retired rules back and fail CI with the kill history attached — and one declared restoration that passed.

Removal with a record — forget

Deleting a rule line loses the reason it died. sagrada-linter forget FILE LINE --reason "…" makes the removal itself an artifact: the line is deleted, a dated tombstone row is appended to .sagrada/tombstones.jsonl (term, definition, reason, residue scan), and a signed receipt is written beside it. Commit all three and the retirement is on the record — guard then locks the grave.

Belief-integrity score — vitals

sagrada-linter vitals            # 0-100 score for the current repo
sagrada-linter vitals --json     # full inputs + active-zombie detail
sagrada-linter vitals --badge-out badge.json   # shields.io endpoint JSON

A deterministic 0-100 score of one repo's belief hygiene over the trailing year, computed under SAGRADA-VITALS-METHOD v0.2 — a frozen, hash-committed formula (active zombies dominate and saturate; historical revivals add a small memory penalty; retraction hygiene can only reduce penalties, never add points). Record-side only: no model, no network, no judgment call anywhere in the number. What it does not measure: whether your agent answers correctly, code quality, security, or anything an LLM said — 100 means "no zombie beliefs detectable in the record," nothing more. The GitHub Action publishes the score to the job summary and uploads the badge (vitals: true, the default).

Band names. A repo reads CLEAR or ROTTEN. The strings frozen with the method are an implementation detail of the receipt format: --json output and sealed records carry them forever so every historical artifact recomputes byte-for-byte, and the display maps over them. WALKING names a rule's state, never a repo's.

Install

uvx sagrada-linter read .           # zero-install run (recommended)
pipx install sagrada-linter         # persistent CLI
pip install sagrada-linter          # into the current environment

Python 3.9+. One dependency (cryptography). Runs fully offline. Verbs: read · guard · forget · restore · scan-history · vitals · check-action · verify.

Pre-commit

Block a zombie before it lands. Add to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/Cruxia-Labs/sagrada-linter
    rev: v0.2.1
    hooks:
      - id: sagrada-linter

GitHub Action

The same check on every PR, with a comment on the offending line. See docs/GITHUB_ACTION.md:

- uses: actions/checkout@v4
  with: { fetch-depth: 0 }
- uses: Cruxia-Labs/sagrada-linter@v0

Verify it yourself

Every check can drop a small receipt (--receipt) into .sagrada/receipts/ — a signed, chained record (.er1.json) of exactly what was checked and what the verdict was. It verifies offline: a stranger recomputes it byte-for-byte with no install and no trust in us, in any of three implementations on disjoint stacks (Python, Node, browser WebCrypto — spec and golden vectors at er1-spec).

sagrada-linter read . --receipt
sagrada-linter verify .sagrada/receipts/*.er1.json     # Python — works from any install
# Or the zero-dependency JS reference verifier (one file; grab it from the repo):
#   curl -O https://raw.githubusercontent.com/Cruxia-Labs/sagrada-linter/v0.2.1/sagrada_linter/er1_verify.mjs
node er1_verify.mjs .sagrada/receipts/*.er1.json

See SCOPE_OF_CERTIFICATION.md for exactly what is certified and what is not.

In your agent — decision-time receipts

read audits the past. To attest what an agent did as it acts, call check_action in your loop (or from an MCP tool) before it runs a step: you get an ALLOW / HALT verdict and a receipt of the exact constraint state the action was taken under — recomputable offline by anyone.

from sagrada_linter import check_action

# your agent's active, deterministic constraints (from your rules / policy)
beliefs = [
    {"entity": "env:DEPLOY_TARGET", "rule": "equals", "value": "staging"},
    {"entity": "lib:boto3", "rule": "excludes"},
]
receipt = check_action(
    beliefs,
    {"tool": "shell", "asserts": {"env:DEPLOY_TARGET": "production"}, "resource": "deploy.sh"},
    receipts_dir=".sagrada/receipts",
)
if receipt["decision"]["verdict"] == "HALT":
    raise RuntimeError(receipt["decision"]["reason_code"])   # -> SUPERSEDED_VALUE

Or from the shell: sagrada-linter check-action --beliefs beliefs.json --action action.json --receipt. Runs locally, no network — we never see your files. The receipt verifies offline, so a relying party never has to trust the agent that produced it.

The Index

beliefrotindex.com — a weekly census of zombie beliefs in tracked public repos, scored with this linter under a frozen method. Named rows are opt-in: run the reading, then self-list.

Status

One person built this and nobody else has used it yet. A flagged rule can be one you re-added on purpose — declare it with restore and the reading shows the decision instead of an accusation. If it is wrong about your repository, that is exactly what we want to hear: open an issue with the two commits.

License

Apache-2.0 © 2026 Cruxia (including the patent grant). Contributions welcome — see CONTRIBUTING.md.


A zombie belief is the smallest, most checkable case of a general problem: systems that re-assert rules they were told to drop. The linter catches the deterministic version of that — and nothing fuzzier — and emits a receipt so the catch is something a stranger can re-verify, not something you take on trust. → Cruxia-Labs

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sagrada_linter-0.2.1.tar.gz (87.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sagrada_linter-0.2.1-py3-none-any.whl (81.3 kB view details)

Uploaded Python 3

File details

Details for the file sagrada_linter-0.2.1.tar.gz.

File metadata

  • Download URL: sagrada_linter-0.2.1.tar.gz
  • Upload date:
  • Size: 87.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.15

File hashes

Hashes for sagrada_linter-0.2.1.tar.gz
Algorithm Hash digest
SHA256 5f033fbecda41a9ff196c2250e3f65624ce4b51a248de2f4fe670e4fad3e347b
MD5 006ff45a4d40ef9f97815d88c8739756
BLAKE2b-256 44efc77a72ff41b43323ed4df9bfb3c3eb9e8415130a40affcee55e87c007210

See more details on using hashes here.

File details

Details for the file sagrada_linter-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: sagrada_linter-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 81.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.15

File hashes

Hashes for sagrada_linter-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d2a313e2cd77dc22f70f3c25fde8b5c472a9032731a7d0cbab74f736f26a357a
MD5 070a9feb06d1e5ab4029f4546610cb89
BLAKE2b-256 acd502283ece6806d2c96ba66bfdad0ad28c464cd5f098382a4ac3bf69a21a3c

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page