Skip to content
All posts
8 min readBrian McManus

What an audit trail for AI coding agents has to prove

Four questions every agent audit record must answer, what each coding agent logs natively, and what a hash chain does and does not establish.

auditcomplianceagent-securityobservability

An audit trail for a coding agent has to prove four things about every action: which agent did it and under whose identity, what it touched, which policy or approval allowed it, and that the record has not changed since it was written. A log that answers only the second question is a debugging aid.

The four questions

QuestionFields you needWhy the usual log fails it
Who actedHuman identity, agent name and version, session idClient transcripts sit on the developer's disk and can be edited or deleted
What happenedTool name, MCP server, arguments or a hash plus a redacted form, resultVendor telemetry is often off by default and redacts arguments
Who allowed itPolicy id, policy version hash, decision, approverThe decision happens in a different product from the action
Is the record intactPrevious-record hash, record hash, an anchor outside the endpointA file the user can rewrite proves nothing

There is a fifth requirement and it is the hard one: coverage. An action that was never recorded leaves no trace, and no amount of hashing fixes a gap. A perfect chain over half the tool calls is a weaker control than an imperfect chain over all of them.

What each coding agent logs today

Claude Code can emit a record for every tool call through PreToolUse and PostToolUse hooks, and they can post to an HTTP endpoint as well as run a local script. Session transcripts live on the local machine under the developer's account. Anthropic also exposes an audit log export and a compliance API, but they cover changes to server-managed settings, not tool calls.

Cursor has the same shape. Its hooks include sessionStart, beforeShellExecution, beforeMCPExecution, afterFileEdit and stop, so a script can write a JSON line per event. Cloud agents run the command hooks committed in .cursor/hooks.json, and Enterprise teams can push hooks from the dashboard.

Codex ships OpenTelemetry instead of hooks. Export is off by default. When you turn it on, the event set includes codex.tool_decision, which records approved or denied and whether the decision came from configuration or the user, plus codex.tool_result, codex.conversation_starts with the sandbox and approval policy, and API and stream events. Prompt text is redacted unless you explicitly enable it. The stream is still client-side: if a developer edits the config or a build never exports, the record is not there.

GitHub's Copilot audit log covers plan and policy changes and agent activity on GitHub. GitHub's own documentation says the audit log "does not include client session data, such as the prompts a user sends to Copilot locally", and the log retains 180 days. That is a reasonable vendor log and it is not a record of what an agent did on a laptop.

The pattern: each vendor answers for its own surface, and the local tool calls that touch source code and credentials are left to a hook or to you.

What a record has to contain

A record that survives an audit has more fields than a log line, and fewer than a packet capture. This is the shape we use:

FieldWhy it is there
tsRFC 3339 in UTC, so events order across machines and time zones
endpoint, tenantWhich machine and which customer the action belongs to
actorThe human, from the IdP, not the shell account
agent, sessionAgent name and version, session id, parent session for subagents
actionTool or MCP call, server name, tool name, argument hash, redacted arguments
policyRule id, policy version hash, decision, and the reason string
resultExecuted, blocked, failed, with an exit status where one exists
prev, hashThe link to the previous record and this record's digest
sigDevice or service signature, so a record can be attributed to a key

One record, trimmed:

{
  "id": "01J9Z6Q3K8",
  "ts": "2026-11-03T14:02:11.418Z",
  "endpoint": "mbp-1842",
  "actor": "dev@example.com",
  "agent": { "name": "claude-code", "version": "2.1.310" },
  "session": "s-7f31",
  "action": {
    "type": "mcp_call",
    "server": "filesystem",
    "tool": "read_file",
    "args_hash": "sha256:9c1f...",
    "args_redacted": { "path": "~/.aws/credentials" }
  },
  "policy": {
    "id": "alyria.lyra/deny.secret-read",
    "version": "sha256:4b2e...",
    "decision": "deny",
    "reason": "secret-tier path"
  },
  "result": { "status": "blocked" },
  "prev": "sha256:1a7c...",
  "hash": "sha256:77d0..."
}

The policy version belongs in the record, because "denied by policy" means nothing six months later if the policy has changed twice. And the arguments go in twice: a hash for integrity, a redacted copy for readability. A record with no arguments cannot answer what was read; a record with full arguments is a copy of the secret you were protecting.

Verifying the chain

A hash chain is cheap to check and worth checking in front of an auditor rather than asserting. Export the records as JSON Lines and recompute:

import hashlib, json

def canon(rec):
    body = {k: v for k, v in rec.items() if k != "hash"}
    return json.dumps(body, sort_keys=True, separators=(",", ":")).encode()

prev = "0" * 64
for line in open("audit-export.jsonl"):
    rec = json.loads(line)
    assert rec["prev"] == prev, f"chain break at {rec['id']}"
    digest = hashlib.sha256(canon(rec)).hexdigest()
    assert digest == rec["hash"], f"record altered at {rec['id']}"
    prev = digest
print("chain ok", prev)

chain ok proves the export is internally consistent and that no record in it was edited after the fact. It does not prove the export is complete, and it does not prove the last record is the real head. For that, compare the final digest with a value stored somewhere the endpoint cannot reach: a SIEM, an object store with immutable retention, or a separate system that received the same head at the time.

A hash chain is tamper-evident, not tamper-proof. Someone who can delete a record and rewrite the rest can produce a chain that verifies, which is why coverage and an external anchor matter more than the digest algorithm.

What the trail still cannot prove

  • It cannot prove completeness from inside itself. Only an independent view of the same activity, such as network logs or the policy service's own stream, can show that nothing was dropped.
  • It cannot prove the content was true when written. A compromised agent can log a lie. This is why the decision record should come from a process the agent does not control, signed with a key the agent does not hold.
  • It cannot prove intent. It can show that a file was read and a connection followed, and let a human decide what that meant.
  • It cannot outlive its retention. A 180-day vendor log and a six-month legal obligation line up badly when the incident is discovered in month seven.

Coverage is the design problem

Start from the actions that matter, not from the log format:

  • Local shell commands, including the ones that never touch the network.
  • File reads and writes, with the path and a hash of the content.
  • MCP calls over stdio and over HTTP, including the server name and tool.
  • Model calls, with the model id and the data classification of the request.
  • Policy decisions and approvals, including who approved and when.
  • Secret leases, issuance and use.

No single client covers that list, and none of them sees a stdio MCP server another tool started. That is the argument for one audit chain at the endpoint, fed by a policy service that mediates the calls, rather than four exports joined by hand. Alyria Beacon produces the decision records on the machine, and Spectra routes them as OpenTelemetry into Observatory or into your own SIEM, so the retention question becomes yours to answer with the tools you already run. See Observatory, Spectra and the audit export guide.

The compliance frame, briefly

The EU AI Act is the reference auditors are starting to use. Article 12 requires high-risk systems to allow automatic recording of events over the system's lifetime. Article 19 requires providers, and Article 26(6) requires deployers, to keep automatically generated logs for at least six months, to the extent the logs are under their control. A coding agent may fall outside the high-risk categories, and the logging pattern is still the one a security questionnaire asks for: automatic, complete over the period, retained, and producible on request.

What breaks in practice

  • Logging only cloud-side activity, then discovering the interesting calls were local stdio.
  • Writing the log to a user-writable file on the endpoint, which the same user can edit.
  • Omitting the policy version, so a denial cannot be explained later.
  • Keeping full arguments, which turns the audit system into a secret store.
  • Never testing the export. The first time anyone recomputes the chain should not be during an incident review.

Questions this usually raises

Is a SIEM enough? A SIEM is the right place to keep and query the trail, and it is not the place the record is created. If the endpoint never emits a record, the SIEM has nothing to store. Emit at the decision point, ship immediately, retain per policy.

Do we need to record prompts? Not by default. Record the model id, the data classification and a hash of the request. Prompt text is where source code and secrets leak into the logging system, and it is rarely needed to answer the four questions.

How is your team using AI agents?

Pick your function and answer 20 questions on agent workflows and controls. Your result appears on the page.

Governed AI agents for the work your teams do.