Yesterday I wrote about the seventy-four days OpenAI's agents spent working their way into Hugging Face, and closed on a bullet I could not properly support: log trajectories rather than actions, and decide in advance what that log is allowed to wake up.
That bullet needs a guide behind it. This is it.
Start with the part that surprised me. You are already logging. Claude Code has been writing a detailed record of every session to your own disk since the day you installed it, and if you are anything like me, you have never opened one.
What is already on your disk
Three separate trails, none of them advertised.
The transcript, at ~/.claude/projects/<project>/<session-id>.jsonl. I took one ordinary session on this machine and counted: 1,434 records, 467 assistant messages and 322 user messages, carrying 146 Bash calls, 101 file reads and 12 edits between them. Across every project there are 1,156 of these files, 676 MB of transcript in total.
The schema is where it gets interesting. Each record can carry cwd, gitBranch, version and entrypoint, a permissionMode plus dedicated records every time that mode changes, isSidechain to mark subagent work, durationMs, toolUseResult, and a uuid/parentUuid/promptId/requestId chain that lets you walk any action back to the prompt that caused it.
Then three fields I would never have guessed were there: attributionSkill, attributionMcpServer and attributionMcpTool. When a call comes from a skill or an MCP server, the transcript records which one. Every file read, every command, every server called, and the answer to "what invoked this" sitting in the same record.
The prompt history, at ~/.claude/history.jsonl. Mine is just under 16,000 lines, and every line has exactly four fields: display, pastedContents, project, timestamp. That is everything you typed and nothing the agent did.
The file snapshots, under ~/.claude/file-history/<session>/. 104 session directories here, 58 MB, holding pre-edit copies of touched files. This is what /rewind restores from, and it is the only one of the three that tells you what a file looked like before.
Two things to know before you lean on any of this. It expires: cleanupPeriodDays defaults to 30, and Claude Code deletes older session data at startup. And it is plaintext, which the docs are direct about: if a tool reads a .env file or a command prints a credential, the value lands in the transcript. Your audit log is also a secrets sink, protected by nothing but file permissions.
Which makes this trail good for reconstructing an accident, and that is exactly what the incident runbook uses it for. Against someone who wants the record changed, a plaintext file owned by the compromised user is worth very little.
Why your compliance feed has none of this
Anthropic shipped a Compliance API in May. Reading the announcements, you would assume the audit problem is handled at the org level now.
Read the scope instead. The Activity Feed records authentication, chat, file, project, administrative and platform actions, in hundreds of distinct activity types, queryable within a minute and kept for six years. The content endpoints serve claude.ai data: chats, files, projects, attachments, and transcripts of Cowork sessions running in Anthropic-managed environments.
Claude Code on your laptop is not in there. Not the Bash commands, not the file edits, not the MCP servers it called. The other half of the enterprise story, the Claude Code Analytics API, returns daily aggregates per user: sessions, lines added and removed, commits, and how many Edit proposals were accepted or rejected. It will tell you that a developer rejected five edits on Tuesday. It will not tell you what they were.
One exception cuts the other way. Claude Code's security docs say every operation in a cloud session is logged for compliance, and the Compliance API already returns remote session transcripts containing tool_use and tool_result blocks, kept for six years. Check the scope, though: that endpoint currently serves only sessions whose product_surface is cowork_remote.
So the machinery for handing an agent's tool calls to a compliance team is built and shipping. Today it points at Cowork, and the beta note tells you to expect other surfaces later. The copy of Claude Code on your own machine, which is where most of us run it, is not one of them yet.
What the feed gives you is a control-plane record: identity, configuration, and the lifecycle of resources. It stops at the moment a conversation opens. So the log your security team can query centrally, and keeps for six years, knows that you opened a session. The log that knows your agent ran rm -rf is a plaintext file on your own machine that deletes itself after thirty days.
Telemetry is the real channel, and it ships redacted
The bridge between those two is OpenTelemetry, which Claude Code supports properly. CLAUDE_CODE_ENABLE_TELEMETRY=1 plus an OTLP endpoint gets you metrics every 60 seconds and log events every 5, including claude_code.tool_result, claude_code.tool_decision, claude_code.permission_mode_changed, claude_code.mcp_server_connection and claude_code.api_error.
Turn it on and the first thing you notice is that it tells you almost nothing. Content is redacted by default, all of it: prompts arrive as <REDACTED> with only a prompt_length, and assistant responses the same. Tool details are gone too, which is the one that matters: without OTEL_LOG_TOOL_DETAILS=1 there are no Bash commands, no file paths, no MCP tool names, and third-party plugin calls collapse to the string custom or mcp. You get a faithful record that a tool ran, and no way to know which.
So you set the flag, and now every command and every file path your agent touches flows to your collector. There is OTEL_LOG_TOOL_CONTENT for tool inputs and outputs on top of that, and OTEL_LOG_RAW_API_BODIES for the full Messages API request and response, conversation history included, which the docs note implies consent to everything the other three flags reveal.
That is the shape of the decision, and it is close to binary. Off, and your audit log cannot distinguish a linter from a database drop. On, and you are shipping source code, commands and credentials to whatever your collector writes to. The middle setting most teams want, tool names and file paths without file contents, is roughly what OTEL_LOG_TOOL_DETAILS alone gives you, and it is the setting I would start from.
Two details worth having. Administrators can lock the OTLP endpoint through managed settings, and Claude Code will strip conflicting developer-set variables at startup so a local export cannot be redirected. And subprocesses do not inherit the OTEL_* variables, so Bash commands, MCP servers and language servers started by the agent are not quietly instrumented.
The log you actually want is one hook
If you want a trail with your own rules, hooks are the mechanism, and PostToolUse fires after every call with tool_name, tool_input, tool_use_id and tool_result.
Building it yourself is the supported path rather than a workaround. Anthropic's own security documentation, under team security, tells you to monitor Claude Code through OpenTelemetry metrics and to audit settings changes with ConfigChange hooks. There is no product that does this for you.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -c '{ts: now, session: .session_id, mode: .permission_mode, tool: .tool_name, input: .tool_input}' >> ~/.claude/audit.jsonl"
}
]
}
]
}
}Every hook payload carries session_id, transcript_path, cwd, permission_mode and hook_event_name, plus agent_id and agent_type inside subagents. permission_mode is the sleeper field: your own log records whether the session was in bypassPermissions at the moment of the call, which is the first question anyone will ask afterwards.
Four other events earn their place. PermissionDenied tells you what the agent tried that you refused, a better signal than anything it was allowed to do. ConfigChange fires with a config_source when settings or skills change under you, and FileChanged takes literal filenames to watch, so .env|.envrc is a one-line tripwire. SubagentStart gives you the agent_type and an agent_id to group a sidechain by.
Collecting was the easy half
Every section above is a plumbing problem, and plumbing problems get solved. The reason I wrote this post is the next question, and it has no configuration flag.
Who reads it, and what wakes someone up?
Hugging Face is the case study, and the detection half of their story worked. Their anomaly-detection pipeline, running LLM-based triage over security telemetry, correlated signals that individually looked like ordinary noise, and it fired. The gap was between seeing and stopping: TechCrunch reported that the criticality was never raised and the on-call team was never paged. The signal sat in a queue, because there was no rule to carry it any further.
That failure mode is not new, and there are numbers on it. Intezer's February report, drawn from 25 million alerts across 10 million monitored endpoints and 82,000 forensic endpoint investigations, found that 1.9% of low-severity and informational endpoint alerts were real incidents. Roughly one in every fifty alerts that nobody is expected to read is a genuine threat, filed as noise and never reviewed by anyone.
Intezer sell alert triage, so weigh that figure accordingly. The independent version comes from Mandiant, whose M-Trends 2026 draws on more than 500,000 hours of frontline incident response during 2025. Global median dwell time went up, from 11 days to 14. Organisations found the intrusion themselves 52% of the time, which is genuine improvement on the 43% a year earlier. The other 48% found out because somebody outside told them.
Now point that at an agent. Your agent generates hundreds of tool calls an hour, and each one looks legitimate on its own, because you gave it the tools on purpose. If you route that into a queue nobody has agreed to read, you have built the 1.9% problem with a much larger denominator.
So write the escalation rule before you build the pipeline. Mine has three tiers and fits on a napkin.
Log everything, cheaply, and expect never to read most of it. Alert on a short list of shapes: a denied permission followed by a different route to the same file, a .env read, an MCP tool called for the first time in this repository, a session that flips to bypassPermissions and then touches infrastructure. Page a human for almost nothing, and know in advance what that nothing is.
The unit matters as much as the threshold. Allowed-or-denied per call is the wrong grain, because it cannot see a sequence. Vispute and Kadam make the formal version of this argument in a March paper proposing what they call reasoning provenance: execution traces and state checkpoints record what an agent did, but why it chose to do it cannot in general be reconstructed from them, so the reasoning has to be captured as a first-class field at the time. It is a position paper with a reference implementation and no evaluation numbers yet, so treat it as a direction rather than a result.
The direction is right. Your transcript already has promptId and parentUuid on every record, which is enough to group a session into trajectories today, without waiting for a standard.
What to actually do
- Look at one transcript this week. Open a
.jsonlfrom~/.claude/projects/and read what a normal session of yours contains. You cannot write an escalation rule for a shape you have never seen. - Decide the retention on purpose.
cleanupPeriodDaysis 30 by default. If you want a trail for an investigation that starts in month two, ship it somewhere else, and remember the file is plaintext when you choose where. - Turn on
OTEL_LOG_TOOL_DETAILSand stop there, unless you have a specific reason to log content. Tool names and file paths answer most questions. Raw API bodies answer the rest and create a much bigger problem. - Write the page-a-human list first. Three shapes, agreed with whoever carries the pager. A pipeline built before that list will produce a queue, and queues do not wake anyone.
The detail I keep returning to from the OpenAI timeline is that both companies had the evidence. Hugging Face's pipeline saw the attack in real time. OpenAI's transcripts contained every step of it, in full, on their own infrastructure. It still took a phone call on 20 July, about credentials that had already been rotated, before anyone worked out what had happened. On Mandiant's split, the company that built the agents was in the 48%.
Nobody needed a better log. They needed a rule about who gets woken up.