Agentic Cyber

Claude Code Audit Trails: What the Session History Cannot Tell an Investigator

By Mara Voss · October 13, 2026

Category: defensive-architecture-security-controls

Claude Code Audit Trails: What the Session History Cannot Tell an Investigator

Saved session history narrates the work. The executions live in a separate stream that is off by default, redacted by default, and gated by hooks that fail open.

Key takeaways

  1. The problem Teams treat a coding agent's conversation history as its forensic record. It narrates intent, it can lag the live session, and it never holds the shell command that changed the file.

  2. Core insight Three planes get confused as one: the exported event record, the written decision about captured content, and the permission system that actually refuses an action. Hooks belong to the first, never the third.

  3. Practical outcome Turn on event export with an explicit protocol, decide the content question with privacy review, stamp ownership attributes, and prove a hook timeout behaves the way you intended.

Your saved session history is not a forensic record of what the coding agent did. It is a record of what the model said about what it was doing, and those diverge exactly when it matters.

The belief is reasonable, which is why it is so common. For a chat product the conversation really is the artifact. For a tool that reads your filesystem, runs shell commands and reaches the network, the conversation is a narration layered over a separate stream of executions that has its own identifiers, its own timing and its own content rules. This piece uses Claude Code's published documentation as the worked example, because it states clearly what gets emitted, what gets withheld, and which of its guard rails stop nothing when they fail. The first half is why the assumption collapses. The second half is the configuration that replaces it. No attack technique appears in either.

Where the Assumption Comes From, and Why It Survives Review

Three things keep it alive. The first is inheritance: everyone arrived at agentic tooling through chat interfaces, where scrollback and history were the same object. The second is visibility. The narration streams past on screen all day, while an event pipeline announces itself only when somebody goes looking for it, so the thing nobody configured is also the thing nobody notices is absent. The third is that the narration is genuinely useful for understanding intent, and usefulness gets mistaken for sufficiency during a design review.

There is a sharper detail that undermines the assumption on its own terms. The hooks reference notes that the on-disk conversation file a hook receives may lag the live in-memory conversation. So even as a story about the work, the saved file is not guaranteed to be synchronous with the work. Anything you intend to rely on during an investigation has to be emitted deliberately, at the moment of the action, by something other than the narrator.

The Conditions Under Which It Falls Apart

Read the defaults rather than the marketing and the failure modes are specific.

  • Nothing is collected until somebody turns it on. The monitoring documentation lists the enabling environment variable as required, with per-signal exporters chosen separately for metrics, logs and traces. It also states that the product ships no default OTLP protocol, so a half-configured collector is a realistic way to end up with an endpoint and no data arriving at it.
  • Counters are not a reconstruction. The documented metrics tally sessions, tokens, cost, lines of code, edit decisions and active time. Those are capacity and spend instruments. None of them can place a single command in a single session on a single machine, which is the only question an investigation asks.
  • Content is withheld by design. Per the same page, prompt text is redacted unless prompt logging is explicitly enabled, assistant responses are replaced with a placeholder unless response logging is enabled, and tool parameters and inputs sit behind a separate tool-details setting that also governs Bash commands, MCP server and tool names, and skill names. Without those decisions you learn that a tool ran, how long it took, how many bytes went each way and whether it succeeded. You do not learn what it ran.
  • The default identity is deliberately anonymous. The documentation describes the user identifier as a random value generated on first run and persisted locally, carrying no personal information and not derived from the account. Email appears only where available. Tying a session to a person and a repository is a configuration task, not a given.

The enforcement side has its own trap. The hooks documentation is explicit that a command, HTTP or MCP-tool hook which reaches its timeout is cancelled with its output discarded, and that on the pre-tool event a timed-out hook does not stop the call: it proceeds through the ordinary permission flow. It also warns that a hook exiting with code 1 counts as a non-blocking error despite being the conventional Unix failure code, and that policy enforcement needs exit code 2. With the default command timeout documented at 600 seconds, a hook can hang for ten minutes and then let the action through. A control that fails open is easy to sign off on and hard to notice.

A Better Model: Three Planes That Must Not Be Confused

Separate the concerns and most of the confusion dissolves. Design each plane on purpose, and never let one stand in for another.

  1. The record plane. Structured per-action events, exported off the developer's machine to storage the session has no credential for. This is what answers questions after the fact.
  2. The disclosure plane. A written decision covering which content is captured, who may read it, and how long it survives. Privacy review belongs here, before the first event ships, not after.
  3. The enforcement plane. Whatever actually refuses an action. This is the permission system. The hooks documentation states that its conditional filter is best-effort and directs hard allow and deny rules to permissions instead, which is the clearest signal you will get that hooks are instrumentation and guard rails rather than a boundary.

The same split is worth drawing across your other agents, and we treat the general version of it in why guard rails are not controls. For the compliance-facing view of the record plane, with the claims an auditor tests, see what an agent's event stream has to establish.

Hardening Each Plane With What the Documentation Gives You

  • Enable telemetry on managed machines, and set the protocol explicitly. Turn on the logs exporter, not only the metrics one. Metrics without events gives you a spend dashboard and calls it observability.
  • Collect the events that rebuild a session. The documented event names include the user prompt, the tool result with its tool name, success flag, duration and decision source, the tool decision with its accept or reject outcome and origin, plus API request, error and refusal events, permission mode changes, authentication, MCP server connections and skill activations. That set, in order, is a session reconstruction.
  • Decide the content question rather than inheriting it. Enabling tool details is what turns "a shell command ran" into the command itself, which is precisely what an investigator needs and precisely where a developer sometimes pastes a credential. Pair it with redaction at the collector, restricted read access and a retention limit. The related exposure path is covered in how coding agents leak API keys.
  • Attach ownership at the resource level. The custom resource-attribute variable lets you stamp team, department and cost-centre keys onto every event, and session identifiers are included by default. Without those, a query by owner or by project is impossible no matter how complete the stream is.
  • Keep an independent copy through hooks. Pre-tool and post-tool hooks receive the session identifier, the working directory, the tool name and the tool input, which is enough to write your own immutable line per action. Treat that as a second source, not as the gate.
  • Make hook failure a choice. Recent versions accept a per-hook failure setting whose default is to continue; set it to block where you want a failed check to behave like a refusal, and note the documented exceptions where it has no effect, including stop and task-completion events and background hooks. Then deliberately break one hook and confirm the behaviour matches the intent.
  • Watch the sub-session boundary. Subagent start and stop are separate hook events, and work delegated into a child session needs the same coverage as the parent. The gaps that open there are catalogued in what to verify about child sessions.
  • Treat the pipeline as a monitored dependency. Export intervals are short for logs and a minute apart for metrics by default, so a suspended laptop or a killed process can drop the tail of a session. Alert when an enrolled machine goes quiet.

Three Gaps the Defaults Page Will Not Warn You About

Four documented details together decide whether your collected stream is complete. None is hidden, and none appears in a getting-started snippet.

  • Subprocesses do not inherit the exporter settings. The monitoring reference states that the Bash tool, hooks, MCP servers and language servers never receive the telemetry environment variables. The hook you wrote to keep an independent copy therefore runs outside the pipeline it backs up.
  • A repository cannot enrol itself. Per the same page, the exporter variables are ignored in a project's own settings files. The right default, and it makes coverage an endpoint management task.
  • Nothing names the machine by default. Metrics and events carry no host name unless you set it through the resource attributes variable. Without it you can prove an action happened and not where.
  • Traces are still labelled beta, behind a second opt-in flag. Useful, not yet what you present to an assessor.

One caution about the wider vocabulary. The vendor-neutral generative AI conventions, which would let you query a coding agent and a production agent through one schema, left the main semantic conventions repository for a dedicated one whose releases list is empty and whose schema URL is a placeholder. Normalise at your collector. Do not promise an auditor a portable schema yet.

What Changed About the Meaning of an Approval

One shift rewrites the field an investigator relies on most. The security documentation describes a starting permission mode in which a separate classifier model reviews actions in place of the person and blocks what it judges unsafe, with explicit allow and deny rules still applying and the organisation able to turn the mode off. Read your records with that in mind. "The action was allowed" no longer implies a human saw it. The decision source on the tool decision event, with the permission mode change event, is what separates a configured rule from a classifier verdict from somebody pressing approve. That is the difference between a control finding and a personnel conversation.

Two more items from that page belong in the record plane. A configuration-change hook can audit or block settings edits made during a session, closing the obvious hole in a reviewed configuration: a session that can rewrite it afterwards. Trust prompts also behave differently in non-interactive runs, where neither the workspace trust dialog nor the project server approval appears, so assert coverage for a continuous integration job rather than inferring it from interactive behaviour. For hosted sessions the page states operations are logged for audit, so part of your trail may sit outside your collector on its own retention. Write down which part.

Build the Record Plane First, and Stop Treating Hooks as a Gate

The instinct after reading this is to write blocking hooks, because enforcement feels more urgent than evidence. The documentation argues against that order better than any outside opinion could. A pre-tool hook that times out is cancelled and the call proceeds through the ordinary permission flow. The conventional Unix failure exit code counts as a non-blocking error. The matcher is best-effort, with hard allow and deny rules directed to the permission system. And a hook inherits none of the telemetry settings. A control that fails open, is bypassed by a wrong exit code and reports through an unconfigured channel is not a gate.

So spend the first week on the record plane: central export off the developer's machine, the event set that rebuilds a session, ownership at the resource level, an alert when an enrolled machine goes quiet. Evidence compounds: it tells you what your agents actually do, which is the only reliable input into what should be refused. Enforcement written first blocks the imagined action and misses the real one.

In an environment where one destructive action is unacceptable, a payments system or production infrastructure, enforcement does come first. Even then it does not come from hooks, but from explicit deny rules, sandbox network and filesystem isolation, and running the process inside a container. Teams that skip the record plane for hook-based blocking can show what they meant to prevent and not what happened.

An Hour of Work, Ordered by Payoff

  1. Establish whether any central event collection exists today. If the answer is no, that is the whole finding, and one team is enough to pilot.
  2. Write the disclosure decision down: prompts, responses, tool details, retention, who reads it. One page, signed by someone who can sign it.
  3. Run the reconstruction drill. Hand a colleague only your central events for a past session that edited several files, and ask which commands ran in what order, which were allowed by a rule versus by a person, and what the agent read just before the riskiest one. If they reach for the developer's machine, you do not yet have a trail.
  4. Force a hook timeout on purpose and observe whether the action proceeds. Fix the setting, then repeat the test.
  5. Move every hard allow or deny rule out of hooks and into permissions, and leave the hooks doing what they are good at.

Repeat the drill after each settings change. It reliably surfaces the problems a design review cannot see: an exporter that silently drops a batch, a collector that rewrites timestamps into its own receipt order, a machine that was never enrolled. Product behaviour also shifts between releases, so verify any setting named here against the current documentation, and route production risk decisions through the people accountable for them.

Frequently Asked Questions

If we enable tool details, how do we keep credentials out of the record?

Redact at the collector rather than at the source, since the agent cannot know which argument is sensitive. Combine pattern-based scrubbing, a short retention window on that field, and read access limited to named investigators.

Is an OpenTelemetry collector on its own enough?

It is the transport, not the evidence store. A collector that developer machines can also write to, or whose backend has no retention lock, gives you queryability without integrity. Mirror the forensic event set into append-only storage.

Do delegated subagents and child sessions appear in the same trail?

Only if you configured them to. Subagent start and stop are their own hook events, and a child session inherits neither your attention nor necessarily your guard rails, so verify coverage there before you rely on the parent's record.

What happens to the trail when a developer works offline?

Buffered events can be lost if the process ends or the machine suspends before the next export. Plan for partial sessions, keep the collector's receipt time alongside the source timestamp, and alert when an enrolled machine stops reporting.