10 min read

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

Update, 10 October 2026. The ingest tier this post proposes shipped in 0.9.0 on 2 October as agentmetry otel. The argument below is unchanged. The last two sections now say what was built, what was not, and where the release differs from the plan.

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 what it does not do section 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: fourteen published 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 did 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.

When this was written the design below was a plan. It shipped in 0.9.0 as agentmetry otel: a receiver for OTLP over HTTP with JSON, bound to 127.0.0.1, that runs each event through the same enrichment as the hooks. A Claude Code call lands in the same canonical schema, with the same argument hash, traits and MITRE and ATLAS labels, and goes into the same chained trail. Everything downstream is unchanged: the chain, the sequence rules, the SIEM forwarders. Here is the plan, point by point, against what shipped:

  • Observed approvals: partly. When a person says yes, claude_code.tool_decision is now recorded as an observed approval, marked otel_decision: instead of inferred:. A config or hook decision is counted and not recorded as a human one. A person's no is counted and not yet forwarded: the rule that binds a denial to a later call matches on the argument hash, and this event carries no arguments, so it would match every later call of the same tool. Fixing that is a rule change, and rule changes wait for the detection freeze to end.
  • Data volume: not yet. tool_result_size_bytes is not mapped. The issue is still open.
  • One trail across agents: shipped. Claude Code events are chained into the same local trail as Cursor and Codex events, so one session can be read against another agent on the same machine in the same hour. prompt.id still correlates within one agent only.
  • Rules unchanged: held, after one surprise. A test replays a session in Claude Code's documented format, and credential-exfil fires on it as critical with no rule changed. Getting there showed the first prototype did not work on the real stream: Claude Code sends arguments and success as strings, and bool("false") is true in Python, so a failed call was recorded as a success. The test fixture now uses the documented format.

Two limits that were not in the plan. It records what happened after it happened, so it cannot deny or ask: blocking still needs the hooks. And it is one path or the other on a given install, not both, because each records every tool call and running both records each one twice. Prompts, responses and error text are never forwarded.

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 shipped in 0.9.0 and closed its issue. agentmetry otel --print-env prints what to start Claude Code with, and --print-mapping prints what is mapped and why. The direction that is still not built is the other one: exporting Agentmetry's own events as OTLP. If you forward Claude Code telemetry and the real stream differs from the documentation, an issue is still the fastest way to reach me. Being told I have this wrong is worth more to me than being told I have it right.