Building an Audit Log Into Your Harness
Can you reconstruct exactly what your agent did, tamper-proof? An agent harness audit log turns 'we think it did X' into an unforgeable, queryable record.
import json, time, uuid
class AuditLog:
def __init__(self, sink):
self.sink = sink # append-only store (file, DB, S3)
def record(self, run_id, event_type, **fields):
entry = {
"id": str(uuid.uuid4()),
"run_id": run_id,
"ts": time.time(),
"event": event_type,
**fields,
}
self.sink.append(json.dumps(entry, sort_keys=True))
return entryWhen something goes wrong with your agent — a bad action, a leaked record, an angry customer — can you reconstruct exactly what happened, step by step, in a way you'd trust in front of a regulator or a court? For most teams the honest answer is no, and they find that out at the worst possible moment. An agent harness audit log is what turns "we think the agent did X" into "here is the exact, tamper-evident record of every decision it made and why." This guide gives you a working one you can drop in today, then explains how to make it something you can actually rely on.
The question at the top isn't rhetorical. Autonomous agents take actions with real consequences, and "the model decided to" is not an acceptable answer when those consequences land on a person. An audit log is the difference between accountability and hand-waving.
Quick-Start (Copy This Right Now)
Here's a structured, append-only audit logger. Every meaningful event — model call, tool call, permission decision, approval — becomes one immutable record.
[object Object], json, time, uuid
,[object Object], ,[object Object],:
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.sink = sink ,[object Object],
,[object Object], ,[object Object],(,[object Object],):
entry = {
,[object Object],: ,[object Object],(uuid.uuid4()),
,[object Object],: run_id,
,[object Object],: time.time(),
,[object Object],: event_type,
**fields,
}
,[object Object],.sink.append(json.dumps(entry, sort_keys=,[object Object],))
,[object Object], entryWhat this does: Writes one structured JSON record per event, stamped with a unique ID, the run it belongs to, and a timestamp, to an append-only sink. Structured means you can query it later ("show every
tool_calldelete⚡ Pro tip: Log the inputs and the decision, never a summary written after the fact. "Agent deleted 3 records" is a story; "tool_call
delete_recordsunder_tenWhat Belongs in an Agent Harness Audit Log
Three choices shape how useful your audit log ends up being, and they all come down to deciding what an agent harness audit log is actually for: reconstructing intent and action after the fact, not just recording that something occurred.
What you log. At minimum: every model request and response (or a hash of them if they're large), every tool call with its arguments and result, every permission and approval decision with the rule and human involved, and every run's start and end. If an event changed what the agent did, it belongs in the log. When in doubt, log it — storage is cheap and missing evidence is expensive.
What you never log. Secrets, raw credentials, and full personal data. This is where the audit log and secrets management intersect: the log is one of the most-read stores in your system, so a credential in it is a credential leaked. Run the same scrubber you use for the model context over audit entries too.
Where it lives. The sink must be append-only and separately permissioned from the agent itself. If the agent's own credentials can rewrite the audit log, the log proves nothing — a compromised agent would simply erase its tracks. Write access to history is the one thing the agent must never have.
Step-by-Step: Making the Log Tamper-Evident
A log that can be silently edited isn't an audit log; it's a suggestion. The fix is a hash chain — each entry includes the hash of the previous one, so any alteration anywhere breaks every hash after it.
Step one: Have each entry carry the previous entry's hash.
[object Object], hashlib
,[object Object], ,[object Object],(,[object Object],):
entry = {,[object Object],: run_id, ,[object Object],: time.time(),
,[object Object],: event_type, ,[object Object],: prev_hash, **fields}
body = json.dumps(entry, sort_keys=,[object Object],)
entry[,[object Object],] = hashlib.sha256((prev_hash + body).encode()).hexdigest()
,[object Object],.sink.append(json.dumps(entry, sort_keys=,[object Object],))
,[object Object], entry[,[object Object],]What this does: Computes each entry's hash from the previous hash plus its own contents, forming a chain. Change any past entry and its hash changes, which breaks the
prevStep two: Verify the chain on read.
[object Object], ,[object Object],(,[object Object],):
prev = ,[object Object],
,[object Object], e ,[object Object], entries:
body = {k: v ,[object Object], k, v ,[object Object], e.items() ,[object Object], k != ,[object Object],}
expect = hashlib.sha256((prev + json.dumps(body, sort_keys=,[object Object],)).encode()).hexdigest()
,[object Object], e[,[object Object],] != expect:
,[object Object], TamperError(,[object Object],)
prev = e[,[object Object],]What this does: Walks the chain recomputing each hash and raising the moment one doesn't match. Running this before you trust the log — during an investigation, or on a schedule — tells you whether the record is intact. A clean verification is what lets you stand behind the log's contents.
⚡ Pro tip: Periodically write the latest chain hash somewhere you don't control — a managed timestamping service, a separate append-only store, even a git commit. That external anchor means even someone who could rewrite your entire log can't match the hash you published elsewhere, which closes the one gap a self-contained chain leaves open.
Pro-Level Variations
For high-volume agents, logging every full model request is expensive. Log a content hash of the request and response plus the metadata, and store the full bodies separately with a lifecycle policy. You keep the ability to prove what was sent without paying to keep every token forever.
For multi-service systems, thread a single correlation ID through every service an agent run touches, and stamp it on every audit entry. When a run spans your harness, a tool service, and a downstream API, that ID is what stitches the fragments into one coherent timeline.
For compliance-heavy domains, the audit log doubles as your data-processing record. Structure entries so you can answer "every action taken on subject X" with a query, because eventually someone will ask exactly that, and grepping unstructured logs at 2 a.m. is not where you want to be.
Troubleshooting Common Issues
The log slows down every run. Synchronous writes to a remote sink add latency to the critical path. Buffer entries and flush asynchronously — but flush before the action they describe completes for anything irreversible, so you never take an action you failed to record.
Entries arrive out of order. With async writes and concurrent runs, timestamps alone won't order events within a run. Add a per-run sequence number so you can reconstruct the exact order regardless of when each write landed.
The chain breaks and you don't know why. Usually it's non-deterministic serialization — a dict that serializes in different key orders. Always serialize with
sort_keys=True⚠️ Common mistake: Building the audit log as an afterthought that the agent writes to with its own permissions. If the agent can write the log, a compromised or buggy agent can also corrupt it, and a corruptible audit log gives you false confidence — arguably worse than none, because you'll trust it when you shouldn't. The log must be write-once from the agent's side and editable by no one.
Making the Log Answer Real Questions
A log you can't query is just a very long file. The point of structuring every entry is that months later you can ask precise questions and get precise answers — and the questions that come up are surprisingly consistent across teams.
"What did run
abc-123run_idRetention is the other half. Full model transcripts are large and sometimes sensitive, so keep the metadata and hashes for as long as your compliance rules require — often years — while aging out the heavy bodies on a shorter clock. The hash chain still proves the record is intact even after the bulky content is gone, because the hash was computed over content you can re-supply or attest to separately. You keep provability cheaply and pay for full fidelity only as long as you actually need it.
⚡ Pro tip: Decide your retention policy before you start logging, not after your storage bill spikes. Retrofitting a policy onto a log that already mixes years of full transcripts with the metadata you actually needed is painful and risky. Tag each entry with a retention class at write time and let a lifecycle job enforce it, so the log prunes itself correctly without anyone deciding, under pressure, what's safe to delete.
Your Turn
Add the append-only logger to your harness today and start recording tool calls and decisions — even without the hash chain, structured records put you far ahead. Then layer in chaining when you need tamper-evidence, and an external anchor when the stakes justify it. Build it early: an audit log you add after an incident can't tell you what happened during the incident. That single constraint — that the log has to exist before you need it — is why it's the piece teams most regret deferring.
Keep your audit schema — which events, which fields, what's scrubbed — versioned alongside your prompts and tool definitions in a library like PromptABCD, so every agent you ship logs the same way and your incident response never depends on remembering which service happened to log which field. Consistency across agents is what lets one investigation query span your whole fleet instead of stopping at each service's idiosyncratic format.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
