March 23, 2026
Detection Engineering for Claude Code, Part 1

TL;DR
- Claude Code emits OpenTelemetry natively across metrics and structured log events. The data is already there.
- That telemetry unlocks four detection categories most teams currently have zero coverage on: unauthorized tool use, data exfiltration via AI, MCP server abuse, and prompt injection.
- Raw OTLP is deeply nested and not analyst-friendly. Part 2 covers how to normalize it and get it into your SIEM.
Most engineering teams have been rolling out Claude Code for weeks or months by the time security asks what it’s doing. Adoption happens fast, visibility comes later. Claude Code already emits telemetry natively over OpenTelemetry. The data exists. The question is whether you have a pipeline to receive it, normalize it, and get it somewhere your security team can actually use.
This post covers what that telemetry contains and what it lets you detect. Part 2 covers how to get it flowing.
What Claude Code actually emits
Claude Code exports metrics and structured log events.
Metrics:
- Session counts
- Token usage by model
- Cost in USD
- Lines of code changed
- Active time
Structured log events:
- user_prompt
- Full prompt text when
OTEL_LOG_USER_PROMPTS=1is enabled. Off by default.
- Full prompt text when
- api_request
- Calls to the Anthropic API. Includes model, token counts, cost, and duration.
- tool_result
- Tool invocations with full detail: tool type in
tool_parameters.bash_command, full command intool_parameters.full_command, outcome insuccess, and decision indecision_typeanddecision_source.
- Tool invocations with full detail: tool type in
- tool_decision
- Fires at the moment a permission decision is made, before execution completes. Carries
decision_type(accept/reject) and decision_source. More useful thantool_resultfor catching rejected or policy-violating commands since it fires at decision time, not after the fact.
- Fires at the moment a permission decision is made, before execution completes. Carries
- mcp_tool_use
- MCP server and tool invocations. Add
OTEL_LOG_TOOL_DETAILS=1for server names and tool names.
- MCP server and tool invocations. Add
What this telemetry lets you detect
Getting Claude Code telemetry into your SIEM opens up four detection categories that most teams currently have zero coverage on.
Unauthorized tool use
Same mental model as “process executed suspicious command,” just through an AI agent. Intent and outcome both live in claude_code.tool_result. Use tool_parameters.bash_command to identify that a Bash tool fired, then read tool_parameters.full_command for the actual command. success (true/false) tells you the outcome, decision_type (accept/reject) tells you whether it was allowed, and decision_source tells you why: config, user_permanent, user_temporary, user_abort, or user_reject. Alert on commands that were attempted but rejected, or auto-accepted when your policy says they shouldn’t have been. The more sophisticated version is behavioral baselining: a frontend engineer whose sessions suddenly start querying databases or accessing infrastructure configs is worth investigating.
Day-one detections:
curlorwgetto an external IP infull_command- Reads of
.aws/credentialsor.envfiles- Packages installed from untrusted registries
chmodon sensitive files or SSH key generation- Commands attempted but rejected (
decision_type: reject)- Commands auto-accepted via config that fall outside policy
Data exfiltration via AI
The threat model with the least existing coverage. Claude Code can read files and make outbound requests in the same session, and without this telemetry none of that is visible. The core detection: correlate tool_result events where tool_parameters.bash_command is Bash to identify shell activity, then key off tool_parameters.full_command for the actual command. bash_command only contains a short label like echo — any rule written against it alone will miss most of the signal. mcp_tool_use extends this to connected services: read internal files, send via a Slack or email MCP server, both actions appear in the telemetry.
What to look for:
- Reads of sensitive file paths followed by
curlorwgetinfull_commandwithin the same session session.idcorrelation between file reads and outbound network callsmcp_tool_useevents that pass file content to a Slack, email, or external API connector
MCP server abuse
MCP servers give Claude Code authenticated access to external systems: Slack, Jira, databases, cloud APIs, internal tools. That’s a lateral movement path through AI tooling with no existing detection coverage for most teams. There is no connection lifecycle event, so you won’t see a session connect to a new MCP server. Visibility is limited to claude_code.tool_result events fired when a tool is actually invoked. Server identity only surfaces when OTEL_LOG_TOOL_DETAILS=1 is set, exposing mcp_server_name and mcp_tool_name inside tool_parameters. If a session is compromised via prompt injection, the attacker inherits access to every connected MCP server. tool_result is where you catch that.
What to flag:
Unknown or unapproved server names in mcp_server_name
- MCP tools accessing resources outside the user’s normal scope
- Bulk data retrieval through internal API connectors
- MCP tool invocations that follow anomalous bash activity in the same session
Prompt injection
The entry point for the other three. Direct injection is detectable when OTEL_LOG_USER_PROMPTS=1 is enabled: write detections against user_prompt for known patterns. Indirect injection is harder. Raw file contents are not included in telemetry, so a malicious README with embedded instructions won’t appear in user_prompt. What it produces is anomalous downstream behavior: unexpected tool_decision events, unusual tool_result commands, or MCP tool usage that doesn’t fit the session. That’s where the other three detections layer in.
Direct injection patterns to detect:
- “Ignore previous instructions” or “output your system prompt”
- Base64-encoded payloads in prompt text
- Attempts to override safety constraints
- References to internal systems the user shouldn’t be aware of
Why raw OTLP isn’t enough
Claude Code ships the data. The problem is the format. Raw OTLP comes out deeply nested, one large blob per session, with each metric and event buried inside arrays of key-value pairs. It is not something an analyst wants to write detections against. Here is a representative slice:
{"resourceMetrics":[{"resource":{"attributes":[
{"key":"department","value":{"stringValue":"engineering"}},
{"key":"service.name","value":{"stringValue":"claude-code"}},
{"key":"service.version","value":{"stringValue":"2.1.76"}}
]},"scopeMetrics":[{"metrics":[{
"name":"claude_code.session.count",
"sum":{"dataPoints":[{"asDouble":1,"attributes":[
{"key":"user.email","value":{"stringValue":"user@domain.com"}},
{"key":"session.id","value":{"stringValue":"..."}}
]}]}
}]}]}]}Before this data is useful, it needs to be split into one record per event and normalized to a flat schema. Part 2 covers how to do that and how to route it to your SIEM without building a custom pipeline.
Up next
Part 2 picks up here. It covers how to ingest raw Claude Code OTel data, normalize it into flat analyst-friendly records, route it to your SIEM or data lake, and run sample detections against it on day one.
Related content


.png)


.png)