Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

claims-mcp

claims-mcp is an MCP server that keeps a ledger of claims about your software in a single JSON file, committed alongside the code.

A claim is one atomic, falsifiable statement: “parse_config rejects files over 1 MiB”, “the README’s install command works on macOS”, “the agent implemented every item in the task”. Each claim carries the evidence an auditor gathered and an append-only history of verdicts in FEVER terms: supported, refuted or not_enough_info. Every verdict must cite evidence that backs it.

Together they give a durable, reviewable record for:

  • Requirements traceability: claims link to parent claims (derived_from), from the system level down to individual artifacts.
  • Verification and validation (IEEE 1012): integrity levels 1-4 and verification methods (inspection, analysis, demonstration, test, formal proof).
  • Conformance and LLM output auditing: checking that what an agent produced matches the instruction it was produced from, and that the docs match the code.

The server only records. It never runs your code. An agent does the verification, guided by the bundled claims-audit skill, and the server enforces the ledger’s rules: verdicts must cite evidence, history is append-only, and a hand-edited ledger is detected and blocks writes.

Thirty seconds of claims-mcp

An agent auditing a project registers a claim, pointing at the text that makes it:

{"tool": "add_claim", "arguments": {
  "statement": "claims-mcp verify exits 1 when any integrity seal check fails",
  "kind": "functional", "level": "software", "integrity_level": 3,
  "verification_method": "test",
  "source_anchor": {"path": "docs/site/src/reference/cli.md", "lines": {"start": 26, "end": 26}}
}}

It inspects the code and runs the test itself, then records what it saw. The server pins the anchor to the current commit:

{"tool": "add_evidence", "arguments": {
  "claim_id": "CLM-0001", "kind": "test", "stance": "supports",
  "locator": "cargo test verify_reports_tampering",
  "summary": "Test edits a sealed verdict; verify exits 1 and names the record.",
  "anchor": {"file": {"path": "src/main.rs", "lines": {"start": 160, "end": 182}}}
}}

It records a verdict that cites the evidence:

{"tool": "record_verdict", "arguments": {
  "claim_id": "CLM-0001", "verdict": "supported", "auditor": "example-model",
  "rationale": "The test exercises the failure path and observes exit code 1.",
  "evidence_ids": ["EVD-0001"]
}}

Weeks later, someone changes the anchored code. claims-mcp recheck notices:

2 anchors checked against 4e16582: 1 current, 1 need re-audit
  CLM-0001 EVD-0001 changed: lines 160-182 of src/main.rs changed since they were recorded
    src/main.rs:160-182 @ 86a73e2

The claim needs re-auditing, and the ledger says exactly where to look. (The ids, line numbers and test name above are illustrative.)

Where to go next

The problem

Software makes promises in many places: specs, READMEs, tickets, prompts given to coding agents. Over time, three things go wrong.

Documentation drifts from code

A README says the server rate-limits requests; a refactor removed the limiter a year ago. Nobody notices, because nothing connects the sentence to the code. Reviews check the change, not the promises the change breaks.

What claims-mcp does: each promise becomes a claim with a source_anchor on the exact lines that make it. Evidence carries anchors too: a file anchor on the code that shows the claim holds, or a search anchor (a git grep pattern with a recorded hit count) showing that something is absent. The server pins every anchor to a commit, and recheck reports anchors that have since moved, changed or been overturned. Drift becomes a finding instead of a surprise.

LLM output is accepted without checking

An agent reports “I implemented all five requirements”. That is the producer certifying its own work. Checking it properly means splitting the instruction into separate assertions, checking each one independently and keeping the result.

What claims-mcp does: the claims-audit skill breaks the instruction into atomic, falsifiable claims. The auditor gathers its own evidence for each one and records a FEVER verdict that must cite that evidence. not_enough_info is an allowed outcome, so “I didn’t check” can’t pass as “it works”.

Traceability is lost

Which requirement does this test cover? Which sub-requirements make up “the export feature works”? If a low-level check fails, which high-level promise is now broken? Spreadsheets and ticket links answer this badly, and go stale.

What claims-mcp does: claims form a hierarchy through derived_from, from system level down to individual artifacts. trace walks it in either direction, and audit_report produces a traceability matrix and lists the gaps: unverified claims, high-integrity claims checked only by inspection, and parents whose children aren’t all supported.

Why a ledger, and why sealed

All of this lives in one claims.json, committed with the code, so it is reviewed, diffed and versioned like the code. Verdict history is append-only: a new verdict doesn’t erase the old one. Every record carries an integrity seal. If anyone, an agent included, edits the file by hand instead of going through the tools, the server notices and refuses further writes until the file is restored.

Next: how claims-mcp relates to formal verification.

Alongside formal verification

Formal verification proves that code meets a specification for every possible input. It is the strongest assurance software can have, and AI is making it much cheaper. So why keep a ledger of claims judged by an auditor?

Because a proof answers one narrow question very well, and most of what you need to trust lies outside it. claims-mcp records everything else, and can hold the proof as its strongest evidence.

What formal verification gives you

Take a Rust project verified with Aeneas. Charon extracts the crate’s MIR to an intermediate form (LLBC). Aeneas turns that into a pure functional model in a proof assistant such as Lean. You write a specification in Lean, for example “sort returns a permutation of its input, in order”, and prove that the model meets it. Lean’s small kernel checks the proof. If it checks, the property holds for every input, not just the ones a test tried.

That is a guarantee no amount of testing or auditing can match. claims-mcp doesn’t compete with it.

What a proof can’t tell you

A proof shows that the model meets the spec, assuming the checker is sound. Each of those three words is a gap, and there is a fourth gap around all of them.

The spec gap: does the spec say what you meant?

A proof can be valid and say nothing useful. “Returns a sorted list” holds for a function that always returns []. A spec with contradictory assumptions proves anything. When an AI writes both the code and the spec, it may quietly weaken the theorem until the proof goes through. Whether a formal spec captures the requirement is a judgment about meaning, and no kernel checks meaning.

In claims-mcp: a claim such as “sort_spec states requirement REQ-7”, whose evidence compares the Lean statement with the requirement text, judged by someone other than the spec’s author.

The model gap: does the model behave like the binary?

The proof is about a model of the code produced by a translator. Translators have bugs and unsupported features, and the shipped binary goes through a compiler the proof never saw.

In claims-mcp: a claim that the model matches the program, with differential testing as evidence: run the model and the real binary on the same inputs and compare the outputs.

The trusted base: what did the proof assume?

Proof assistants have escape hatches. In Lean, sorry and admit mark unfinished proofs, an axiom can assert anything, and native_decide trusts compiled code instead of the kernel. The toolchain (Charon, Aeneas, rustc) is also trusted.

In claims-mcp: an absence claim. A search anchor for the escape hatches over the proof directory, recorded with zero hits, is evidence that none are used. If one appears later, recheck reports the anchor as overturned. A separate claim lists the trust assumptions explicitly.

Coverage: what can’t be proved at all?

Proof tools reach a subset of code. Aeneas handles safe, sequential Rust; it doesn’t handle unsafe, concurrency or I/O. No one formally verifies a README, a deployment config, an install script or an agent’s report of what it did. In practice, most failures come from requirements and integration, not from the algorithmic core.

In claims-mcp: ordinary claims, with evidence from tests, inspection and demonstration, for everything the proof doesn’t reach. That is most of the system.

Side by side

Formal verification (for example Aeneas and Lean)claims-mcp
Question answeredDoes this function meet this spec, for every input?What did we claim, what evidence backs each claim, and what was the verdict?
ScopeA few critical functions: narrow and deepA whole system and its documentation: broad
Who judgesA proof checker’s kernelAn auditor, citing recorded evidence
Strength of “yes”Mathematical, given the spec and the trusted baseAs strong as the cited evidence
When code changesThe proof breaks and must be repairedrecheck flags anchors that moved, changed or were overturned
CostHigh; needs proof-assistant skillsLow

Using them together

The two fit naturally. A proof becomes the evidence for a high-integrity claim, and the audit covers the questions around it. A verified function might carry these claims:

{"tool": "add_claims", "arguments": {"claims": [
  {"statement": "parse_header never reads past its input and rejects malformed lengths",
   "kind": "safety", "level": "unit", "integrity_level": 4,
   "verification_method": "formal_proof", "artifacts": ["src/header.rs"]},
  {"statement": "The Lean spec parse_header_spec states the header requirement",
   "kind": "documentation", "level": "artifact", "integrity_level": 4,
   "verification_method": "inspection", "derived_from": ["@0"]},
  {"statement": "The proofs use no sorry, admit or axiom",
   "kind": "constraint", "level": "artifact", "integrity_level": 4,
   "verification_method": "analysis", "derived_from": ["@0"]},
  {"statement": "The Lean model of parse_header matches the compiled function on 10,000 random inputs",
   "kind": "functional", "level": "artifact", "integrity_level": 4,
   "verification_method": "test", "derived_from": ["@0"]}
]}}

The evidence for each:

  • The proof itself: kind: "proof", with a file anchor on the theorem in the .lean file and the proof checker’s output as the locator.

  • No escape hatches: a search anchor, recorded with zero hits:

    {"search": {
      "pattern": "(^|[^[:alnum:]_])(sorry|admit)([^[:alnum:]_]|$)|^[[:space:]]*((private|protected)[[:space:]]+)?axiom[[:space:]]|native_decide",
      "scope": ["proofs"]
    }}
    

    The pattern sticks to POSIX character classes rather than \b, because git grep -E uses the platform’s regex library and not every platform supports \b. It also catches private and protected axioms.

  • Model fidelity: the differential-testing run, as test evidence.

  • Spec fidelity: an inspection comparing the spec with the requirement, recorded by a reviewer who did not write the spec.

The integrity_level of 4 matters. audit_report flags level 3-4 claims verified only by inspection as rigor gaps, so the proof claim needs formal_proof (or test, analysis or demonstration). The spec-fidelity claim is inspection by nature, so it shows up as a gap that a person has to look at. That is deliberate.

Questions for any “formally verified” claim

Leo de Moura, who created Lean, suggests asking four questions of any claim that something is formally verified. Each maps onto a claim you can record and recheck:

QuestionAs a claim in the ledger
Show me the specifications.“Spec X states requirement Y”, judged by an independent reviewer
Which properties were verified?“Theorem T proves property P”, anchored to the .lean file
Which proof checker and kernel?“The proofs pass Lean’s kernel and an independent checker”, with CI logs as evidence
How large is the trusted base?The zero-hit escape-hatch search, plus a claim listing the toolchain assumptions

AI makes this more important, not less

LLMs are getting good at writing Lean. Proofs that took experts months can be drafted quickly, and proof repair after a code change, historically the biggest cost of formal methods, is exactly the kind of task models do well. Because the kernel rejects wrong proofs, AI-written proofs can be trusted in a way AI-written code can’t.

The weak point moves to the spec. If a model writes the spec, the code and the proof, you get a verified program that may do the wrong thing. The human’s job shifts from reviewing code to reviewing specs, and the review needs to be recorded, independent and rechecked as things change. That is what an audit ledger is for.

Sources

Where it doesn’t fit

claims-mcp is useful because it is honest about what a verdict means. Here is the same honesty about the tool itself.

  • A verdict is a judgment, not a proof. supported means an auditor looked at cited evidence and concluded the claim holds. The server enforces that the evidence exists and points the right way (supported must cite supports evidence). It cannot check that the auditor read the evidence correctly. When you need “holds for every input”, use formal verification and record the proof as evidence.
  • The ledger is only as good as its evidence. A vague locator, or an inspection where a test was needed, gives a weak verdict. audit_report flags some of this (high-integrity claims checked only by inspection show up as rigor gaps), but it can’t judge quality.
  • The server never runs your code. It records what the auditor observed. The only commands it runs are read-only git queries for anchors (rev-parse, status, grep, diff, cat-file). Running tests, programs and proof checkers is the auditor’s job.
  • Seals deter hand edits; they are not cryptographic security. The HMAC key that seals each record is in the public source, so someone determined can forge seals. What seals stop is the casual edit, in particular an agent “fixing” a verdict by editing the JSON. See Integrity seals.
  • Anchors need git. Evidence without anchors works anywhere, but commit pinning and recheck need the catalog to be inside a git repository.
  • Claims must be atomic to be useful. One claim covering five behaviours hides partial failure. Splitting claims well takes judgment; the skill guides it, but nothing enforces it.
  • One catalog per server. Each server instance keeps one claims.json, over stdio only.

Claims

A claim is one atomic, falsifiable assertion about an artifact or a requirement.

  • Atomic: one assertion. If it contains “and”, it is probably two claims. A compound claim hides partial failure.
  • Falsifiable: some observation would show it false.
  • Observable: it describes behaviour or a property of the artifact, not an intention.
WeakStrong
“Handles errors well”“parse_config returns ConfigError::TooLarge for files over 1 MiB”
“Fast and secure”A performance claim and a security claim, each with concrete bounds
“Uses the rmcp crate”“Cargo.toml depends on rmcp and the server is built with rmcp’s tool_router”

Fields

FieldMeaning
idCLM-0007; assigned by the server and never reused
statementThe assertion; must not be empty
kindWhat sort of property it asserts (below)
levelWhere it sits in the requirements hierarchy (below)
verification_methodHow it is meant to be verified; see Integrity levels and methods
integrity_levelOptional, 1-4; see Integrity levels and methods
sourceOptional free text: where the claim came from (prompt, spec section, ticket)
source_anchorOptional file anchor on the exact text that makes the claim
derived_fromParent claims; see Traceability
artifactsPaths the claim is about, so list_claims can find it by artifact
tagsFree-form labels; see below
statusactive, or retired with a reason and time

Duplicate tags and artifacts are removed (the first occurrence wins).

Kinds

kindAsserts
functionalObservable behaviour or output
non_functionalA quality attribute such as usability or maintainability
interfaceA contract with another component, an API or a user
performanceA latency, throughput or resource bound
safetyProtection against harm to people or property
securityA confidentiality, integrity or availability property
documentationA statement about documentation content
constraintAn imposed limitation, such as a language, licence or dependency rule

Levels

From broadest to narrowest: system (the whole task or system), software, component, unit (one function or type) and artifact (a concrete produced thing).

Tags with meaning

Tags are free-form, but two prefixes are used by the tools:

  • area:<name> groups claims. next_steps and claims-mcp tldr count the remaining work per area.
  • issue:<N> links a claim to issue N. The PDF report links it to the repository’s issue tracker.

Source anchors

When the claim comes from a file in the repository, such as a README line that promises a feature, set source_anchor to those lines. If the claim is refuted, that is the text someone has to fix. audit_report lists refuted claims without one under refuted_without_source_anchor. update_claim can replace a source anchor but not remove it.

Changing and retiring claims

update_claim edits metadata. Evidence and verdicts can’t be edited. To change what a judged claim means, retire it with retire_claim and add a corrected one. Editing a judged claim’s statement until it passes would rewrite history.

Retired claims stay in the catalog, so derived_from links that point at them remain valid, but they accept no more evidence, verdicts or updates.

Evidence

Evidence is one finding the auditor gathered, attached to a claim. The auditor reads the code, runs the test or program, and records what it observed. The producer saying “I implemented it” is not evidence.

Record one finding per evidence item. Record findings that refute the claim, and neutral ones, as well as supporting ones; leaving them out biases the audit.

Fields

FieldMeaning
idEVD-0014; unique across the whole catalog and never reused
kindThe form of the evidence (below)
locatorWhere to find it, precisely enough to repeat: src/store.rs:88, a test name, the exact command run, or a URL
summaryWhat was observed, not what was concluded
stancesupports, refutes or neutral toward the claim
anchorOptional machine-checkable location, pinned to a commit; see Anchors and recheck
recorded_atSet by the server

Kinds

kindIs
testThe result of an executed test
inspectionAn observation from reading an artifact
analysisThe result of reasoning or static analysis
demonstrationAn observation from running the system
proofA proof artifact or proof-checker output
logA captured log or command output
otherAnything else

Stance

The stance uses FEVER terminology. supports means the evidence shows the claim holds, refutes that it does not, and neutral that the evidence is relevant but decides nothing. Verdicts must cite evidence with the matching stance.

Showing that something is absent

When a document promises a feature the code lacks, there is no line to point at. Record refutes evidence with a search anchor instead: a git grep -E pattern broad enough to catch any reasonable implementation, over a scope that covers every place it could live. The server runs the search itself and records the hit count, which must be zero. Add a second refutes item with a file anchor on where the feature would have to plug in. A search alone can miss an implementation filed under an unexpected name.

A demonstrated absence supports refuted. Not having looked does not: that is not_enough_info.

Evidence is permanent

Evidence can’t be edited or removed. If the code changes, add new evidence and record a new verdict. When evidence is recorded after a claim’s latest verdict, audit_report lists the claim as stale until it is judged again.

Verdicts

A verdict is the auditor’s conclusion about a claim, in the three-way scheme from the FEVER fact-verification benchmark:

VerdictMeansMust cite
supportedCited evidence shows the claim holdsAt least one supports evidence item
refutedCited evidence shows the claim does not holdAt least one refutes evidence item
not_enough_infoThe evidence can’t decide the claimNothing required, but the rationale must not be empty

Cited evidence must belong to the same claim. Every verdict also needs a non-empty rationale (why the evidence decides the claim) and auditor (the person or model id).

not_enough_info is an honest, legitimate outcome. Use it instead of guessing. Never infer support from the absence of failure (“no errors, so it works”), and never infer absence from not having looked.

History is append-only

record_verdict appends to the claim’s verdict history; nothing is overwritten. The claim’s current verdict is its latest one, or unverified if it has none. After a fix, the auditor gathers new evidence and records a new verdict, and the earlier one stays as the audit trail.

The Fix: convention

A refuted or not_enough_info rationale should end with Fix: <one concrete next step>: what to change, or which test or evidence would decide the claim. next_steps and claims-mcp tldr show the text after the last Fix: as the claim’s next step. Make it specific and self-contained: “Fix: add a test that sends a 1024-bit RSA JWK and asserts it is rejected”, not “Fix: add tests” or “same as CLM-0012”.

Parent claims

A parent claim’s verdict follows from its children. Judge the leaves first. Once every active child is supported, record one analysis evidence item on the parent (locator children of CLM-0002, the parent’s own id) explaining how the children cover the parent, then record the parent’s verdict citing it. If any child is refuted, record refutes evidence on the parent the same way, naming the refuted children, and mark the parent refuted. Until then, audit_report lists the parent under decomposition.

Integrity levels and methods

claims-mcp borrows two ideas from IEEE 1012, the standard for system and software verification and validation: how much a failure would matter, and how a claim is verified.

Integrity levels

integrity_level is optional and ranges from 1 to 4. Any other value is rejected.

LevelUse when failure would cause
4Safety harm, a security breach or data loss
3Loss of a core function the source explicitly asked for
2A degraded or secondary function
1Cosmetic, style or documentation issues

audit_report counts claims per level (including unassigned), and next_steps lists open level 3-4 claims ahead of the rest.

Verification methods

verification_method says how the claim is meant to be verified:

MethodMeans
inspectionReading code, documents or output
analysisReasoning, calculation or static analysis
demonstrationRunning the system and observing behaviour without instrumentation
testExecuting a defined test with pass/fail criteria
formal_proofA machine-checked or rigorous mathematical proof

The first four are the IEEE 1012 and INCOSE methods. formal_proof is added for claims backed by proof assistants such as Lean; see Alongside formal verification.

The rigor rule

Inspection is acceptable at levels 1-2. Levels 3-4 need test, analysis, demonstration or formal_proof. audit_report lists level 3-4 claims whose method is inspection as rigor gaps. Resolve a rigor gap by adding a test or analysis, not by lowering the level or switching the method on a judged claim. That would be moving the goalposts; retire the claim and add a corrected one instead.

Traceability

Claims form a hierarchy. Each claim can name parent claims in derived_from, from a system-level requirement (“the export feature works”) down to unit and artifact claims (“the CSV writer quotes fields containing commas”).

Rules

  • Every derived_from id must exist. A new or updated link can’t point at a retired claim; an existing link to a parent retired later is kept and reported as dangling_links.
  • Links never form a cycle, including a claim linking to itself.
  • add_claims registers a whole hierarchy in one all-or-nothing call. Within the batch, "derived_from": ["@0"] refers to the entry at index 0, which must come earlier.

Walking the hierarchy

trace follows links from a claim: up to its ancestors, down to its active descendants, or both (the default), optionally limited by max_depth. Each claim appears once, at its shortest depth, with its current verdict.

get_claim also lists a claim’s active children.

The audit report

audit_report covers active claims and returns:

  • Totals: counts by current verdict, kind, level and integrity level.

  • Gaps, each a list of claim ids:

    GapMeaning
    no_evidenceNo evidence at all
    unverifiedNo verdict
    refutedCurrent verdict is refuted
    not_enough_infoCurrent verdict is not_enough_info
    staleEvidence was recorded after the latest verdict
    rigorIntegrity level 3-4 verified only by inspection
    decompositionHas active children, and at least one isn’t supported
    dangling_linksLinks to a retired parent
    refuted_without_source_anchorRefuted, with nothing pointing at the text that makes the claim
  • Integrity: the result of checking every seal.

  • Traceability matrix (unless include_matrix is false): one row per active claim with its parents, children, verification method, evidence locators, refuting locators and current verdict.

On a large catalog, the report is too long to act on. next_steps (and claims-mcp tldr) reduce it to a ranked to-do list; see Your first audit.

Integrity seals

The catalog is meant to change only through the tools, which enforce its rules: verdicts must cite evidence, the history is append-only, and claims are retired rather than deleted. To stop an agent skipping the tools and editing claims.json directly, every claim, evidence item and verdict, plus each of the two id counters, carries a seal: an HMAC-SHA256 over the record’s content and its place in the catalog, under a key built into the binary.

  • Every call verifies every seal. If any check fails, the server refuses every write until the file is restored. Reads and audit_report keep working, so the damage can be inspected. There is no option to turn this off.
  • What counts as tampering: a changed field, an added record without a seal, a deleted claim, evidence item or verdict, reordered verdicts, evidence moved to another claim, a changed id counter, and a schema_version lowered to 1.
  • Recovering: restore the file from git (git checkout -- claims.json, or revert the commit that edited it), then redo the change through the tools.
  • Not affected: reformatting the file or reordering its keys (seals cover content, not layout), and git merges of branches that changed different claims. Each counter has its own seal, so a branch that added a claim merges cleanly with one that added evidence. Two branches that both added claims, or both added evidence, hand out the same ids; replay one onto the other with claims-mcp rebase.

How a seal binds a record

Each seal is {"by": "server", "mac": "<64 hex digits>"}, or "by": "adopt" for records sealed during migration. The MAC covers the record and its place:

  • evidence binds its claim id;
  • a verdict binds its claim id, its index, and the previous verdict’s MAC, so verdicts can’t be reordered or dropped;
  • a claim binds its own fields, its evidence ids, and its latest verdict’s MAC;
  • each counter binds schema_version and its value. Ids are never reused, so a gap below the counter reveals a deleted claim, and lowering the counter to hide the newest claim’s deletion breaks the counter’s seal.

What seals are not

Seals are a deterrent, not cryptographic security. The key is in the public source, so anyone who reads it can forge seals. What they stop is the casual edit: in your project, an agent would have to go and find the key and write code to recompute seals. To keep agents away from the file entirely, add deny rules.

claims-mcp verify checks every seal from the command line and exits 1 on any failure, so CI can reject a hand-edited catalog; see Checking in CI.

Anchors and recheck

An evidence locator is free text. To make evidence point at an exact, checkable place, add an anchor. The server pins it to the commit checked out at the time.

AnchorShapeUse it for
file{"file": {"path": "src/server.rs", "lines": {"start": 40, "end": 61}}}Code or text that shows the finding; omit lines for the whole file
search{"search": {"pattern": "rate.?limit|throttl", "case_insensitive": true, "scope": ["src"]}}A git grep -E search; with zero hits, it shows that something does not exist

Claims take a source_anchor (a file anchor) on the text that makes them, such as the README line that promises a feature. If the claim is refuted, that is the text to fix.

What the server records

The server, not the caller, records the commit (git rev-parse HEAD). For a search, it runs git grep itself and records the hit count; a hits value the caller passes must match.

It rejects paths outside the repository, line ranges past the end of the file, and search scopes that don’t exist. A misspelled directory would otherwise “prove” anything absent. Searches and the dirty check skip the catalog file and its lock and temporary files, so a committed claims.json never matches its own recorded patterns.

An anchor on a file with uncommitted changes is marked dirty. Anchors need the catalog to be inside a git repository; evidence without one works anywhere.

Recheck

claims-mcp recheck (and the recheck tool) compares every anchor on an active claim with HEAD:

StatusMeaning
movedThe anchored lines are unchanged but at new line numbers (shown)
changedThe anchored lines or file changed, or a search’s hit count changed
overturnedAn absence search now matches, or a matching search now finds nothing
missingThe file no longer exists
commit_missingThe pinned commit was removed by a history rewrite
unpinnedRecorded on uncommitted changes, so the commit may not show what was seen

The command prints one line per anchor that isn’t current and exits 1 if there are any. --json prints the same data as the tool, which the PDF report takes as its recheck input.

A moved anchor still holds; re-record it if you want current line numbers. Anything changed, overturned, missing or commit_missing means the evidence no longer describes the code, so re-audit that claim.

Anchors recorded on a branch

An anchor pins whatever commit is checked out, so an audit run on a feature branch pins branch commits. A true merge commit keeps them reachable from the main branch. A squash or rebase merge rewrites them, and once the branch is deleted, fresh clones report every such anchor as commit_missing. Either merge pull requests that carry anchored evidence with a merge commit, or re-record the evidence on the main branch after merging.

Install

Installing claims-mcp takes three steps: get the binary, register the server with your MCP client, and install the claims-audit skill that teaches the agent the workflow.

1. Install the server

Pick one.

Download a release

Each GitHub release has an archive per platform, a .sha256 checksum for each, and the audit report for that version (audit.pdf, the summary, and audit-full.pdf, every record):

PlatformArchive
Linux x86_64 (static)claims-mcp-<version>-x86_64-unknown-linux-musl.tar.gz
Linux ARM64 (static)claims-mcp-<version>-aarch64-unknown-linux-musl.tar.gz
Windows x86_64claims-mcp-<version>-x86_64-pc-windows-msvc.zip
macOS Intelclaims-mcp-<version>-x86_64-apple-darwin.tar.gz
macOS Apple Siliconclaims-mcp-<version>-aarch64-apple-darwin.tar.gz

On Linux and macOS:

sha256sum -c claims-mcp-<version>-<target>.tar.gz.sha256   # macOS: shasum -a 256 -c
tar -xzf claims-mcp-<version>-<target>.tar.gz
mkdir -p ~/.local/bin
install -m 755 claims-mcp-<version>-<target>/claims-mcp ~/.local/bin/

On Windows (PowerShell), compare the hash with the one in the .sha256 file, then extract and put claims-mcp.exe in a directory on your PATH:

$archive = "claims-mcp-<version>-x86_64-pc-windows-msvc"
(Get-FileHash "$archive.zip" -Algorithm SHA256).Hash   # must match "$archive.zip.sha256"
Expand-Archive "$archive.zip" -DestinationPath .

Each archive also contains the README, LICENSE, NOTICE and the skills/ directory. The macOS binaries are not signed or notarized; if Gatekeeper blocks one, clear the quarantine flag with xattr -d com.apple.quarantine ~/.local/bin/claims-mcp.

Install with cargo

With Rust 1.89 or newer:

cargo install --locked --git https://github.com/hypotheosis/claims-mcp

This puts claims-mcp in ~/.cargo/bin.

Build from a clone

cargo build --release   # binary at target/release/claims-mcp

2. Register the server with Claude Code

With claims-mcp on your PATH, register it from the root of the project whose catalog it should keep:

claude mcp add --scope project claims -- claims-mcp --catalog claims.json

--scope project writes .mcp.json, so everyone working in the repository gets the server. Use --scope user to make it available in all your projects, or leave the flag off to register it for this project only. A relative --catalog resolves against the directory Claude Code is started in; an agent working in another checkout, such as a git worktree, names its own ledger with each tool’s catalog parameter. If you prefer to write the .mcp.json entry by hand (use an absolute command path if the binary is not on PATH):

{
  "mcpServers": {
    "claims": {
      "command": "claims-mcp",
      "args": ["--catalog", "claims.json"]
    }
  }
}

Add the catalog’s lock and temporary files to .gitignore: for claims.json, these are .claims.json.lock and .claims.json.tmp.

Other MCP clients work the same way: run claims-mcp --catalog <path> as a stdio server. See the CLI reference for options.

3. Install the skill

The claims-audit skill teaches Claude the audit workflow. Copy it from a clone or a release archive, either for yourself (all projects):

mkdir -p ~/.claude/skills
cp -r skills/claims-audit ~/.claude/skills/

or into a project, committed so collaborators get it too:

mkdir -p .claude/skills
cp -r skills/claims-audit .claude/skills/

On Windows the user directory is %USERPROFILE%\.claude\skills. Restart Claude Code, then continue with your first audit.

Your first audit

With the server and skill installed, ask Claude to audit something: “audit this change against the task”, “check the README against the code”, or run /claims-audit. The skill walks the agent through six steps. This page explains them so you know what to expect, and what to check.

1. Scope

The agent identifies the artifact under audit (files, a module, a PR, generated output) and the source it was produced from (the prompt, ticket or spec section). When the source is a file in the repository, each claim gets a source_anchor on the lines that make it. The agent calls list_claims first, so an existing audit continues instead of duplicating claims.

2. Extract claims

The source is broken into atomic, falsifiable claims and registered as a hierarchy in one add_claims call: the task as a whole at the top, narrower claims below, each linked with derived_from. Each claim gets a kind, an integrity level and a verification method.

What to check: compound claims (“handles X and Y”) and vague ones (“works well”). They hide partial failures.

3. Gather evidence

Starting with the highest integrity level, the agent reads the code, runs the tests and runs the program, then records each finding with add_evidence: a precise locator, an anchor whenever the finding is in the repository, a summary of what it observed, and an honest stance. Refuting and neutral findings are recorded too.

For a promised feature that doesn’t exist, it records a zero-hit search anchor plus a file anchor on where the feature would plug in. See Showing that something is absent.

4. Adjudicate

The agent records a FEVER verdict per claim, leaves first, citing the evidence that decides it. refuted and not_enough_info verdicts end with a Fix: line naming one concrete next step. Parent claims get their verdict once their children are judged.

5. Report

The agent summarizes audit_report: totals, every non-empty gap with a remediation, and the traceability matrix if you need a V&V record. On a large catalog it starts from next_steps.

The ranked to-do list

next_steps (MCP) and claims-mcp tldr (CLI) list only what to do next:

claims-mcp --catalog claims.json tldr             # Markdown, 10 items per tier
claims-mcp --catalog claims.json tldr --limit 0   # every item
claims-mcp --catalog claims.json tldr --json      # same data as the next_steps tool

Active leaf claims (no active children) are listed when they need work. A parent is listed only once every active child is done and its own verdict is not (missing, refuted, not_enough_info or stale): its step is then to record the roll-up verdict. Open claims are ranked into tiers:

TierContents
0. STOPShown only when the integrity check fails: make claims-mcp verify pass first (usually by restoring the catalog from git)
1. WrongRefuted claims
2. UnprovenOther open claims at integrity level 3-4 (not_enough_info, unverified, stale, no evidence)
3. Everything elseCounts per area: tag

Tiers 1 and 2 are sorted by integrity level, then id. Each item shows its issue: tags and its next step: the text after the last Fix: in the latest verdict rationale, or else the rationale’s first sentence. Items beyond --limit are counted per area. The command exits 0 whenever the catalog can be read; use verify to gate CI.

For a fixed record a reviewer can read, render a PDF report.

6. Re-audit after fixes

After the code is fixed, the agent calls recheck first. Every anchor that is changed, overturned, missing or commit_missing means that evidence no longer describes the code, so the claim is re-audited. Then it works through refuted, not_enough_info and stale claims: new evidence, new verdict. The earlier verdicts remain as the audit trail.

Commit the catalog

claims.json is meant to be committed with the code, so the audit is reviewed and versioned like everything else. Add claims-mcp verify to CI to catch hand edits; see Checking in CI.

Checking in CI

claims-mcp verify checks every integrity seal in the catalog. It exits 1 when any check fails and names each failing record, so a hand-edited catalog fails the pull request:

claims-mcp --catalog claims.json verify

--json prints the result as JSON. The PDF report takes that output as its integrity input.

A GitHub Actions step

Install a release binary (see Install), then:

- name: Check the claims catalog's integrity seals
  run: claims-mcp --catalog claims.json verify

Also worth gating

claims-mcp recheck exits 1 when any anchor is no longer current. Whether to fail CI on it is a policy choice. Failing makes every change to anchored code trigger a re-audit. Not failing (and reporting it instead) keeps drift visible without blocking merges. claims-mcp tldr always exits 0 while the catalog can be read, so use it for reporting, not gating.

Branches that both write the ledger

Ids come from counters in the ledger, so two branches that both add claims, or both add evidence, hand out the same ids. Git does not always see a conflict: two branches that each add one evidence item to different claims merge cleanly into a ledger with two records of the same id, and the server then refuses to read it. Run claims-mcp verify on the result of the merge, which GitHub’s pull_request event checks out by default, so a PR that would break the ledger fails before it merges. To fix such a branch, replay its records onto main’s ledger with claims-mcp rebase (see Branches that both write the ledger); never edit or merge the file by hand.

Branches that both write the ledger

Claim and evidence ids come from two counters in the ledger. Two branches that both add claims, or both add evidence, therefore hand out the same ids. Merging them in git goes wrong in one of two ways:

  • No conflict. Two branches that each add one evidence item to different claims merge cleanly into a ledger with two EVD-0001 records. claims-mcp verify and every tool then fail with catalog is inconsistent: EVD-0001 appears more than once.
  • A conflict. With unequal counts, or new claims, git reports a conflict in claims.json. Resolving it by hand breaks the seals.

Never resolve either case by editing the file. claims-mcp rebase does it for you instead.

Rebase the ledger during the merge

Bring main into the branch with a merge, and replace whatever git made of claims.json with the branch’s records replayed onto main’s ledger:

git fetch origin
git merge --no-commit origin/main      # claims.json may conflict, or merge "cleanly" and break
git checkout HEAD -- claims.json       # the branch's own ledger, as it was before the merge
git show MERGE_HEAD:claims.json > /tmp/main.json
git show "$(git merge-base HEAD MERGE_HEAD)":claims.json > /tmp/base.json || rm /tmp/base.json
claims-mcp --catalog claims.json rebase --onto /tmp/main.json --base /tmp/base.json
claims-mcp --catalog claims.json verify
git add claims.json && git commit      # finishes the merge

If claims.json did not exist at the merge base, the branch created the ledger. The rm removes the empty file git show left, and a missing --base counts as an empty ledger.

rebase prints which ids changed:

rebased claims.json onto /tmp/main.json: 1 new claim, 2 new evidence items
  CLM-0003 -> CLM-0004
  EVD-0001 -> EVD-0002
  EVD-0002 -> EVD-0003

Update the pull request description with it. --json prints the same mapping as {"claims": [{"from": ..., "to": ...}], "evidence": [...]}.

What it replays

--base is the ledger both sides started from. Everything the branch did since then is replayed onto --onto:

  • New claims get the next free claim ids on --onto, in their original order.
  • New evidence, on new or existing claims, gets the next free evidence ids, in id order.
  • New verdicts on existing claims are appended after any that --onto added, and the evidence ids they cite are renumbered.
  • Field changes to existing claims (update_claim, retire_claim) are applied, unless --onto changed that claim’s fields too. Where both sides made the same change, that’s not a conflict.

derived_from links to renumbered claims are rewritten. Records that existed at --base keep their ids, and anchors keep the commits they were pinned to, so merge the pull request with a merge commit as usual. The result is resealed and written atomically over --catalog.

When it refuses

rebase changes nothing and exits 1 when:

  • any of the three ledgers fails its integrity check, so a hand edit can’t be laundered;
  • the branch or --onto doesn’t descend from --base, which usually means --base is not the merge base;
  • --onto already holds the branch’s new records, because the branch was already rebased onto it;
  • both sides changed the same claim’s fields differently;
  • the branch added evidence or verdicts to a claim that --onto retired;
  • links from the two sides form a derived_from cycle.

To fix a conflict, make the two sides agree through the tools on one of them, then rebase again.

Catching it in CI

Run claims-mcp verify on the merge result, which GitHub’s pull_request event checks out by default (see Checking in CI). A pull request whose ledger would break main then fails before it merges.

PDF reports

A Typst template renders a catalog as a self-contained PDF audit report. It runs outside the server, which never executes commands. It has two detail levels:

  • summary (default): the result and assessment, integrity seal status, findings grouped by area (refuted claims in full, the rest as one-line next steps), coverage by integrity level, and gap counts.
  • full: everything in the summary, plus every finding in full with its evidence, the claim ids in each gap, the claim hierarchy, the traceability matrix, and an evidence appendix with each claim’s full verdict history.

Both collapse long claim id lists into ranges and roll-up evidence into child counts. Both end with a glossary defining every term the report uses (verdicts, stances, integrity levels, verification methods, seals), so readers need not know claims-mcp.

Rendering a report

The template is built into the binary, so every install carries the matching version. In the audited project’s root:

claims-mcp report init            # writes report/claims-report.typ, report/text.typ,
                                  # report/audit-notes.example.toml, and report/audit-notes.toml
# uncomment and fill in report/audit-notes.toml as its comments say, then:
claims-mcp --catalog claims.json verify --json > integrity.json
claims-mcp --catalog claims.json recheck --json > recheck.json
typst compile --root . report/claims-report.typ audit.pdf \
  --input catalog=claims.json \
  --input detail=summary \
  --input commit=$(git rev-parse --short HEAD) \
  --input notes=report/audit-notes.toml \
  --input integrity=integrity.json \
  --input recheck=recheck.json

report init replaces the template files, so they always match the installed binary, but never replaces an existing audit-notes.toml; it prints what it wrote and what it kept. A new audit-notes.toml has every setting of the example commented out, so the example’s invented project can’t end up in a report. Pass a directory (claims-mcp report init docs/audit) to write elsewhere, and adjust the paths above. Use --input detail=full and audit-full.pdf for the full record.

An agent with only the MCP connection can read the same files as resources: claims://report/claims-report.typ, claims://report/text.typ, and claims://report/audit-notes.example.toml, the commented example of the notes file.

Inputs

Paths are relative to --root. All inputs are optional:

InputMeaning
catalogCatalog to report on (default claims.json)
detailsummary (default) or full; any other value is an error
integrityOutput of claims-mcp verify --json for that catalog. Typst can’t check seals itself; without this input, the report says integrity was not checked. A failed check puts a warning on the result and lists the failing records
recheckOutput of claims-mcp recheck --json. Flags anchors that moved, changed or were overturned, on the evidence and in an anchor recheck section; without it, the report says anchors were not rechecked
commitCatalog commit shown on the cover and in the reproduce section
notesTOML sidecar with the audited project’s name and the auditor’s prose: artifact, version, repository, method, assessment, and [[points]] (title, body) for the evaluator. Each audited project needs its own; the commented example explains every key
dateReport date (default today)
artifact, version, repository, method, assessmentOverride the matching sidecar key

Tag a claim issue:N to link it to issue N of repository in the findings.

Writing the notes

The assessment should say what was audited, whether the project meets it, and which findings decide that, citing claim ids and issues. Use [[points]] for caveats the reader should weigh: whether the audit was independent of the builder, heuristic evidence, and hierarchy changes during the audit. Don’t restate counts or verdicts; the report computes them.

Migrating with seal --adopt

Catalogs written before integrity seals existed (schema_version 1) can still be read, but the server refuses to write to them until a maintainer runs:

claims-mcp --catalog claims.json seal --adopt   # then commit claims.json

It seals every unsealed record and labels it adopt. verify and audit_report list adopted records separately from ones the server recorded.

Adoption never re-seals a record whose seal is present but wrong, so it can’t hide an edit to a sealed record. It also refuses when a claim is missing, and when a schema_version 1 file carries seals (a genuine v1 file has none, so the version was lowered by hand). Removing a seal by hand and adopting the record is possible, but the record then shows as adopted in every report and in the git diff.

If two branches changed the same claim, the merge breaks that claim’s seal. Keep one side, and redo the other side’s change through the tools.

Keeping agents away from the file

Agents should never run seal --adopt; the server’s own refusal message says so. To keep agents away from the file entirely, add deny rules to the project’s .claude/settings.json:

{
  "permissions": {
    "deny": ["Edit(**/claims.json)", "Write(**/claims.json)", "Bash(claims-mcp seal:*)"]
  }
}

Claims viewer

The claims viewer is a page for reading a claims.json in the browser. It is for people reviewing an audit: what was claimed, what the verdicts are and why, what evidence backs them, and how the claims fit together.

Try it on claims-mcp’s own ledger.

What it shows

  • Overview: the share of active claims that are supported, counts per verdict, the claims that need attention (refuted, then not enough info, then unverified, highest integrity level first), and progress per area: tag.
  • Claims: a searchable list with filters for verdict, area, kind, level and integrity level, and an option to include retired claims.
  • A claim: its statement, metadata and tags (issue:N tags link to the repository’s issues); its parent and child claims, so you can walk the hierarchy; the verdict history, newest first, with each rationale’s Fix: step highlighted; and its evidence. File anchors link to the exact lines on GitHub at the commit they were pinned to. Search anchors show the git grep command they ran and its hit count.

Every view has its own address, so you can share a link to a filtered list or to one claim, and the browser’s back button works.

Opening a ledger

  • From GitHub: enter owner/name or a repository URL. Under “More options” you can choose a branch, tag or commit (the default branch otherwise) and a catalog path (claims.json otherwise).
  • From a file: choose a claims.json or drop it on the page. The file is read in your browser and never uploaded.

A link can open a ledger directly:

viewer/index.html?repo=owner/name
viewer/index.html?repo=owner/name&ref=release-1.2&path=audit/claims.json

Limits

  • Public repositories only. The viewer fetches files from raw.githubusercontent.com without credentials. It doesn’t accept access tokens: every project site under one github.io account shares a browser origin, so a stored token could be read by other projects’ pages. For a private repository, download the file and open it from disk.
  • It shows the ledger as recorded. It doesn’t check integrity seals, recompute audit gaps or re-run anchors. Use claims-mcp verify, claims-mcp recheck and claims-mcp tldr for those; see Your first audit.
  • Read-only. Change a ledger only through the MCP tools.

MCP tools

All tools return structured JSON. Domain errors, such as a verdict citing the wrong evidence, come back as tool errors (is_error: true) with a clear message, so the agent can recover. Every call verifies the catalog’s integrity seals; while any check fails, write tools refuse and read tools keep working.

ToolPurposeWrites
add_claimCreate one claimyes
add_claimsCreate many claims atomically; @N in derived_from links to an earlier entryyes
get_claimOne claim with evidence, verdict history, current verdict and childrenno
list_claimsFiltered summariesno
update_claimEdit metadata; evidence and verdicts can’t be changedyes
retire_claimWithdraw a claim; it stays so trace links remain validyes
add_evidenceAttach evidence with a locator, an optional anchor and a stanceyes
record_verdictAppend a FEVER verdict that cites evidence backing ityes
traceAncestors and descendants of a claimno
audit_reportTotals, gaps, integrity and the traceability matrixno
next_stepsRanked to-do list: open claims in tiers, each with one next stepno
recheckAnchors that moved, changed or were overturned since they were recordedno

Choosing the ledger

Every tool also takes an optional catalog parameter: the ledger file for that call only, as an absolute path or one relative to the server’s working directory. The call reads or writes that file, and anchors are pinned to HEAD of the git repository that holds it. Without catalog, the call uses the server’s own --catalog.

An MCP client starts claims-mcp once per session, so its --catalog names the ledger of the checkout the session started in. Pass catalog on every call when you work in another checkout, such as a git worktree or a subagent’s branch; otherwise the calls write the session’s ledger and pin that repository’s commit. Because the choice is made per call, not stored in the server, agents sharing one connection can each work on their own ledger at the same time.

Servers before 0.2.5 don’t have catalog and ignore it without an error, writing the session’s ledger instead. To check the server you’re connected to, call a read tool with catalog set to a ledger in a directory that doesn’t exist, such as /nonexistent-claims-probe/claims.json: a server that supports the parameter fails to open it, with an error that names that path, and an older one answers as if it weren’t there. Treat an error that doesn’t name the probe path as inconclusive: an older server reading its own ledger can fail for unrelated reasons. The tool schema your client shows isn’t proof, because a client can keep the schema from before the server was upgraded.

add_claim

ParameterRequiredMeaning
statementyesOne atomic, falsifiable assertion
kindyesSee kinds
levelyessystem, software, component, unit or artifact
verification_methodyesinspection, analysis, demonstration, test or formal_proof
integrity_levelno1-4
sourcenoWhere the claim came from
source_anchorno{"path", "lines": {"start", "end"}}; pinned to the current commit
derived_fromnoParent claim ids
artifactsnoPaths the claim is about
tagsnoLabels, such as area:auth or issue:12

Returns the full claim with its new id.

add_claims

claims: a list of entries shaped like add_claim’s parameters. All are created or none are. In derived_from, "@N" refers to the entry at 0-based index N of the same batch, which must come earlier. Returns the created claims in order.

get_claim

id. Returns the claim plus current_verdict and children (ids of active claims whose derived_from contains it).

list_claims

All filters are optional, and all set filters must match: verdict (unverified, supported, refuted, not_enough_info), kind, level, tag (exact), artifact (substring of any artifact), text (case-insensitive substring of the statement), include_retired (default false). Returns summaries: id, statement, kind, level, integrity_level, current_verdict, evidence_count, retired.

update_claim

id, plus any of statement, kind, level, verification_method, integrity_level, source, source_anchor, derived_from, artifacts, tags. Omitted or null fields are unchanged. List fields replace the whole list ([] empties it). source: "" removes the source. integrity_level and source_anchor can be replaced but not removed. A new derived_from link must point at an active claim. Retired claims can’t be updated.

retire_claim

id, reason. Retired claims stay in the catalog but accept no further evidence, verdicts or updates.

add_evidence

ParameterRequiredMeaning
claim_idyesThe claim the evidence is about
kindyestest, inspection, analysis, demonstration, proof, log or other
locatoryespath:line, test name, command or URL
summaryyesWhat was observed
stanceyessupports, refutes or neutral
anchorno{"file": {"path", "lines"?}} or {"search": {"pattern", "case_insensitive"?, "scope", "hits"?}}; see Anchors

Returns the evidence item with its id and pinned anchor.

record_verdict

ParameterRequiredMeaning
claim_idyesThe claim being judged
verdictyessupported, refuted or not_enough_info
rationaleyesWhy the evidence leads to this verdict
auditoryesPerson or model id
evidence_idsnoEvidence on this claim that the verdict relies on

supported must cite supports evidence, and refuted must cite refutes evidence. See Verdicts. Returns the claim with the verdict appended.

trace

id, direction (up, down or both, default both), max_depth (default unlimited). Returns breadth-first ancestors and descendants lists of {id, statement, current_verdict, depth, retired}.

audit_report

include_matrix (default true). Returns totals, gaps, the integrity check and the traceability matrix; see Traceability.

next_steps

limit: items listed in each of tiers 1 and 2 (default 10; 0 lists everything). Same data as claims-mcp tldr --json; see the ranked to-do list.

recheck

No parameters besides catalog. Returns head, checked, current, and items for every anchor on an active claim that is not current, each with claim, role, evidence, stance, anchor, status and detail. See Recheck.

Resources

The server also offers read-only resources: the PDF report’s files under claims://report/ (claims-report.typ, text.typ and audit-notes.example.toml). See PDF reports.

Command line

claims-mcp [--catalog PATH] [SUBCOMMAND]

Without a subcommand, claims-mcp runs the MCP server on stdio. MCP clients such as Claude Code normally start it.

OptionEnvDefaultMeaning
--catalogCLAIMS_CATALOGclaims.jsonCatalog file; created on first write
RUST_LOGwarnLog filter; logs go to stderr

The catalog is re-read on every call and written atomically (temporary file, fsync, rename) under an exclusive lock on .<name>.lock. Git checkouts and several server instances sharing one file are safe. Add .<name>.lock and .<name>.tmp to .gitignore.

Subcommands

The subcommands are for people and CI; agents use the MCP tools.

SubcommandPurposeExit code
(none)Run the MCP server on stdio
verify [--json]Check every integrity seal1 if any check fails
tldr [--limit N] [--json]Print the ranked next steps; --json matches the next_steps tool0 whenever the catalog can be read
recheck [--json]Compare every anchor with HEAD; --json matches the recheck tool. See Recheck1 if any anchor is not current
rebase --onto FILE --base FILE [--json]Replay the records this branch added since --base onto --onto (such as main’s ledger) under new ids, rewrite --catalog, and print the old-to-new ids. See Branches that both write the ledger1 if it refuses, with nothing changed
seal --adoptSeal unsealed records once, to migrate a catalog; maintainers only
report init [DIR]Write the PDF report template and a notes file to DIR (default report)
schema [--versioned]Print the catalog JSON Schema; --versioned uses its pinned URL as $id

--catalog is global, so it can go before or after the subcommand. In the MCP server it is the default ledger: a tool call can name another with its catalog parameter.

Catalog format

The catalog is one JSON file, claims.json by default, meant to be committed alongside the code. Change it only through the tools: hand edits break its seals.

{
  "schema_version": 2,
  "next_claim_seq": 8,
  "next_claim_seq_seal": { "by": "server", "mac": "<64 hex digits>" },
  "next_evidence_seq": 15,
  "next_evidence_seq_seal": { "by": "server", "mac": "<64 hex digits>" },
  "claims": [
    {
      "id": "CLM-0007",
      "statement": "parse_config rejects files larger than 1 MiB",
      "kind": "functional",
      "level": "software",
      "integrity_level": 3,
      "verification_method": "test",
      "source": "prompt: task-42 / spec.md#limits",
      "derived_from": ["CLM-0002"],
      "artifacts": ["src/config.rs"],
      "tags": ["config"],
      "status": { "state": "active" },
      "evidence": [
        {
          "id": "EVD-0014",
          "kind": "test",
          "locator": "tests/config.rs:88 oversized_file_rejected",
          "summary": "test passes; returns ConfigError::TooLarge",
          "stance": "supports",
          "recorded_at": "2026-10-23T14:02:11Z",
          "seal": { "by": "server", "mac": "<64 hex digits>" }
        }
      ],
      "verdicts": [
        {
          "verdict": "supported",
          "rationale": "Direct test covers the boundary and passes.",
          "auditor": "example-model",
          "evidence_ids": ["EVD-0014"],
          "recorded_at": "2026-10-23T14:03:40Z",
          "seal": { "by": "server", "mac": "<64 hex digits>" }
        }
      ],
      "created_at": "2026-10-23T13:58:00Z",
      "updated_at": "2026-10-23T14:03:40Z",
      "seal": { "by": "server", "mac": "<64 hex digits>" }
    }
  ]
}
  • Ids look like CLM-0007 and EVD-0014. They are never reused: the counters only increase, and evidence ids are unique across the whole catalog.
  • Deterministic output: two-space indentation and a trailing newline, claims sorted by id, fields in a fixed order, so diffs stay small.
  • Optional fields (integrity_level, source, source_anchor, evidence anchor) are omitted when absent. Empty lists are always written.
  • Anchors, when present, are stored with the commit the server pinned, a dirty flag, and a search’s hits; see Anchors and recheck.
  • Retired claims have "status": {"state": "retired", "reason": ..., "retired_at": ...}.
  • schema_version is 2. A version 1 file (written before seals) is read, but the server won’t write to it until it is migrated. Any other value is an error, and the file is not touched.

The machine-readable catalog schema documents every field and enum. The design spec’s section 3 provides additional background.

Catalog JSON Schema

The generated JSON Schema is the official machine-readable definition of claims.json. It uses JSON Schema 2020-12 and describes catalog schema version 2. The stable URL always describes the latest catalog format:

https://hypotheosis.github.io/claims-mcp/schema/claims.schema.json

Consumers that need a format-version pin can use schema/v2/claims.schema.json. Every GitHub release also contains claims.schema.json, so consumers can pin to a claims-mcp release.

To associate claims.json with the schema in VS Code, add this to settings.json:

{
  "json.schemas": [
    {
      "fileMatch": ["claims.json"],
      "url": "https://hypotheosis.github.io/claims-mcp/schema/claims.schema.json"
    }
  ]
}

Maintainers regenerate both committed copies with just schema. The parity test fails if the Rust model and committed schema differ.

Rules beyond JSON Schema

The server enforces relationships and history that a schema cannot express:

  • Integrity seals authenticate records and counters, bind evidence to its claim and position, and chain each verdict to the preceding verdict.
  • Claim and evidence ids are never reused; both sequence counters only increase.
  • Every derived_from id exists, cannot refer to a claim newly retired by the same operation, and the resulting graph has no cycle.
  • A supported verdict cites at least one supports evidence item, and a refuted verdict cites at least one refutes item.
  • not_enough_info may omit evidence; other verdicts may not.
  • The server, rather than tool input, sets anchor commit, dirty, and hits fields.

Security

Report vulnerabilities privately, through GitHub’s private vulnerability reporting, not in a public issue. The full policy, including supported versions, is in SECURITY.md.

Two properties matter when you decide how far to trust a catalog:

  • Seals are a deterrent, not cryptographic security. The HMAC key is in the public source. See Integrity seals.
  • The server doesn’t execute your code. It only reads and writes the catalog and runs read-only git queries for anchors. Search scopes and file paths outside the repository are rejected.
  • A tool call can name its own ledger. The catalog parameter lets the client point a call at any path the server’s user can write, where it creates the ledger and its .<name>.lock sidecar. That is no more than the client that started the server can already do, but don’t expose the server to callers you wouldn’t let write files.

Development

claims-mcp is a Rust crate (minimum Rust version in Cargo.toml’s rust-version) built on the rmcp crate. Common tasks use just:

just fmt          # cargo fmt
just clippy       # cargo clippy --all-targets -- -D warnings
just test         # cargo test
just report       # render audit.pdf from the repository's own claims.json
just report full  # render audit-full.pdf, every record
just docs         # build this site into docs/site/book
just docs-serve   # serve it with live reload

The repository’s own .mcp.json runs the server with cargo run --release, so the first start compiles it. Run cargo build --release once beforehand to avoid a client startup timeout.

claims-mcp audits itself: claims.json in the repository holds claims about its own specification and documentation, including this site.

Template checks

cargo test typst_template checks that the PDF template’s totals, gaps and matrix match audit_report in both detail levels, and that its text helpers pass report/text-tests.typ. When typst is not installed, that check is skipped and still counts as passed. Set REQUIRE_TYPST=1 (as CI does) to make a missing typst fail the tests instead.

Continuous integration

.github/workflows/ci.yml runs on every pull request and push to main:

  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, a build with RUSTFLAGS=-D warnings, and cargo test with Typst installed and REQUIRE_TYPST=1;
  • claims-mcp verify, which fails if claims.json was edited outside the tools;
  • cargo test on Windows and macOS, and cargo check on the minimum supported Rust version;
  • just release-test and shellcheck on the release scripts, and on a release/v* pull request a check that the branch names the Cargo.toml version (see Releasing);
  • just report and just report full on the PR head commit. Both PDFs are uploaded as the run’s audit-report-pr-<number>-<commit> artifact; open the run’s summary page to download it.

.github/workflows/docs.yml builds this site and checks its internal links on pull requests that touch docs/site/, and deploys it to GitHub Pages from main.

Releasing

Releases are cut by three just recipes, the same way every time, with no hand-written text. They need cargo-release and an authenticated gh.

just release-prepare patch   # or minor, major, or an exact version such as 0.3.0
just release-ship

just release-prepare <level> runs cargo-release with release.toml. It makes one local commit, chore: Release vX.Y.Z, which sets the version in Cargo.toml, Cargo.lock and report/audit-notes.toml and adds the release’s changelog entry, and an annotated tag vX.Y.Z on it. It pushes nothing. It refuses to run off main, with a dirty working tree, or when local main is not exactly origin/main.

just release-ship pushes the release commit to release/vX.Y.Z and opens a pull request, then:

  1. waits for its checks, and stops if one fails;
  2. waits for the review bot’s review, and stops if any review thread is unresolved;
  3. closes the pull request and stops if main moved meanwhile;
  4. merges it with a merge commit, moves the local tag to that merge commit, pushes the tag, and fast-forwards local main. The tag starts release.yml, which publishes the release.

When it stops on a check or a review, the pull request stays open and no tag is pushed (the local tag from just release-prepare stays where it is). If the pull request merges but the tag push fails, it stops and prints the commands that finish the release: git push origin refs/tags/vX.Y.Z && git merge --ff-only origin/main. It refuses to start unless HEAD carries the tag for the Cargo.toml version, that tag is not on origin, and HEAD is a single-parent commit directly on the current origin/main.

just release-abort abandons a release that has not been published: it closes the release pull request and deletes its branch if they exist, deletes the local tag, and resets local main to origin/main. It refuses off main, with a dirty working tree, when HEAD does not carry the version’s tag, when the tag is already on origin or the pull request has merged, and when local main has commits besides the release commit that are not on origin. After a stopped ship, land the fix through its own pull request, then abort, prepare and ship again.

Release notes

scripts/release-notes.sh <from> <to> (or just release-notes <from> <to>) prints the notes from git history alone: one line per pull request merged into main in the range, linked to it and grouped by the conventional-commit type of its title (Breaking changes, Features, Fixes, Documentation, Dependencies, and Other, as written). The release’s own pull request is left out. just release-prepare writes them, from the previous tag to HEAD, as the changelog entry # Release vX.Y.Z; release.yml publishes them, from the previous tag to the new one, as the GitHub release body. The two cover the same pull requests, so they are the same text.

report/audit-notes.toml needs no edit per release: its assessment is fixed, and just release-prepare sets its version. The report computes counts, verdicts and gaps from the ledger.

Checks

.github/workflows/release.yml checks that the tag equals v plus the Cargo.toml version and points at a commit on main (and fails without publishing otherwise). It reruns CI on the tagged commit, builds the five archives listed under Install, and publishes a GitHub release with the archives, their .sha256 files and the audit report. A pull request that changes release.yml runs the builds as a dry run without publishing. CI also fails a release/v* pull request whose branch does not name its Cargo.toml version.

just release-test tests the scripts against throwaway git repositories and a fake gh, including the real cargo-release when it is installed. CI runs it in the release scripts job.

Release tags (v*) can’t be moved or deleted once pushed, so a bad release is fixed with a new version. That includes a tag that fails the release checks: its version number is used up even though nothing was published.

The release also includes claims.schema.json. If the catalog format changes, bump SCHEMA_VERSION in src/catalog.rs and run just schema in the same pull request. That command regenerates the latest schema and creates the current format-version-pinned copy without changing older pins.

Merging pull requests

Merge pull requests with a merge commit, not a squash or rebase merge. The anchors in claims.json pin the commits they were recorded on, which for a pull request are branch commits. A squash or rebase merge rewrites them, and recheck would then report those anchors as commit_missing; see Anchors recorded on a branch.