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

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

Related field notes


Try it: free for 3 agents · Docs: Claude Code · cron