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

30.1 s, captions on. Narration: Microsoft Zira Desktop (provisional voice; an approved narration source is pending).Transcript
Read the narration
  1. 0:00 A merged change claims an author.
  2. 0:02 Nobody has checked the claim.
  3. 0:06 RepoOps recomputes the digest and checks the signature.
  4. 0:09 Tampering fails honestly.
  5. 0:13 A valid signature is not a signer you know.
  6. 0:16 Pin the key, or find the digest in the witnessed log.
  7. 0:23 Hand over the record.
  8. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

SettingWhereA sensible choiceWhy it matters
REPOOPS_ATTESTATIONthe data directory's .envleave it unset (armed since 2026-09-08); set 0 to opt outAny 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_PUBKEYthe data directory's .envall three, or noneMissing any one means no witness, so the checkpoint reads witnessed false rather than claiming an anchor it does not have.
REPOOPS_VERIFY_PUBKEYthe environment the command line runs inthe install's Ed25519 public key, base64The fallback pin when --pubkey is not passed. Publishing a public key grants no signing power.
REPOOPS_VERIFY_KEYthe environment the command line runs inset it only if you hold legacy HMAC recordsThe per-install ledger key in hex. Without it a legacy record reports no verification key rather than failing wrongly.
REPOOPS_TRUST_CREDENTIALthe data directory's .envleave unset unless you issue credentialsExactly 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_PUBKEYthe data directory's .envoff unless you want the Sigstore mirrorThe mirror fires only when REPOOPS_REKOR is set, and submits commitments only: the leaf hash and the tree head, never content.
REPOOPS_LOOP_GITHUB_APPthe data directory's .env1, with its webhook credential set, if you want on-merge emissionsThe 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_HOSTthe data directory's .env127.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_MBthe data directory's .env512 (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.jsonVerify a record, Pinned-issuer registryone entry per organization you receive credentials fromPlain 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.
ⓘ
To stop or undo
Set REPOOPS_ATTESTATION=0 and restart to stop emitting and signing; nothing already written is removed, and verification keeps working. Remove a pin with Remove pin, which arms on the first click and removes on the second, after telling you that removal restores cannot verify rather than marking the issuer untrusted.

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

SymptomLikely causeNext 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.

Maintenance evidence

Feature id
verify-attestation (spine leaf verify-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