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
- Why claims-mcp explains the problems it solves, and how it works alongside formal verification.
- Install claims-mcp to try it.
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 answered | Does this function meet this spec, for every input? | What did we claim, what evidence backs each claim, and what was the verdict? |
| Scope | A few critical functions: narrow and deep | A whole system and its documentation: broad |
| Who judges | A proof checker’s kernel | An auditor, citing recorded evidence |
| Strength of “yes” | Mathematical, given the spec and the trusted base | As strong as the cited evidence |
| When code changes | The proof breaks and must be repaired | recheck flags anchors that moved, changed or were overturned |
| Cost | High; needs proof-assistant skills | Low |
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 afileanchor on the theorem in the.leanfile and the proof checker’s output as the locator. -
No escape hatches: a
searchanchor, 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, becausegit grep -Euses the platform’s regex library and not every platform supports\b. It also catchesprivateandprotectedaxioms. -
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:
| Question | As 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
- Aeneas: https://github.com/AeneasVerif/aeneas
- Lean: https://lean-lang.org
- Ben Lorica and Leo de Moura, “When AI Writes the Code, Who Proves It Works?”, The Data Exchange, October 2026: https://thedataexchange.media/leo-de-moura-lean-aws/
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.
supportedmeans an auditor looked at cited evidence and concluded the claim holds. The server enforces that the evidence exists and points the right way (supportedmust citesupportsevidence). 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_reportflags some of this (high-integrity claims checked only by inspection show up asrigorgaps), 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
gitqueries 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
recheckneed 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.
| Weak | Strong |
|---|---|
| “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
| Field | Meaning |
|---|---|
id | CLM-0007; assigned by the server and never reused |
statement | The assertion; must not be empty |
kind | What sort of property it asserts (below) |
level | Where it sits in the requirements hierarchy (below) |
verification_method | How it is meant to be verified; see Integrity levels and methods |
integrity_level | Optional, 1-4; see Integrity levels and methods |
source | Optional free text: where the claim came from (prompt, spec section, ticket) |
source_anchor | Optional file anchor on the exact text that makes the claim |
derived_from | Parent claims; see Traceability |
artifacts | Paths the claim is about, so list_claims can find it by artifact |
tags | Free-form labels; see below |
status | active, or retired with a reason and time |
Duplicate tags and artifacts are removed (the first occurrence wins).
Kinds
kind | Asserts |
|---|---|
functional | Observable behaviour or output |
non_functional | A quality attribute such as usability or maintainability |
interface | A contract with another component, an API or a user |
performance | A latency, throughput or resource bound |
safety | Protection against harm to people or property |
security | A confidentiality, integrity or availability property |
documentation | A statement about documentation content |
constraint | An 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_stepsandclaims-mcp tldrcount 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
| Field | Meaning |
|---|---|
id | EVD-0014; unique across the whole catalog and never reused |
kind | The form of the evidence (below) |
locator | Where to find it, precisely enough to repeat: src/store.rs:88, a test name, the exact command run, or a URL |
summary | What was observed, not what was concluded |
stance | supports, refutes or neutral toward the claim |
anchor | Optional machine-checkable location, pinned to a commit; see Anchors and recheck |
recorded_at | Set by the server |
Kinds
kind | Is |
|---|---|
test | The result of an executed test |
inspection | An observation from reading an artifact |
analysis | The result of reasoning or static analysis |
demonstration | An observation from running the system |
proof | A proof artifact or proof-checker output |
log | A captured log or command output |
other | Anything 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:
| Verdict | Means | Must cite |
|---|---|---|
supported | Cited evidence shows the claim holds | At least one supports evidence item |
refuted | Cited evidence shows the claim does not hold | At least one refutes evidence item |
not_enough_info | The evidence can’t decide the claim | Nothing 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.
| Level | Use when failure would cause |
|---|---|
| 4 | Safety harm, a security breach or data loss |
| 3 | Loss of a core function the source explicitly asked for |
| 2 | A degraded or secondary function |
| 1 | Cosmetic, 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:
| Method | Means |
|---|---|
inspection | Reading code, documents or output |
analysis | Reasoning, calculation or static analysis |
demonstration | Running the system and observing behaviour without instrumentation |
test | Executing a defined test with pass/fail criteria |
formal_proof | A 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_fromid 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 asdangling_links. - Links never form a cycle, including a claim linking to itself.
add_claimsregisters 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:
Gap Meaning no_evidenceNo evidence at all unverifiedNo verdict refutedCurrent verdict is refutednot_enough_infoCurrent verdict is not_enough_infostaleEvidence was recorded after the latest verdict rigorIntegrity level 3-4 verified only by inspection decompositionHas active children, and at least one isn’t supporteddangling_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_matrixis 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_reportkeep 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_versionlowered 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_versionand 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.
| Anchor | Shape | Use 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:
| Status | Meaning |
|---|---|
moved | The anchored lines are unchanged but at new line numbers (shown) |
changed | The anchored lines or file changed, or a search’s hit count changed |
overturned | An absence search now matches, or a matching search now finds nothing |
missing | The file no longer exists |
commit_missing | The pinned commit was removed by a history rewrite |
unpinned | Recorded 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):
| Platform | Archive |
|---|---|
| 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_64 | claims-mcp-<version>-x86_64-pc-windows-msvc.zip |
| macOS Intel | claims-mcp-<version>-x86_64-apple-darwin.tar.gz |
| macOS Apple Silicon | claims-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:
| Tier | Contents |
|---|---|
| 0. STOP | Shown only when the integrity check fails: make claims-mcp verify pass first (usually by restoring the catalog from git) |
| 1. Wrong | Refuted claims |
| 2. Unproven | Other open claims at integrity level 3-4 (not_enough_info, unverified, stale, no evidence) |
| 3. Everything else | Counts 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-0001records.claims-mcp verifyand every tool then fail withcatalog 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
--ontoadded, and the evidence ids they cite are renumbered. - Field changes to existing claims (
update_claim,retire_claim) are applied, unless--ontochanged 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
--ontodoesn’t descend from--base, which usually means--baseis not the merge base; --ontoalready 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
--ontoretired; - links from the two sides form a
derived_fromcycle.
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:
| Input | Meaning |
|---|---|
catalog | Catalog to report on (default claims.json) |
detail | summary (default) or full; any other value is an error |
integrity | Output 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 |
recheck | Output 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 |
commit | Catalog commit shown on the cover and in the reproduce section |
notes | TOML 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 |
date | Report date (default today) |
artifact, version, repository, method, assessment | Override 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:Ntags 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’sFix:step highlighted; and its evidence. File anchors link to the exact lines on GitHub at the commit they were pinned to. Search anchors show thegit grepcommand 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/nameor a repository URL. Under “More options” you can choose a branch, tag or commit (the default branch otherwise) and a catalog path (claims.jsonotherwise). - From a file: choose a
claims.jsonor 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.comwithout credentials. It doesn’t accept access tokens: every project site under onegithub.ioaccount 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 recheckandclaims-mcp tldrfor 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.
| Tool | Purpose | Writes |
|---|---|---|
add_claim | Create one claim | yes |
add_claims | Create many claims atomically; @N in derived_from links to an earlier entry | yes |
get_claim | One claim with evidence, verdict history, current verdict and children | no |
list_claims | Filtered summaries | no |
update_claim | Edit metadata; evidence and verdicts can’t be changed | yes |
retire_claim | Withdraw a claim; it stays so trace links remain valid | yes |
add_evidence | Attach evidence with a locator, an optional anchor and a stance | yes |
record_verdict | Append a FEVER verdict that cites evidence backing it | yes |
trace | Ancestors and descendants of a claim | no |
audit_report | Totals, gaps, integrity and the traceability matrix | no |
next_steps | Ranked to-do list: open claims in tiers, each with one next step | no |
recheck | Anchors that moved, changed or were overturned since they were recorded | no |
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
| Parameter | Required | Meaning |
|---|---|---|
statement | yes | One atomic, falsifiable assertion |
kind | yes | See kinds |
level | yes | system, software, component, unit or artifact |
verification_method | yes | inspection, analysis, demonstration, test or formal_proof |
integrity_level | no | 1-4 |
source | no | Where the claim came from |
source_anchor | no | {"path", "lines": {"start", "end"}}; pinned to the current commit |
derived_from | no | Parent claim ids |
artifacts | no | Paths the claim is about |
tags | no | Labels, 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
| Parameter | Required | Meaning |
|---|---|---|
claim_id | yes | The claim the evidence is about |
kind | yes | test, inspection, analysis, demonstration, proof, log or other |
locator | yes | path:line, test name, command or URL |
summary | yes | What was observed |
stance | yes | supports, refutes or neutral |
anchor | no | {"file": {"path", "lines"?}} or {"search": {"pattern", "case_insensitive"?, "scope", "hits"?}}; see Anchors |
Returns the evidence item with its id and pinned anchor.
record_verdict
| Parameter | Required | Meaning |
|---|---|---|
claim_id | yes | The claim being judged |
verdict | yes | supported, refuted or not_enough_info |
rationale | yes | Why the evidence leads to this verdict |
auditor | yes | Person or model id |
evidence_ids | no | Evidence 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.
| Option | Env | Default | Meaning |
|---|---|---|---|
--catalog | CLAIMS_CATALOG | claims.json | Catalog file; created on first write |
RUST_LOG | warn | Log 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.
| Subcommand | Purpose | Exit code |
|---|---|---|
| (none) | Run the MCP server on stdio | |
verify [--json] | Check every integrity seal | 1 if any check fails |
tldr [--limit N] [--json] | Print the ranked next steps; --json matches the next_steps tool | 0 whenever the catalog can be read |
recheck [--json] | Compare every anchor with HEAD; --json matches the recheck tool. See Recheck | 1 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 ledger | 1 if it refuses, with nothing changed |
seal --adopt | Seal 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-0007andEVD-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, evidenceanchor) are omitted when absent. Empty lists are always written. - Anchors, when present, are stored with the commit the server pinned, a
dirtyflag, and a search’shits; see Anchors and recheck. - Retired claims have
"status": {"state": "retired", "reason": ..., "retired_at": ...}. schema_versionis 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_fromid exists, cannot refer to a claim newly retired by the same operation, and the resulting graph has no cycle. - A
supportedverdict cites at least onesupportsevidence item, and arefutedverdict cites at least onerefutesitem. not_enough_infomay omit evidence; other verdicts may not.- The server, rather than tool input, sets anchor
commit,dirty, andhitsfields.
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
gitqueries for anchors. Search scopes and file paths outside the repository are rejected. - A tool call can name its own ledger. The
catalogparameter lets the client point a call at any path the server’s user can write, where it creates the ledger and its.<name>.locksidecar. 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 withRUSTFLAGS=-D warnings, andcargo testwith Typst installed andREQUIRE_TYPST=1;claims-mcp verify, which fails ifclaims.jsonwas edited outside the tools;cargo teston Windows and macOS, andcargo checkon the minimum supported Rust version;just release-testandshellcheckon the release scripts, and on arelease/v*pull request a check that the branch names theCargo.tomlversion (see Releasing);just reportandjust report fullon the PR head commit. Both PDFs are uploaded as the run’saudit-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:
- waits for its checks, and stops if one fails;
- waits for the review bot’s review, and stops if any review thread is unresolved;
- closes the pull request and stops if
mainmoved meanwhile; - merges it with a merge commit, moves the local tag to that merge commit, pushes the tag, and
fast-forwards local
main. The tag startsrelease.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.