9 min read

Claude Code already emits OpenTelemetry, and on one field it beats my recorder.

The question every security engineer asks within about ninety seconds of reading the Agentmetry homepage is a good one, and until this week I did not have a written answer to it.

Claude Code already exports OpenTelemetry. Why would I run your sensor instead of pointing our collector at it?

I went and read the specification properly before arguing with it. Two things came out of that. The first is that Claude Code's telemetry is better than I had assumed, and on one specific field it is strictly better than what my own recorder produces. The second is that the four gaps that remain are the ones I actually care about, which was reassuring but not something I get to just assert.

Here is the whole thing, including the part where I was wrong.

What it actually emits

Telemetry is off by default and turns on with one variable. On a managed machine an administrator can set it in managed settings, which locks the destination and a developer cannot turn it back off. That last part matters and I will come back to it.

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.internal:4317

You get eight metrics (sessions, tokens, cost, commits, pull requests, lines changed, active time, code-edit decisions) and twelve event types. The events are the interesting half for anyone doing detection:

claude_code.tool_result            tool_name, success, duration_ms, error_type,
                                   tool_input_size_bytes, tool_result_size_bytes
claude_code.tool_decision          tool_name, decision, source, tool_source
claude_code.mcp_server_connection  status, transport_type, server_scope, server_name
claude_code.permission_mode_changed  from_mode, to_mode, trigger
claude_code.api_request            model, cost_usd, tokens, duration_ms
claude_code.user_prompt            prompt_length, command_name, (prompt, gated)
claude_code.auth                   action, success, auth_method
claude_code.internal_error         error_name, error_code

Every event carries session.id, user.id, user.email, organization.id, and a prompt.id that links every event produced by a single user prompt. Bash command strings, tool parameters and MCP tool names arrive when you set OTEL_LOG_TOOL_DETAILS. There is a beta trace mode with spans for the interaction, the model call, the tool execution, and the permission wait.

That is a serious piece of instrumentation. It is not a token counter with a security label stuck on it, and anyone who tells you otherwise has not read it.

The field where it beats me

Agentmetry's coverage and limitations page has said the same uncomfortable thing since the first release:

Approval responses are inferred, not observed. No IDE reports the human's click, so a tool that runs after a prompt is treated as approved.

Every inferred event is marked inferred:tool_ran_after_ask and must never be cited as a human decision. On a typical month that is the large majority of the approval gates in my trail.

Claude Code does not have that problem. claude_code.tool_decision carries a source attribute with the actual provenance of the decision:

source = config            the permission was already granted by configuration
         hook              a hook decided it
         user_permanent    the human clicked "always allow"
         user_temporary    the human clicked "allow once"
         user_reject       the human said no
         user_abort        the human interrupted

That is the observed human decision, from inside the process that received the click. I reconstruct it from timing and label the reconstruction. Claude Code reports it. For the specific question "did a person approve this, or did configuration", the telemetry is right and my inference is a guess.

The same applies to a gap I have had an open issue about since June. tool_result_size_bytes tells you how much data a tool returned. My trail records that a file was read and not how large the read was, which means it cannot distinguish a config lookup from a database dump. Claude Code has been emitting that number the whole time.

The four things it cannot tell you

1. It covers one agent.

Claude Code emits this. Cursor does not. Nor does Codex, Antigravity, Qwen Code, Kimi, Qoder, or CodeBuddy. If your engineering organisation standardised on exactly one coding agent and will never add another, this is not a gap for you. Most of the security teams I have spoken to are looking at three at once, usually because somebody expensed a Cursor licence before anybody wrote a policy.

A per-agent telemetry format also means per-agent normalisation forever. Every new agent is a new schema, or it is invisible.

2. There is no artifact, so there is nothing to verify.

OTLP is a stream. It leaves the machine, crosses a network, lands in a collector, and is written by that collector into whatever it writes into. At no point does it produce a file that can be checked later by somebody who does not trust the pipeline.

That is fine for dashboards and it is not fine for evidence. When an incident responder asks whether the record was altered between the laptop and the console, the answer with OTLP is "we trust the collector". Agentmetry writes a local hash-chained JSONL file first, and forwards second:

agentmetry verify --trail data/agentmetry-trail.jsonl

  chained     26408 lines
  chain head  seq 26408  sha256 9c4f2ae1...
  legacy      0 unchained prefix lines
  OK

Each line commits to the one before it, and anchoring publishes a Merkle root somewhere the audited machine cannot rewrite. A 1.2 KB inclusion proof against an 8.3 MB trail is something a third party can check without trusting me, the collector, or the machine.

I should be precise about the ceiling here, because it is easy to oversell. The chain catches corruption, truncation, insertion and in-place edits. It does not stop somebody with write access to the data directory from rebuilding the whole file consistently. That is what the external anchor is for, and without one the chain is the weaker claim.

3. The commands are behind a flag your privacy review may never open.

By default, tool_result gives you tool_name and not what the tool was asked to do. You get "Bash succeeded in 240ms", which is a metric. To get the command string you set OTEL_LOG_TOOL_DETAILS, and the documentation is candid about why that is off: tool parameters can contain source code, file paths and secrets.

So the security value sits behind a switch whose whole purpose is to send more sensitive content to a central collector. In a regulated organisation that is a conversation with legal, and it is a conversation about a stream leaving developer machines and being retained centrally.

This is the argument for doing the sensitive part locally. Agentmetry hashes tool arguments by default, scrubs inline secrets before storage, and runs the DLP scan at the hook boundary rather than in a collector. The full-fidelity copy stays on the machine that produced it. What leaves is the finding.

4. Events are not findings.

This is the largest gap and the easiest to underestimate. OpenTelemetry hands you a correctly structured stream of things that happened. It does not tell you that a credential read at 09:14:22 and an outbound POST at 09:14:29 in the same session are one event and that event is critical.

Somebody has to write that correlation. You can do it in your SIEM, and you should cost that honestly: it is detection engineering against a schema you do not control, maintained by you, per agent. Monad has published a good series on doing exactly this, which is the clearest evidence I can offer that the gap is real rather than something I invented to sell a sensor. They build the pipeline layer on top of OTel. That is a legitimate architecture and if you are Claude-only it may be the right one.

Agentmetry ships the correlation as the product: fifteen sequence rules, MITRE ATT&CK and ATLAS tags, and a benchmark you can run in ten seconds to check the claims before you believe them.

The argument I am not going to make

There is a tempting line here that I want to disarm before somebody uses it, because it is wrong and it is wrong in a way that would damage my own credibility.

The line is: OpenTelemetry is the agent reporting on itself, so a compromised agent can simply not report. True. But my IDE hook tier has exactly the same property. A Claude Code hook is invoked by Claude Code. If the agent does not call it, I record nothing, and my own documentation says so in those words: hooks are cooperative, and absence of an event is not evidence that nothing happened.

I also said, in a draft of this post, that a developer can defeat OTel by unsetting an environment variable. On an unmanaged machine that is true. On a machine with managed settings it is not: the docs are explicit that a user cannot disable telemetry an administrator has enabled. I removed the claim rather than keep a line that a reader could check in one minute and find false.

The only tier where Agentmetry is genuinely not taking the agent's word is the mcp_audit_proxy, which sits in the stdio path and sees tools/call because the bytes go through it. That is one tier of three, it only covers MCP servers you wrap, and I am not going to describe the other two as something they are not.

What I am doing about it

The conclusion I reached is not the one I expected when I sat down. It is not that OpenTelemetry is a competitor to route around.

Claude Code OpenTelemetry should be a capture tier in Agentmetry, alongside IDE hooks and the MCP proxy.

Nothing below is built yet, so read it as a design rather than a changelog. The shape is an OTLP receiver that normalises events into the same canonical schema as every other source, which would leave everything downstream unchanged: the hash chain, the MITRE mapping, the sequence rules, the SIEM forwarders. Concretely that would mean:

  • -claude_code.tool_decision would replace my inferred approval for Claude Code sessions. The observed decision beats my reconstruction, so it should win, and the inferred: marker would leave that half of the trail.
  • -tool_result_size_bytes would close the how-much-data-moved gap that has been an open issue since June.
  • -Claude Code events would be chained into the same local trail as Cursor and Codex events, so one session could be correlated against another agent on the same machine in the same hour. prompt.id correlates within one agent. Nothing in OTel correlates across two.
  • -The existing rules should fire on it without modification, because normalisation happens before detection rather than inside it. That is the part I am least sure of and the first thing I would test.

This is more useful than arguing. A team that has already turned telemetry on has done the hard organisational half, which is the privacy review. Asking them to replace that with my sensor is a worse offer than reading what they already have and adding integrity, cross-agent correlation, and detection on top.

When you should not run Agentmetry

If all of the following are true, turn on CLAUDE_CODE_ENABLE_TELEMETRY, point it at your collector, read Monad's series, and do not install my sensor:

  • -Claude Code is the only coding agent in your organisation, and will stay that way.
  • -Your machines are managed, so an administrator can enforce the setting.
  • -Your privacy review is comfortable with OTEL_LOG_TOOL_DETAILS sending command strings to a central collector.
  • -Nobody is going to ask you to prove the record was not altered between the laptop and the console.
  • -You have detection engineering capacity to write the correlation yourself.

That is a real configuration and some organisations are in it. If you are not in it, the questions worth asking are which agents you actually have, whether the record needs to be verifiable by somebody who does not trust your pipeline, and who is writing the sequence rules.

And the honest boundary on all of this, which has not changed: none of it sees unmanaged ChatGPT in a browser, Copilot outside a hooked IDE, or an agent with hooks switched off. That is CASB and secure-web-gateway territory, Agentmetry does not claim it, and neither does OpenTelemetry.

To be clear about status, because this post has otherwise been about what is checkable: the ingest tier is a plan and not a release. It is filed as an issue rather than announced as a feature, and if it turns out to be the wrong idea I would rather find that out before building it than after. If you are already forwarding Claude Code telemetry and can tell me what the schema looks like in practice rather than in documentation, that issue is the fastest way to reach me. Being told I have this wrong is worth more to me right now than being told I have it right.