CL
Clausgap
/Docs

Verifying evidence

Clausgap records every governed agent decision in a tamper-evident hash chain. This page covers checking that chain, and exporting an evidence package that someone outside your organisation — an auditor, a regulator, an insurer, a court — can verify offline, with no Clausgap account, no network access and no need to trust the Clausgap server.

1. Check the live chain

bash
curl -H "Authorization: Bearer $CLAUSGAP_API_KEY" https://control.example.com/v1/forensics/verify
curl -H "Authorization: Bearer $CLAUSGAP_API_KEY" https://control.example.com/v1/audit/verify

/v1/forensics/verify covers agent decisions; /v1/audit/verify covers administrative actions (policy changes, votes, exports). Each recomputes every hash in the tenant's chain from the first record.

FieldMeaning
verified / validtrue: no record was altered, inserted or removed.
lineartrue: one unbroken chain with no forks.
first_mismatch_id / break_at_event_idThe first record that doesn't recompute: where tampering starts.

Run it on a schedule. A false means someone changed the database directly; ordinary use can't produce one.

2. Export an evidence package

In the dashboard, go to Forensics → Export evidence. Or call the API as a signed-in user:

json
// POST /v1/forensics/export
{ "purpose": "regulatory_inquiry", "from": "2026-09-01T00:00:00Z", "to": "2026-10-01T00:00:00Z" }

// → 201
{
  "package_id": "epkg_…",
  "record_count": 1520,
  "chain_intact": true,
  "package_hash": "sha256:2365cc1e…",
  "public_key_fingerprint": "sha256:c3385619…"
}

purpose is required and recorded. from and to default to the last month; add sequence_id to export one agent session. A package holds up to 10,000 records — export a larger window in parts.

✦

Write down package_hash. It identifies this exact package. Later, --expect-hash proves the file someone hands you is that package.

chain_intact: false means the server found records whose hashes don't recompute. The package is still exported — the break is itself evidence — and the verifier reports it.

Download it with GET /v1/forensics/packages/{package_id}/download or the dashboard's Download link. A package is stored exactly as it was signed, so every download returns the same bytes — after restarts, upgrades and any later change to the database.

3. Get the control plane's public key

The verifier needs to know who should have signed the package. Fetch the key once, from a source you trust, and keep it with your records:

bash
curl -o clausgap.pub https://control.example.com/pubkey
shasum -a 256 clausgap.pub      # → the fingerprint, as in public_key_fingerprint

/pubkey serves the raw 32-byte Ed25519 key. To hand someone a short identifier instead, give them the fingerprint; they pin it with --fingerprint.

4. Verify a package offline

clausgap-verify is a small standalone program, shipped in the control-plane image. Run it there with networking disabled:

bash
docker run --rm --network none -v "$PWD:/w:ro" --entrypoint /clausgap-verify \
  <your clausgap-control image> --pubkey /w/clausgap.pub --expect-hash sha256:2365cc1e… /w/epkg_….json
text
  PASS  format           evidence package version 2, 15 records
  PASS  signer           signed by the pinned key sha256:c3385619…
  PASS  signature        Ed25519 signature verifies over the whole package (sha256:2365cc1e…)
  PASS  expected-hash    matches the package hash recorded at export
  PASS  record-hashes    all 15 records recompute to their chain hash
  PASS  chain-links      every record links to the one before it — none missing between the first and last
  PASS  merkle-root      Merkle root cdae0c5c… recomputed from the records
  info  timestamp        no external timestamp token attached

VERIFIED — the package is intact and was signed by the pinned key.
ExitVerdictMeaning
0VERIFIEDEvery check passed against the key you pinned.
1FAILEDDon't rely on the package; the FAIL lines say why.
2—Usage error, or a file couldn't be read.
3UNPINNEDInternally consistent, but you gave no --pubkey or --fingerprint, so nothing ties it to your Clausgap.

Add --json for a machine-readable report.

What each check proves

CheckFails when
formatThe file isn't a version 2 package, or contains fields the format doesn't define.
signerThe package was signed by a key other than the one you pinned.
signatureAnything in the package changed after signing: any record, field, the purpose, the scope or the custody entries.
expected-hashThe file isn't the package recorded at export.
record-hashesA record's content doesn't match the hash it was chained with when recorded — this catches database edits made before export, even though the server then signed the package.
chain-linksFor a time-window package: a record is missing between two others, or records are out of order. (A single-session package skips other sessions' records, so gaps there are expected and reported as info.)
merkle-rootThe record count, Merkle root or first and last hashes don't match the records.
chain-at-exportThe server's own check at export found a broken chain.

What this does and doesn't prove

  • Edits after export can't go unnoticed. Changing the package breaks the signature; re-signing it needs the control plane's private key, and a package signed with another key fails signer.
  • Database edits before export are caught by record-hashes, and packages exported before an edit are stored unchanged and still verify — the original evidence survives.
  • Not covered by a record's hash: agent_role, session_id, tool_name and severity. The package signature covers them, but only from the moment of export.
  • Someone with database write access could rewrite the tail of the chain consistently — the chain isn't keyed. What anchors history against that is evidence kept outside the database: packages you've already exported and their package_hash, and an external timestamp on each package. Export regularly (monthly, say) and store packages and hashes where the Clausgap operators can't write.
  • External timestamps (forensics setting external_timestamp) attach your timestamp authority's token for the package hash. The verifier reports that one is present but doesn't check it — the request is a simplified HTTP one, not RFC 3161 — so check it with your authority.
  • Signing key rotation: keep every public key you've used, to verify packages signed with older ones.

Package format (version 2)

For anyone writing their own verifier:

Signature

Ed25519 over sha256(C), where C is the package JSON without its signature and timestamp_proof members, in canonical form: object keys sorted, no insignificant whitespace, strings JSON-escaped without HTML escaping, numbers in shortest round-trip form. signature.package_hash is sha256: plus the hex digest; signature.public_key is the base64 raw key and public_key_fingerprint is sha256: plus its hex.

Record chain hash

BLAKE3-256, hex-encoded, over these lines, each ending in a newline:

text
prev:<prev_chain_hash>          (empty for the first record in the tenant's chain)
tenant:<tenant_id>
sequence:<sequence_id>
agent:<agent_id>
action:<action>
input_hash:<input_hash>
output_hash:<output_hash>
confidence:<confidence to 2 decimal places, or empty>
policy_snapshot:<policy_snapshot>
model_id:<model_id>
context:<context_summary as JSON with sorted keys>
created_at:<UTC, YYYY-MM-DDTHH:MM:SS.ffffffZ>

Merkle root

Leaves are BLAKE3(0x00 ‖ chain_hash); each parent is BLAKE3(0x01 ‖ left "\n" right "\n"); an odd node is paired with itself.

To see all of this run end to end — export, offline verification, a restart, and tampering with the package and the database — use the evidence evaluation.