Your agent's run log can catch tampering without holding a word of what the agent saw
An AI agent's run log can catch someone editing it without containing a single word of what the agent saw. Sounds wrong, since you'd think checking a record means having the record. But it comes down to a few small idea
An AI agent's run log can catch someone editing it without containing a single word of what the agent saw.
Sounds wrong, since you'd think checking a record means having the record. But it comes down to a few small ideas, and you can run most of them yourself.
Let's use one example the whole way through. An agent picks up a support ticket, reads the customer's message, calls a refund tool, gets a result back, and writes a reply.
Later someone asks what happened on that run. You want to be able to show the sequence, and you don't want a file full of a customer's message sitting around, or getting passed to whoever's asking.
Step one: keep a fingerprint instead of the content
Run each step through SHA-256 and store the hash. The hash changes if the content changes, and you can't read the content back out of it.
Except there's a hole. You can't read it back, but you can guess:
import hashlib
def sha(text):
return hashlib.sha256(text.encode()).hexdigest()
plain = sha("issue_refund(approved=True)")
for guess in ["issue_refund(approved=False)", "issue_refund(approved=True)"]:
print(guess, sha(guess) == plain)
The first guess prints False, the second prints True. So anyone holding that log just learned the refund went through, without ever seeing the step.
A hash only hides things that are hard to guess, and a tool call often isn't. Hardly any options, a predictable shape, and you can just try them.
Step two: salt it with a secret for that run
Generate a random secret when the run starts and mix it into every fingerprint. Now a guess is useless unless you've also got the secret.
import secrets
secret = secrets.token_hex()
def fingerprint(content):
return sha(secret + content)
The same refund call on a different run gets a different fingerprint too, so you can't line runs up and spot repeats.
Step three: chain the steps
Separate fingerprints have a problem of their own. Someone can delete the awkward one, or swap two of them, and every fingerprint left still looks fine.
So each link folds in the one before it:
steps = [
"prompt: the customer's ticket",
"tool_call: issue_refund(approved=True)",
"tool_result: refund accepted",
"output: the reply to the customer",
]
head = ""
for content in steps:
head = sha(head + fingerprint(content))
print(head)
Change a step, drop one, or reorder two, and head comes out different. One hash now stands in for the whole run, order included.
That code's a toy, by the way. It's there to show the idea, and it isn't the actual receipt format.
Step four: get that one hash out of your hands
A chain on its own doesn't stop much, because whoever holds the log can edit a step and just recompute every link after it. You get a new head, perfectly consistent, and nothing looks wrong.
The fix is to commit to the head somewhere you can't quietly go back and change, at a known time. That's what anchoring a hash on a public chain gets you. Once the head's anchored, a rewritten log gives you a different head, and that head doesn't match the proof.
And the only thing that had to leave your machine is one hash. No ticket, no reply, no tool call.
Why you can throw the secret away
This is the part that makes the opening line true.
The chain is built from fingerprints, not from content. So to check the chain you only need the fingerprints and the proof. You recompute the links, compare the head, done. The secret doesn't come into it.
The secret's only needed to connect a fingerprint back to real content. That gives you two separate checks:
- Is the sequence intact? Anyone can check this from the log alone, with no content and no secret.
- Was this the content of step two? Checking this takes the content and the secret.
So you can strip the secret out and hand the log to someone. They can confirm the sequence hasn't changed since the anchor time, and they can't guess their way to the refund result.
If the refund gets disputed later, you produce that one tool call and the secret, and they can check it against the step.
What this costs
I built this into verify-proof as agent run receipts, so these are limits of my own design.
The secret's per run, not per step. Hand it over to prove one step, and that person can now run the guessing attack from step one on every other step in the run. If you need to reveal a step to someone you don't trust with the rest, that's a real cost.
Not everything is hidden. A receipt keeps the fingerprint, plus the step's type, its time and an optional label of up to 120 characters. Those are readable. So a stripped receipt still shows the shape of the run, and if you put the customer's name in a label, you've published it.
Lose your copy of the secret and you lose the per-step check. The chain still verifies, but you can't tie a step back to its content anymore. Keep the secret somewhere before you strip it from the copy you share.
It says nothing about whether the agent was right. The refund could've been a terrible call and the receipt would verify fine. It also doesn't show who ran the agent. What it shows is that this sequence of steps existed by the anchor time and hasn't changed since.
It covers what was recorded, from when it was anchored. A step nobody added isn't in the chain. And anything edited before the anchor just looks like the original, because the anchor's the first moment there's anything to contradict it. So anchor when the run ends, not whenever you get around to it.
Trying it
The check itself is offline and doesn't need a key or an account. From a clone of the repo, there's a fixture you can verify:
pip install verify-proof
verify-proof receipt verify examples/receipt.json
In Python, agent_receipt.RunReceipt records the steps as prompt, tool_call, tool_result, output or note, and anchor() sends only the head to ProofLedger as one ordinary proof, using the run id as its external_id. The proof bundle gets saved into the receipt. That anchoring call does need a free ProofLedger API key.
verify_receipt recomputes the chain and checks the head against the bundle. verify_step takes one step's content and the secret. There's also a Node port that follows the same rule, and that fixture verifies in both.
If you only take one thing from this, take the split between the two questions, because it works anywhere you've got a sensitive log. Checking that a record is intact and reading the record don't have to need the same access.
verify-proof is free and MIT licensed, I built and maintain it, and the code's at https://github.com/Fulcrum-Enterprises/verify-proof
Disclosure: this article was drafted by an AI agent I built and run, from facts I supplied about my own project.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.