Field notes · 2026-08-25 · RunVouch
Prove what your AI agent did: an audit trail for unattended agents
A real, tamper-evident proof of one agent run: hashed record, public daily Merkle chain, Bitcoin anchor. How to verify it with a stdlib script, and what it cannot prove.
Last night an agent of mine ran at 02:00, wrote a file, and exited 0. I know this because RunVouch told me. But "RunVouch told me" is not something I can hand to an auditor. This post is about the part that shipped this week: a record per run that nobody, including me, can edit afterwards without it showing, and a script that checks it without calling our API.
Why a log is not enough
A log file proves that someone wrote a log file. If your agent runs unattended and a customer, a compliance officer or a regulator later asks "show me what it did on the 25th", a text file on the same box that runs the agent answers the question with "trust me". For most teams that was fine until this year. It is getting less fine for three reasons.
First, regulation. The EU AI Act introduces logging and record-keeping duties for high-risk AI systems: the system must be able to record events automatically over its lifetime, and deployers must keep those logs for at least six months (Article 12 and Article 26; most of these obligations apply from 2 August 2026). Whether your agent is high-risk is a question for your counsel, not for me. If it is, an editable log is a weak log. And to be clear: RunVouch does not make anyone compliant. It gives you a record an auditor can verify themselves.
Second, customers. When the output of an agent is the product (a nightly digest, an enriched list, a report), the buyer increasingly wants to know it ran on time, produced the thing and cost what you said.
Third, incidents. After a bad night the first question is "what happened", the second is "is this record the original". The second one is the hard one.
What a proof looks like
This is a real proof, fetched with rv proof 1a19989090b6f268 for a run of 25 August. It contains hashes and numbers, nothing else, so there is nothing to redact:
{
"run_id": "1a19989090b6f268",
"record": {
"run_id": "1a19989090b6f268",
"agent": "health-smart-money-13f",
"account_id": 1,
"started": 1787676816.2562478,
"ended": 1787676821.2587738,
"status": "ok",
"cost": 0.0, "tokens": 0, "tool_calls": 0, "output_bytes": 14,
"evidence": {}, "evidence_ok": 1,
"source": "cron", "exit": 0,
"tool_events_hash": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945"
},
"leaf_hash": "5ffce31f6834c91f56d0de332fd42f8f9dffe942a9bcdefdeb3a2d9f08b86a3d",
"stored_leaf_hash": "5ffce31f6834c91f56d0de332fd42f8f9dffe942a9bcdefdeb3a2d9f08b86a3d",
"date": "2026-08-25",
"merkle_path": [
["51b100b29618cb10244793b08ecd89a7eac14a60dd5ac0ee529b6196c151687a", "right"],
["cc81c8fe2e56979ddd25091b52273eb304975b4a0f40410de49d3e28d13dbcf0", "right"],
["5494215feb161f53f94e956f25c88c3e07c2fb1b5c9af20f4d5dc2b864a5da4c", "right"],
["600c3f2670d839a1250e210508152454f7d4cbd55d0ef09ac77415a4128ac184", "right"]
],
"root": "f49af4df6074ec71de34400d835b725439aa87a4b9d1ac1e05ed26b6d4e015a6",
"sealed": true,
"chain_hash": "4cdcf3566b0b3e6eb68ed2f7df6a39755dd64da6c66e334eac7df6422dea6370",
"prev": "0000000000000000000000000000000000000000000000000000000000000000",
"ots_status": "pending",
"verify_url": "https://api.runvouch.com/proof/days/2026-08-25.json",
"ots_url": "https://api.runvouch.com/proof/days/2026-08-25.ots"
}
Reading it top to bottom: record is the run as one JSON object with fixed keys. leaf_hash is the sha256 of that object in canonical form (sorted keys, no spaces). stored_leaf_hash is the hash written in the same database transaction that ended the run; if the row changed later, these two would differ. merkle_path is the list of sibling hashes that leads from this leaf to the root of the day. chain_hash ties the day to the previous one, and prev is 64 zeros here because 25 August is the first sealed day. ots_status: pending means the day file was stamped with OpenTimestamps and the Bitcoin block was not in yet when I fetched this.
The day file itself is public, no key needed: api.runvouch.com/proof/days/2026-08-25.json, thirteen runs from every account that day, listed as random run ids and leaves. The index of all sealed days is at api.runvouch.com/proof/.
Verifying it without us
verify_proof.py is a Python 3 script, standard library only, short enough to read before you run it. Give it the proof and the day file:
python3 verify_proof.py proof.json 2026-08-25.json PASS leaf hash matches the record PASS merkle path leads to the root PASS day file lists this run with this leaf PASS day file root recomputed from its leaves PASS chain hash of the day date 2026-08-25 runs 13 chain 4cdcf3566b0b3e6e... ots pending VERIFIED
Then I changed cost in the record from 0.0 to 0.01 and ran it again:
FAIL leaf hash matches the record computed 4178697a28c799a9ee597f81e5ab402d7fc26602072d15ffca12b6dcae2b5733 FAIL merkle path leads to the root computed 0848cd92f876625b6d8b5ae7fa22ad0c0111e1ee39d4d056c8df2b236584d928 FAIL day file lists this run with this leaf PASS day file root recomputed from its leaves PASS chain hash of the day NOT VERIFIED
Exit code 1. The day file is still internally consistent (the last two lines pass) because I did not touch it; the edited record simply is not in it. That is the whole point: to make an edit stick, you would have to change the public day file that every other customer can also download, and then the Bitcoin attestation would no longer match.
The Bitcoin part uses the open-source OpenTimestamps client, not our code: pip install opentimestamps-client, download the .ots file next to the day file, ots verify 2026-08-25.ots -f 2026-08-25.json. With a local Bitcoin node it checks the block header; without one it tells you which block to look up.
The limits, plainly
- The record has no content. It proves that a run with these numbers and these evidence verdicts ended at that time. It does not prove what the agent wrote. If you need the output under seal, hash the file yourself and pass the hash as an evidence check; then it is in the record.
- The record is what your client reported. A client that lies about cost gets its lie preserved faithfully. The chain rules out editing afterwards, not lying at the time.
- A run that never called run end (killed, stalled) has no leaf.
- Until the UTC day is sealed, the proof endpoint returns
sealed: false. That is our word, not a proof, for up to 24 hours. - OpenTimestamps proves the day file existed before a block, not that it did not exist earlier. Ordering between days comes from the chain hash.
- If the ots client or a calendar server is down, the day is sealed without an anchor and the index says so. We do not backfill anchors quietly.
What to do with it
Nothing, most days. The proof is written whether you look or not, on every plan including Free. When someone asks, rv proof RUN_ID --verify gives them the JSON and a zero exit code, and the script and the public day file let them check it on their own machine. The mechanism, key by key, is on docs/proof; who needs it and the questions an auditor will ask are on verifiable agent runs.
Sources
- RunVouch docs — verifiable runs (what is hashed, limits)
- Public proof index, api.runvouch.com/proof/
- Day file 2026-08-25
- verify_proof.py, standalone verifier
- OpenTimestamps
- OpenTimestamps client
- EU AI Act, Article 12 (record-keeping)
- EU AI Act, Article 26 (obligations of deployers)
Related field notes
- My Claude Code cron ran up $1,800 in two nights — the watchdog that stops it at $2
- Claude Code Routine failed silently? How to know your scheduled agent actually ran
- The dead man's switch for AI agents (and why a ping isn't enough)
Try it: free for 3 agents · Docs: Claude Code · cron