The agent decision log: what auditors, insurers and incident responders will ask your coding agents to prove
An agent decision log is one record per tool call that states what the agent tried to do, which artifact was involved, which policy version judged it, what the verdict was, and who or what made that call. Auditors, insurers and incident responders converge on three questions: what was installed or fetched, under which policy, and who approved it. Claude Code already exposes most of the raw fields through PreToolUse hooks and OpenTelemetry tool_decision events. The gaps are the policy version, the artifact identity and tamper evidence, and those you add yourself. The schema below is the minimum set that answers all three questions.
The symptom: an incident you cannot scope
The failure mode is easy to describe. A malicious package or MCP server is disclosed. Someone asks, "Did any of our agents install it, and when?" The honest answer is a week of collecting shell histories and transcript files from laptops.
The postmark-mcp case shows what that question looks like in practice. Version 1.0.16 added one line that BCC'd every outgoing email to an outside address, and the advice after disclosure was to remove the package, rotate credentials exposed through email and review email logs for the BCC traffic (The Hacker News, 29 September 2025). Every one of those steps starts with a list of the machines and sessions that ran the bad version. Without a central record, that list does not exist.
Vendor guidance states the goal but not the record. Cerbos's MCP vetting checklist asks: "Can you produce a record of every action a given server or agent took, with the policy that allowed or denied each call," and says answering an incident review should be "a query, not an investigation" (Cerbos, 16 August 2026). It names no fields.
What the agent runtime already emits
Claude Code offers two sources, and they differ in timing and content.
Hooks. Every hook receives common fields including session_id, prompt_id, transcript_path, cwd, permission_mode and hook_event_name. A PreToolUse hook also receives tool_name, tool_input and tool_use_id, before the tool runs (Claude Code hooks reference). For a Bash call, tool_input.command holds the exact command line. A PermissionDenied event fires "when auto mode denies a tool call."
OpenTelemetry. With telemetry enabled, Claude Code exports a claude_code.tool_decision event with tool_name, tool_use_id, decision (accept or reject), tool_source and source. The source attribute takes the values config, hook, user_permanent, user_temporary, user_abort or user_reject (Claude Code monitoring). That one attribute answers "who approved it" at the level of mechanism.
The trap is the default. Tool parameters, including bash_command and full_command for Bash, are logged only when OTEL_LOG_TOOL_DETAILS=1 is set. Without it, the telemetry says a Bash call was accepted by a user and omits what the command was. A tool_result event is "Not emitted if the tool call was rejected", so blocked actions appear only in tool_decision.
Three things neither source records
Policy version. A hook that blocks a package knows which rule fired. The runtime does not. If the hook does not write the rule ID and the version of the policy file it evaluated, you cannot later prove that the control existed at the time of the decision.
Artifact identity. tool_input.command is a string such as npm install foo. An auditor needs npm, foo and the resolved version. A responder matching against an advisory needs the same three values. Parse them at decision time; the registry may serve a different version tomorrow.
Tamper evidence. A JSON file on the developer's laptop can be edited by the developer, by the agent, or by malware running as either. The log must leave the machine, or carry a hash chain that makes edits detectable, or both.
The minimum schema
One JSON object per decision. Fields marked source: runtime are copied from hook input or telemetry; source: you must be produced by your check.
{
"event_id": "uuid", // you
"ts": "2026-10-15T09:14:03.120Z", // you, UTC
"host_id": "device inventory ID", // you
"user": "corporate identity, not local uid", // you
"agent": "claude-code", // you
"agent_version": "x.y.z", // you
"session_id": "...", // runtime
"tool_use_id": "toolu_...", // runtime
"hook_event_name": "PreToolUse", // runtime
"permission_mode": "default", // runtime
"cwd": "/path/to/repo", // runtime
"tool_name": "Bash", // runtime
"action_raw": "npm install [email protected]", // runtime (tool_input)
"artifact": { // you
"kind": "package|mcp_server|skill|url",
"ecosystem": "npm",
"name": "foo",
"version": "1.2.3",
"digest": "sha512-... if known"
},
"verdict": "known_bad|unknown|no_known_bad", // you
"decision": "block|ask|allow", // you
"decided_by": "policy|user|classifier", // you, cross-check with OTel source
"approver": "identity if a human approved",// you
"policy_id": "agent-install-policy", // you
"policy_version": "git SHA or semver", // you
"rule_id": "R4-age-window", // you
"intel_snapshot": "list version or date", // you
"prev_hash": "sha256 of previous record", // you
"record_hash": "sha256 of this record" // you
}
JSON does not allow comments; strip them before use. The verdict field keeps the three-state model deliberately. "No known bad" and "unknown" are different facts, and an insurer or auditor will treat a log that records "safe" as an overclaim.
Mapping fields to the people who ask
Question Asked by Fields that answer it
-------------------------------------------------- ------------------ ------------------------------------------
Did any agent install X@V? Where, when? Incident response artifact.*, host_id, user, ts, decision
What else did that session do? Incident response session_id, tool_use_id, action_raw
Which policy allowed it, and did it exist then? Auditor policy_id, policy_version, rule_id, ts
Who approved it: a rule, a person, or a model? Auditor, insurer decided_by, approver, OTel source
Was the threat data current at decision time? Insurer, IR intel_snapshot, verdict
Has anyone edited this record? Auditor, counsel prev_hash, record_hash, off-host copy
How fast could you stop it everywhere? Insurer ts of first block after policy change
The last row is easy to miss. If a denylist update reaches devices at 10:02 and the first block for that artifact appears at 10:05, the log itself measures your containment time.
Implementation notes and the trade-offs they introduce
Write the record in the PreToolUse hook, before the exit code. The hook already holds the decision. Append the record, then exit 2 to block or 0 to allow. Anthropic's documentation notes that a timed-out PreToolUse hook "doesn't block the tool call" unless "onFailure": "block" is set. Logging adds latency, so buffer locally and ship asynchronously; never make the block depend on a network write.
Turn on tool details deliberately. OTEL_LOG_TOOL_DETAILS=1 puts command lines into your collector. Command lines can contain tokens and internal hostnames. Treat the collector as a sensitive store and set its access controls before enabling the flag.
Use the OTel stream as a cross-check, not the source of truth. If your hook log shows allow by policy and the OTel source for the same tool_use_id is user_permanent, a user rule fired first. That disagreement is a finding.
Retention is a legal question. How long to keep these records depends on your regulators and contracts. This is general information, not legal advice; set retention with counsel.
Residual risk
The schema records decisions the hook sees. Actions taken inside an already-approved process, such as a post-install script or an MCP server's own network calls, never pass through PreToolUse. Pair the decision log with egress and endpoint telemetry for those. A hash chain also proves only that records were not altered after writing; it cannot prove a record was written. Ship records off the host quickly so a gap is visible.
Reproduction notes
Field names are from the Claude Code hooks and monitoring documentation as read on 15 October 2026. Hook and telemetry schemas change between versions; pin agent_version in every record so a reader knows which field set applied.
threat.cafe's planned team tier centres on this kind of searchable log of every agent check and block; the schema above is open, and you can build it today with your own hooks.
Sources
- First Malicious MCP Server Found Stealing Emails in Rogue Postmark-MCP Package — The Hacker News, 29 September 2025
- MCP server vetting checklist for enterprises — Alex Olivier, Cerbos, 16 August 2026
- Hooks reference — Claude Code documentation, accessed 15 October 2026
- Monitoring usage — Claude Code documentation, accessed 15 October 2026