Audit log · Proof

How do I know my audit log wasn't changed?

It's the first question an auditor, a client or your own gut will ask. Someone could break into the database. An admin could quietly edit a record. A row could go missing.

You shouldn't have to take our word that none of that happened, so you don't have to: check the whole log yourself. It works on every plan and covers your full history.

How it works, in plain terms

Every record in your audit log gets a digital fingerprint (a SHA-256 hash) that includes the fingerprint of the record before it — like a chain where each link is welded to the last. Edit one record, delete one, or slip a fake one in between, and the links after it no longer match. The check redoes every fingerprint from scratch and tells you whether the chain is whole.

Check it now

Log in, then come back here. Your log is downloaded and every record is re-checked in your browser — not on our server, which would just be us vouching for ourselves. You can also check a log file you saved earlier without logging in.

Verify my audit log

Downloads your log and re-checks every record here, in your browser.

No checkpoint in this browser yet — the first clean check saves one.

Two honest limits

  • Keep a checkpoint outside AgentGovern.The check proves your log is consistent. Without a checkpoint you kept yourself, it can't catch the whole log being rewritten and re-fingerprinted, or the newest records being removed. After each clean check this page saves a checkpoint in your browser and offers it as a download — keep the download somewhere we can't reach, because browser storage can be cleared, and our page can read it. More on checkpoints.
  • This check runs on code we serve.It runs in your browser, but the page comes from us. For a check that doesn't depend on our code at all, use the command-line checker — one short file you can read line by line. Both give the same answer, byte for byte.

Check it without our code: the command line

The fully independent option. You need a computer with Node.js 18 or newer (free from nodejs.org) and a terminal (Terminal on a Mac, PowerShellon Windows). It's three copy-and-paste commands; if you've never opened a terminal, send this section to whoever looks after your tech.

01

Download your log

While logged in, click below (also in Audit log → Export → Verifiable chain). You'll get a file named like agentgovern-audit-chain-2026-10-08.ndjson with every record, in order.

Download my audit log
02

Download the checker

In your terminal, in the same folder as the file:

curl -O https://agentgovern.io/audit/verify-audit-chain.mjs

One short file (about 200 lines), nothing else to install. You — or anyone you trust — can read every line of it before running it.

03

Run the check

node verify-audit-chain.mjs agentgovern-audit-chain-2026-10-08.ndjson --save-checkpoint checkpoint.json

Swap in your file's actual name. You'll see one of two results:

Your log is intact
✓ audit chain intact: 812 rows, head seq 812 fe11162e41a4…7582f
  794 rows predate the chain (backfilled when it was switched on): proven unchanged since then.
  checkpoint saved to checkpoint.json — keep it somewhere AgentGovern can't reach.

Look for ✓ audit chain intact: every record checks out and none are missing. "Predate the chain" means records written before this protection was switched on (October 2026): they were sealed that day, so they're proven unchanged since then — not before.

Something was changed
✗ AUDIT CHAIN VERIFICATION FAILED — 3 problem(s):
  - seq 412 (id 512): row_hash doesn't match its contents (row was altered)
  - seq 601: row missing (deleted?)
  - seq 602: prev_hash doesn't match seq 600's row_hash (rows changed, removed or re-ordered before it)

It starts with ✗ AUDIT CHAIN VERIFICATION FAILEDand lists every problem by record number ("seq"). Keep the file, and email hello@agentgovern.io.

Keep your checkpoint — here's why it matters

The check proves your log is consistent. On its own, it can't prove it's the same log you saw last month. Someone with full control of the database — and that includes us — could rewrite the entire log and redo every fingerprint, or remove the newest records, and the check would still pass.

That's what the checkpoint is for: a tiny file (checkpoint.json) holding the record number and fingerprint of your latest record. Keep it somewhere we can't reach — your own computer, a shared drive, an email to yourself. Next time, the button uses the one saved in your browser (or pick your file with “Use a checkpoint file…”); on the command line, include it:

node verify-audit-chain.mjs agentgovern-audit-chain-2026-11-08.ndjson \
  --checkpoint checkpoint.json --save-checkpoint checkpoint.json

If anything up to your checkpoint was changed, removed or rewritten, the check fails. Records added since your last checkpoint are protected from the next one on, so make it a habit — once a month, and before you hand the log to an auditor or client.

What the check catches

If this happened to your log…Caught without a checkpointCaught with your checkpoint
A record was editedYesYes
A record was edited and its fingerprint redoneYesYes
A record was deleted, or a fake one slipped in betweenYesYes
Records were shuffled out of orderYesYes
The newest records were deletedNoYes
The entire log was rewritten from scratchNoYes
For developers: the exact formula

Each account has its own chain, numbered 1, 2, 3… with no gaps. Each record's prev_hashis the previous record's row_hash. The database computes the hash as each record is written; values sent by an app are ignored. You can re-implement the check from this:

row_hash  = lower-hex( sha256( utf8( (prev_hash ?? "") + "\n" + canonical ) ) )
canonical = "agentgovern-audit-v1"
            + for each field, in order: "|" + (null ? "~" : byteLength(value) + ":" + value)
fields    = chain_seq, id, user_id, agent_id, policy_id, action_type, resource_type,
            resource_identifier, decision, cost_cents, request_payload, response_summary,
            ip_address, resolves_log_id, created_at

Values are exactly the strings in the export. Times are UTC, written as YYYY-MM-DDTHH:MM:SS.ffffffZ. Lengths are counted in UTF-8 bytes, so a |inside a value can't pass for a separator.