Tamper-Evident AI Agent Logs: Integrity, Gaps, and Limits
A runnable local log-chain experiment covering edits, deletion, replay, truncation, drops, restarts, and signer compromise.
By Kevin O'Connor
A hash-chained agent log can detect an edited record and still accept an incomplete history. Delete the last few records and the remaining prefix may be perfectly valid. If the verifier has no independent knowledge of how far the log progressed, it cannot distinguish that prefix from a run that ended earlier.
That is why I prefer a specific claim: these controls make certain changes detectable under a stated trust model. Calling the result tamper-proof hides the assumptions that matter most when the log is needed.
The local experiment below demonstrates those assumptions. It includes an external-checkpoint model, but uses separate directories on one machine to represent the writer and witness. That is a teaching arrangement, not independent custody or protected production retention.
Three questions that need separate answers
Integrity: Have the received records changed relative to an authenticated sequence or an independently retained checkpoint?
Completeness: Did the logger receive every expected record, and do we have independent information about the expected beginning and end of the stream?
Execution: Did the downstream system actually perform the action the agent described?
A valid record saying refund_completed establishes none of the third answer by itself. You need a receipt or resulting state from the payment service, linked to the action identifier. Even then, inspect which principal could create that receipt and whether duplicate delivery is possible.
RFC 5848, Signed Syslog Messages is useful design context for message authentication, ordering, replay resistance, and missing-message detection. It was published in 2010 as a Standards Track RFC. The following JSON/HMAC format is a small reference design, not an RFC 5848 implementation or compliance claim.
Run the chain and witness example
Download tamper_log.py and run:
python3 tamper_log.py
The script uses standard-library JSON, SHA-256, and HMAC. Each record includes a sequence number, boot epoch, event ID, action label, producer sequence, previous authentication value, and its own HMAC. Canonical serialization fixes JSON key order and separators before calculating the MAC.
The key is a public fixed demonstration string. HMAC is symmetric: anyone who can verify using that key can also forge records. There is no asymmetric signature, identity enrollment, protected key storage, or nonrepudiation in this example. Its value is making the failure cases observable without a dependency or external service.
After three records, the script saves a checkpoint containing the record count and chain head in a separate temporary witness directory. The verifier checks that the current log contains the checkpointed prefix and that its head agrees. New records may follow that prefix.
What the test actually found
The September 9, 2026 run on Python 3.13.12 completed all 14 assertions. Saved results contain each expected and observed acceptance decision.
| Case | Verifier result | Meaning |
|---|---|---|
| Original log and checkpoint | Accepted | Received sequence matches the retained reference |
| Edit a middle action | Rejected | MAC no longer matches |
| Delete a middle record | Rejected | Sequence and chain continuity fail |
| Append an old record again | Rejected | Duplicate/order checks fail |
| Remove the tail, no checkpoint supplied | Accepted | A valid prefix has no known missing endpoint |
| Remove the checkpointed tail | Rejected | The independently expected prefix is absent |
| Reload verified state, append in a new boot epoch | Accepted | Sequence and linkage continue across state reconstruction |
| Restart with empty state | Rejected against old checkpoint | New history cannot replace the known old prefix |
| Drop an allocated producer event | Chain accepted; producer sequence check rejected | Collector integrity and producer completeness differ |
| Never emit or allocate the omitted event | Accepted | The verifier has no information about the missing action |
| Rewrite history with the key | Accepted without old checkpoint; rejected with it | A retained prior head limits retroactive replacement |
| Forge a new receipt after the checkpoint with the key | Accepted | A checkpoint cannot make a compromised signer truthful |
The restart case reloads JSON into fresh in-memory state and verifies it before appending. It does not kill a process, simulate power loss, or test atomic persistence. Those require a storage design with crash consistency, durable acknowledgements, and a recovery protocol.
The actions are inert labels. No AI agent, refund service, or external tool ran during this experiment. The false-receipt case specifically shows how an authenticated record can make an untrue execution claim.
Put the checkpoint outside the writer's authority
In production, retaining the checkpoint beside the log under the same administrator defeats its purpose against that administrator. A separate collector or witness needs its own access controls, retention policy, and observable acknowledgement. Preserve the stream identity as well as the count and head so one stream cannot be substituted for another.
The interval between checkpoints is a window of uncertainty. A writer that loses an uncheckpointed tail may present an earlier valid prefix. An external heartbeat or expected-close receipt can help establish whether more records should exist, but only if its own delivery and custody are reliable.
Protected retention addresses another part of the problem. For example, Amazon S3 Object Lock supports retention controls on object versions. Governance and compliance modes have different bypass properties; the selected mode, permissions, retention period, and legal holds matter. The demo neither configures nor tests S3. Write-once storage also cannot preserve an event the collector never received.
Instrument loss before signing
Assign producer event IDs before queueing, record acknowledged ranges, and expose dropped-event counters through a separately monitored path. Decide what happens when buffers fill: block consequential work, apply backpressure, or continue with an explicit degraded-logging state. A silent drop is an operational fact, regardless of how carefully surviving records are signed.
Multiple workers need explicit stream identities or a trusted sequencing service. Don't pretend a wall-clock timestamp supplies a total order. Clock correction, concurrent actions, and retries all complicate that story. Preserve action IDs that link proposal, authorization, execution request, downstream receipt, and final state without assuming they happen in one process.
For a real deployment, my next test would be a collector outage followed by restart and reconciliation against downstream receipts. The chain, retained checkpoints, acknowledged ranges, and observed action count should explain what survived and where confidence ends. Both temporary directories in this local example are removed automatically; only the downloaded script and any saved JSON remain.
Email updates
Get new research by email
In-depth notes on AI security, threat research, and practical defensive work.