Prove · Verify a record
Check a merge record without trusting the claimant
RepoOps signs a record of who authored each merged change. Verify a record is where that signature is checked: paste a commit SHA, a digest or a pull request number and read the verdict the server computed. The page does no cryptography of its own, and it never lets a valid signature stand in for a signer you can name.
For: the engineer checking one merged change, and the reviewer outside the install who was handed the record
What it does, and why it helps
An attestation is an in-toto statement carrying the graded origin band, the model, the window-scoped cost, the approver, the review state and the ledger chain head that was live when it was signed. The page resolves one by digest, commit SHA or pull request number, and GET /api/attestation/verify runs four checks against it: the statement's shape, the digest recomputed from the statement, the signature, and the bound chain head compared with the live head. A missing check is never a pass. A statement somebody altered fails on the digest, and an unknown token reads No attestation found rather than anything greener.
The second half is the part that gets skipped. A record carrying an Ed25519 DSSE envelope verifies against the public key inside that envelope, and this page pins no key, so anyone with a keypair they generated can produce a record whose signature checks out. That case reads Verified (signer not pinned) in amber, with the sentence that says why. It turns green when the expected public key is pinned, or when the digest is proven present in the externally witnessed transparency log. Below the lookup sit three more panels: the normalized authorship record per merged pull request, the attestations signed automatically on merge, and the portable trust credential with the pinned-issuer registry that authenticates a credential from another organization.
The pain. A merged change says an agent wrote it. The claim and the evidence for the claim come from the same place, so a reader who does not already trust the team has nothing to check.
The point of view. Signature validity and signer identity are two questions, and collapsing them is how a proof page starts lying. Check the record, then check who signed it, and let the verdict stay amber until the second one is answered.
What gets easier. Reading the answer. Four labelled checks, a trust line that names which half is missing, an inclusion row with the leaf index and whether the checkpoint was witnessed, and the bound chain head shown beside the live one.
When it helps. Someone questions the authorship of a merged change, an auditor asks for evidence you cannot produce by asserting it, or a credential arrives from another organization and you have to decide what it proves.
Its limits. It checks records, it does not produce them: an install with nothing emitted has nothing to look up. It cannot tell you a record is complete, only that the one in front of you is intact. Without a pinned key or a witnessed log entry, a valid signature says nothing about who signed. Retraction, deletion and key rotation are not provided.
Understand it in 30 seconds
Read the narration
- 0:00 A merged change claims an author.
- 0:02 Nobody has checked the claim.
- 0:06 RepoOps recomputes the digest and checks the signature.
- 0:09 Tampering fails honestly.
- 0:13 A valid signature is not a signer you know.
- 0:16 Pin the key, or find the digest in the witnessed log.
- 0:23 Hand over the record.
- 0:24 An offline check reaches the same verdict, no server needed.
Synthetic example. Read the guide
Where to find it
Where to find it
- Desktop:
localhost:4000, then Attribution in the sidebar, then Verify a record under All tools, in the Attestation group. - Hosted:
repoops.ai/team/verify-attestation, from Attribution in the sidebar, then Verify a record under All tools, in the Attestation group. - Keyboard: ⌘ K, then type “Verify a record”.
When to use it
A merged pull request whose authorship is in question
Situation. Attestation is armed, the install has been emitting on merge, and someone asks who authored pull request 4312.
What you do. Open Verify a record, pick the repository, type the pull request number into the field marked Commit SHA, token (digest) or PR number, and press Verify. A short integer is read as a pull request number, a 64-character hex string as a digest, anything else as a commit SHA.
What you see. The banner reads Verified or names the failure. The card shows the origin band with its confidence and signals, the model, the window-scoped cost, the approver, the review state and the session. Four badges read shape, digest, signature and ledger anchor. The inclusion row gives the leaf index and whether the checkpoint was witnessed.
What it establishes. That the record is intact and was signed by whatever key the banner names. It does not establish that the origin band is correct, only that nobody altered the statement after signing.
A record handed to someone outside the install
Situation. An auditor wants to reach the verdict themselves, without an account on your dashboard and without your server being up.
What you do. Send them the record and, on a separate channel, the install's Ed25519 public key. They run repoops verify against the file and pin the signer with the expected key. Adding the transparency response and the external witness key also proves the record is in the witnessed log.
What you see. One line per record, either verified, verified but UNTRUSTED, or NOT verified, then the reason. The closing line counts how many verified offline and how many had a confirmed signer. The command exits 1 unless every record both verified and had a confirmed signer.
What it establishes. That the verdict does not depend on RepoOps. The command runs the same shipped verifier the page calls, so the two cannot disagree.
A trust credential from another organization
Situation. A partner sends a signed credential carrying an agent's trust score and the attestation digests it was scored over.
What you do. Get their issuer public key and their log-note public key on their own channel, never out of the credential, and pin both in the Pinned-issuer registry. Paste the credential and their log bundle into Verify a credential you received.
What you see. Three legs reported separately: the signature, whether the referenced attestations are in the witnessed log, and whether the score recomputes from those same records. A pin equal to the key inside the credential is downgraded to unknown with the reason spelled out, never rendered as a pass.
What it establishes. That you know which of the three legs held. An unanswerable inclusion leg reads cannot verify, which is an honest unknown about your own material and not a verdict against the credential.
Before you start
- Supported versions
- RepoOps desktop v0.3.1, the release this guide was read against. The offline check is the repoops command line, which ships with the same package. Node crypto only; this path adds no runtime dependency.
- Where it runs
- Local: the Verify a record tab, under Attribution. Hosted: /team/verify-attestation, which holds no attestation store and checks whatever you paste. The desktop page runs one check the hosted page cannot, the ledger anchor, because the hosted side holds no copy of that ledger.
- Permissions
- Local: no account. The server binds 127.0.0.1 unless REPOOPS_BIND_HOST says otherwise, so the loopback interface is the boundary and the lookup stays on the machine. Hosted: a signed-in RepoOps account. The hosted route resolves no team and touches no team data, which is recorded as its reason for having no tier gate.
- Connections
- None to verify. To emit on merge, the RepoOps GitHub App installed and its webhook receiver armed. To reach witnessed, an external witness service and all three of its settings. To pin a foreign issuer, that issuer's two public keys obtained on a channel they control.
- Plan
- No plan gate. The hosted route is listed in website/lib/team-route-tier-guard.test.ts as a session-gated pure function of caller-supplied input, so it is available at every tier.
Configure it
- Know which way the flag points.
REPOOPS_ATTESTATION has been armed by default since 2026-09-08. isAttestationEnabled reads it as on unless the value trims to the string 0, so emitting and signing run without you setting anything. The tab note, the hosted page intro and the Activation console copy still say the older thing, that emission is dormant until you set it to 1. The code is the authority here and those three sentences are stale.
- Look one record up.
Pick the repository, then type a commit SHA, a digest or a pull request number into the single field and press Verify. When the store has records, a strip of recent ones sits above the form and each badge is clickable. The page also takes a deep link on token, sha or pr.
- Read the banner before the badge.
Verified means all four checks held and the signer is pinned or the digest is in the witnessed log. Verified (signer not pinned) is amber and means the signature was checked against the key the record carries. Verified (anchor superseded) means the chain moved on since the record was signed, which is ordinary. NOT verified names the failing check. No attestation found is an honest miss, not a failure.
- Pin the signer when identity matters.
Pass the expected Ed25519 public key with --pubkey on the command line, or set REPOOPS_VERIFY_PUBKEY so it resolves without the flag. Legacy records that predate the envelope are HMAC-signed and need the install ledger key instead. The command treats an unpinned signature as untrusted and exits 1, unless you pass --allow-unpinned.
- Point the log at an external witness, or leave it honest.
REPOOPS_WITNESS_URL, REPOOPS_WITNESS_ID and REPOOPS_WITNESS_PUBKEY resolve together. Set all three or resolveWitness returns nothing and the inclusion row reads not witnessed, which is truthful. The witness co-signs with a key RepoOps does not hold, which is the whole point.
- Pin an issuer before verifying their credential.
In Pin an issuer, fill Org id, Issuer public key (base64) and Issuer log-note public key (base64), with an optional Label for your own reference, then press Pin issuer. Both keys are required and both must decode to 32 bytes. Type them from the issuer's own channel. Pinning an org that is already pinned replaces its keys.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
REPOOPS_ATTESTATION | the data directory's .env | leave it unset (armed since 2026-09-08); set 0 to opt out | Any value other than 0 is on. With 0 the emit route answers 403 and the recent-record strip says emission is dormant; verification still works, because reading and checking a record is never gated. |
REPOOPS_WITNESS_URL, REPOOPS_WITNESS_ID, REPOOPS_WITNESS_PUBKEY | the data directory's .env | all three, or none | Missing any one means no witness, so the checkpoint reads witnessed false rather than claiming an anchor it does not have. |
REPOOPS_VERIFY_PUBKEY | the environment the command line runs in | the install's Ed25519 public key, base64 | The fallback pin when --pubkey is not passed. Publishing a public key grants no signing power. |
REPOOPS_VERIFY_KEY | the environment the command line runs in | set it only if you hold legacy HMAC records | The per-install ledger key in hex. Without it a legacy record reports no verification key rather than failing wrongly. |
REPOOPS_TRUST_CREDENTIAL | the data directory's .env | leave unset unless you issue credentials | Exactly the string 1 arms issuing. Issue answers 403 otherwise, and the panel says which flag before you press it. Verifying a credential you received is never gated. |
REPOOPS_REKOR, REPOOPS_REKOR_URL, REPOOPS_REKOR_PUBKEY | the data directory's .env | off unless you want the Sigstore mirror | The mirror fires only when REPOOPS_REKOR is set, and submits commitments only: the leaf hash and the tree head, never content. |
REPOOPS_LOOP_GITHUB_APP | the data directory's .env | 1, with its webhook credential set, if you want on-merge emissions | The On-merge emissions panel stays honestly empty until the App is installed, the receiver is armed and attestation is on. Unarmed, the webhook endpoint answers 501 and nothing arrives. |
REPOOPS_BIND_HOST | the data directory's .env | 127.0.0.1 (the default) | The local page has no account of its own, so the listen interface is what keeps a lookup on the machine. |
REPOOPS_TELEMETRY_MAX_FILE_MB | the data directory's .env | 512 (the default) | The byte cap on the streaming read of the attestation store and the transparency log. It bounds the read, it does not prune the file. |
issuer-pins.json | Verify a record, Pinned-issuer registry | one entry per organization you receive credentials from | Plain JSON in the data directory holding two verification keys per org and no secret. An unreadable registry reads as no pins, which fails a foreign verification closed. |
What you should see
A record this install signed, with the signer pinned
Configuration. Attestation armed, the store holding records, the expected public key pinned or the witness configured.
Expect. A green banner reading Verified. Four badges reading shape, digest, signature and ledger anchor. An inclusion row with the leaf index and a witnessed marker.
Verify. The anchor line shows the bound chain head, the truncated digest and the algorithm. Running the same record through the offline command with the same pinned key prints verified and exits 0.
A valid signature from a signer you cannot name
Configuration. Any DSSE record, no key pinned, and the digest not yet proven present in a witnessed log.
Expect. An amber banner reading Verified (signer not pinned), with the sentence that the signature was checked against the public key carried inside the envelope and that anyone can sign a fabricated record with their own key.
Verify. The offline command prints verified but UNTRUSTED and exits 1. Pin the expected key, or confirm inclusion in the witnessed log, and the same record turns green.
An unknown token, or a statement somebody edited
Configuration. Any install. Paste a digest the store does not hold, or a record whose statement was changed after signing.
Expect. For the unknown token, an amber No attestation found naming what you searched by, described on the page as an honest not-found. For the edited statement, a red NOT verified reading digest mismatch (statement altered).
Verify. The reason line comes from the server response, never from the browser. The page runs no cryptography and never decides verified on its own.
Data and cost
- What is captured
- Verifying captures nothing and writes no audit row. The records it reads are written by the emit path as one JSON line each in the repository's .claude/brain/claude-code-usage/attestations.jsonl, with each digest appended as a leaf to transparency-log.jsonl beside it. Issuer pins live in issuer-pins.json in the data directory.
- Who can see it
- Local by default. The local page is served on the loopback interface with no account; the hosted tab needs a signed-in RepoOps account and keeps no attestation store of its own, so it can only check what a person pastes into it. The record itself is portable, and the signature and digest it carries are verification outputs rather than secrets. No route returns the signing keys.
- How long it is kept
- Append-only and unbounded. Nothing prunes the attestation store or the transparency log, and no retention setting shortens them. Reads are byte-capped at REPOOPS_TELEMETRY_MAX_FILE_MB, 512 by default, which bounds the read and not the file. See Delete below.
- What leaves the machine
- Verifying makes no network call. With the three witness settings present, the log posts a checkpoint note holding the log origin, the tree size and the root hash to the witness, and no attestation content. With the Rekor mirror armed, only the leaf hash and the tree head go out. On the hosted page, what you paste is posted to the hosted verifier and nothing is stored.
- What it costs
- No model call on any path described here. The checks are sha256, HMAC and Ed25519 from Node's own crypto module, so verification has no metered cost. The cost facet shown on a record is the window-scoped figure the attestation already carried, and reads not priced per PR rather than a fabricated zero.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| The recent-record strip says attestation emission is dormant on this install. | REPOOPS_ATTESTATION is set to 0, so the list route reported enabled false. | Remove the line from the data directory's .env, or set it to 1, and restart. The default is armed. |
| No attestations stored for this repo yet. | The flag is on but nothing has been emitted for that repository. | Emit for recently merged pull requests with POST /api/attestation/emit, or install the GitHub App so merges emit on their own. |
| Verified (signer not pinned). | The record carries a DSSE envelope and the endpoint pins no key, so the signature was checked against the key the envelope embeds. | Pin the expected public key on the command line, or confirm the digest is in the witnessed log. Treat it as unproven until one of the two holds. |
| Verified (anchor superseded). | The chain head bound at signing time is no longer the live head, which a later append causes. | Read it as verified against history. The digest and signature still held; only the anchor is older than the chain. |
| The inclusion row reads not included, or not witnessed. | The digest is absent from the log, was appended after the signed checkpoint, or no external witness is configured. | Read the reason the row carries. Set all three witness settings if you want an anchor this install does not hold. |
| A credential's inclusion leg reads cannot verify. | No pin on that issuer's log-note key is available, so the checkpoint cannot be authenticated. | Paste their log-note public key on the verification, or add the issuer to the Pinned-issuer registry. This is an unknown about your material, not a verdict against the credential. |
- Disable
- Set REPOOPS_ATTESTATION=0 in the data directory's .env and restart. Emitting and signing stop, the emit route answers 403 naming the flag, and the advisory verdicts elsewhere go back to reading signed false. Verification of existing records is unaffected, because no route gates it.
- Roll back
- Not provided. An emitted attestation cannot be retracted or rewritten: the store appends and never edits in place, and a transparency-log leaf is append-only by construction. A record that should not stand is answered with a later record, not by removing the first.
- Revoke access
- Issuer pins: press Remove pin twice on the row, or call DELETE /api/attestation/issuer-pins with the org id; a removal when there was no pin is reported, not treated as an error. Not provided for this install's own keys: there is no rotation route for the three entries lib/attestation/signer.mjs creates once per install in the key-value store, the HMAC ledger key, the Ed25519 attestation key and the log-note key.
- Delete
- Not provided. No route deletes an attestation or a log leaf. The files are the repository's .claude/brain/claude-code-usage/attestations.jsonl and transparency-log.jsonl, and removing a leaf would break every inclusion proof taken over the checkpoint that covered it. The hosted side stores nothing, so there is nothing there to delete.
Related tasks
Maintenance evidence
- Feature id
verify-attestation(spine leafverify-attestation)- Owner
- Cloud-Native Accountability program (CNA.8 the attestation, CNA.9 this page, CNA.22 normalized authorship, CNA.26 the offline verifier, CNA.29 and CNA.29c the transparency log and the pinned-issuer registry, CNA.34 the portable credential, E1 on-merge emission). Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (public/attestation-verify.html) and the hosted form (website/app/team/(home)/verify-attestation/verify-form.tsx). The REPOOPS_ATTESTATION default was re-read in lib/attestation/attestation.mjs and .env.example: armed unless the value is 0, moved 2026-09-08. Every related link was checked against a page.tsx under website/app/(marketing)/.
- Example fixtures
- lib/attestation/conformance-fixtures/ holds conforming.json (a real record from the shipped emitter), conforming-superset.json and nonconformant-boolean-origin.json. The behaviour is exercised by lib/attestation/attestation.test.mjs (the flag default, the four checks, the trust values), verifier-sdk.test.mjs (the offline verdict matches the page), transparency-log.test.mjs and witness-client.test.mjs (inclusion and the witness co-signature), issuer-pins.test.mjs with test/issuer-pins-panel.test.mjs (the routes and the rendered panel), trust-credential.test.mjs, emit-on-merge.test.mjs (the 2026-09-08 default, both directions) and test/attestation-credential-and-models.test.mjs.
- Source references
lib/attestation/attestation.mjs,lib/attestation/store.mjs,lib/attestation/signer.mjs,lib/attestation/transparency-log.mjs,lib/attestation/witness-client.mjs,lib/attestation/issuer-pins.mjs,lib/attestation/trust-credential.mjs,lib/attestation/emit-on-merge.mjs,lib/attestation/verifier-sdk.mjs,lib/routes/attestation.mjs,bin/commands/verify.mjs,public/attestation-verify.html,website/lib/attestation-verify.ts- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Story script written 2026-09-15; render and review pending in the same slice.
Last updated