Skip to content

Monitoring usage with OpenTelemetry

Export Claude Code metrics, events and traces over OpenTelemetry: setup, admin locking, every metric and event, privacy gates and SIEM auditing.

Claude Code can export its own telemetry to your observability stack over OpenTelemetry (OTel). You get three signals: metrics as time series (sessions, tokens, cost, lines changed), events through the logs protocol (each prompt, API call, tool run and permission decision), and, in beta, traces that tie a prompt to every API call and tool it triggered. Everything goes to the collector you configure, not to Anthropic.

I set this up for every client team larger than about ten people. It answers "who is using it, what does it cost, and what did it run?" in tools they already have, and it works on every provider, including Bedrock and Vertex where the Anthropic dashboards cannot see.

Five-minute start

Run a local collector (or use your vendor's OTLP endpoint), then:

# Turn telemetry on
export CLAUDE_CODE_ENABLE_TELEMETRY=1

# Pick signals: otlp, prometheus (metrics only), console or none
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp

# Where to send it. There is no default protocol, so always set one
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.acme-health.internal:4318
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer otel_ingest_9f31"

# While testing, flush faster than the 60 s metrics default
export OTEL_METRIC_EXPORT_INTERVAL=10000

claude

How to tell it worked:

  • Metrics: look for claude_code.session.count, which is emitted when a session starts.
  • Logs only: send a prompt and look for a claude_code.user_prompt event.
  • Nothing arriving: start claude --debug-file /tmp/otel-debug.txt. Failures from your exporters are logged as [3P telemetry] errors. Lines tagged [Anthropic telemetry] are Anthropic's separate operational telemetry (see Data usage) and are not your problem.

Remember to put the export interval back to the default before rolling out.

Rolling it out to everyone

Put the variables in the env block of a managed settings file so every machine reports to the same place:

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://otel-gw.platform.svc:4317",
    "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer otel_ingest_9f31",
    "OTEL_RESOURCE_ATTRIBUTES": "department=engineering,cost_centre=ENG-204"
  }
}

Points that trip people up:

  • Desktop. Code tab sessions in Claude Desktop read managed settings from the sources that reach Desktop sessions (see Desktop). The OpenTelemetry form under Monitoring in the claude.ai admin console's Data and privacy settings configures Cowork only; neither the CLI nor the Code tab uses it.
  • Repositories cannot turn telemetry on. Exporter variables in .claude/settings.json and .claude/settings.local.json are ignored, so a cloned repo cannot enable telemetry, redirect it or capture content. A repo can still switch a signal off by setting its selector (such as OTEL_LOGS_EXPORTER) to none, unless managed settings, a --settings file or the launch environment sets that variable.
  • Child processes do not inherit OTEL_*. Bash commands, hooks, MCP servers and language servers started by Claude Code do not see the exporter configuration. If an instrumented app you run through Bash should export its own telemetry, set its variables in the command.

How managed settings pin the destination

When an OTEL_EXPORTER_OTLP_* variable is set in managed settings, Claude Code strips conflicting developer-set variables at startup (with a debug log warning) so telemetry cannot be diverted:

You set in managed settingsClaude Code removes from developer config
OTEL_EXPORTER_OTLP_ENDPOINTEvery per-signal endpoint
OTEL_EXPORTER_OTLP_PROTOCOLEvery per-signal protocol
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_CLIENT_KEY or OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATEThe per-signal versions of that variable, plus every endpoint variable, generic or per-signal, so the credentials cannot reach another collector
Anything that decides where logs or traces go under detailed beta tracing (a generic or logs/traces endpoint or credential, an otelHeadersHelper, a logs or traces selector of none, console or empty, or telemetry switched off)A developer-set BETA_TRACING_ENDPOINT (from v2.1.251). A metrics-only endpoint or credential does not trigger this

Exporter selectors (OTEL_METRICS_EXPORTER, OTEL_LOGS_EXPORTER, OTEL_TRACES_EXPORTER) are not stripped. They follow normal precedence, so a developer can still turn a signal off or send it to the console; set the selectors in managed settings too if you need them locked. Across admin sources, OTEL_LOGS_EXPORTER follows the telemetry unit while the other two merge per key (v2.1.223+).

Per-signal variables you set in managed settings yourself are kept, which is how you send one signal elsewhere (the SIEM example does this). A per-signal credential in managed settings removes the developer-set endpoint for that signal. None of this changes what is collected, only where it goes. Before v2.1.217 each variable followed precedence independently, so a user-level per-signal endpoint could redirect a signal.

From v2.1.251 the same pinning applies when Claude Desktop or a self-hosted environment runner launches Claude Code with an OTLP endpoint in the environment it provides: the launcher's variables win over developer-set ones.

Configuration reference

Core variables

Per-signal endpoint and protocol variables replace the generic one for that signal. Per-signal headers are merged with the generic headers.

VariablePurposeDefault or example
CLAUDE_CODE_ENABLE_TELEMETRYMaster switch, required1
OTEL_METRICS_EXPORTERComma-separated metrics exportersotlp, prometheus, console, none
OTEL_LOGS_EXPORTERComma-separated events exportersotlp, console, none
OTEL_EXPORTER_OTLP_PROTOCOLProtocol for all signals. No defaultgrpc, http/json, http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINTCollector for all signalshttp://localhost:4317
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OTEL_EXPORTER_OTLP_LOGS_PROTOCOLPer-signal protocolas above
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINTPer-signal endpointhttp://localhost:4318/v1/logs
OTEL_EXPORTER_OTLP_HEADERSHeaders for every signalAuthorization=Bearer ...
OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_HEADERSExtra per-signal headersmerged with the generic set
OTEL_METRIC_EXPORT_INTERVALMetrics flush interval (ms)60000
OTEL_LOGS_EXPORT_INTERVALEvents flush interval (ms)5000
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCEdelta or cumulativedelta
CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MSHow often the dynamic headers helper reruns1740000 (29 minutes)

Over http/protobuf and http/json, export requests carry a Content-Length header. (Versions v2.1.191 to v2.1.211 used chunked encoding, which Azure Monitor and similar endpoints rejected with 411 or 400.)

Content gates

By default no prompt text, response text, tool arguments or file contents leave the machine. Each of these opts in to more:

VariableWhat it adds
OTEL_LOG_USER_PROMPTS=1Prompt text on user_prompt events and the interaction span
OTEL_LOG_ASSISTANT_RESPONSES=1Response text on assistant_response events. Unset, it follows OTEL_LOG_USER_PROMPTS; set 0 to keep responses redacted while logging prompts
OTEL_LOG_TOOL_DETAILS=1Tool parameters and input (Bash commands, MCP server and tool names, skill names, workflow names, file paths), real command names on user_prompt, and real agent, skill, plugin and MCP names on the cost and token counters
OTEL_LOG_TOOL_CONTENT=1Tool output in the tool.output span event. Needs tracing
OTEL_LOG_MANAGED_SETTINGS=1Redacted managed settings and a SHA-256 digest on managed_settings_resolved events (v2.1.274+). Ignored in project or local settings
OTEL_LOG_RAW_API_BODIES=1 or =file:<dir>Full Messages API request and response JSON as events, inline (truncated) or as untruncated files with a pointer. Implies everything the other gates reveal
CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTHTruncation limit for content attributes, in UTF-16 code units (default 61440, 60 KB; v2.1.214+). A lower OpenTelemetry SDK attribute length limit wins

There is one built-in exception: for Claude Desktop's own in-process MCP servers, in sessions Desktop owns, mcp_server_name and mcp_tool_name appear on tool_decision and tool_result even without OTEL_LOG_TOOL_DETAILS (v2.1.214+), because the host defines those names.

mTLS

ProtocolClient certificateTrust the collector's CA with
http/protobuf, http/jsonCLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, optional CLAUDE_CODE_CLIENT_KEY_PASSPHRASE (see Network configuration)NODE_EXTRA_CA_CERTS
grpcOTEL_EXPORTER_OTLP_CLIENT_KEY and OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, or per-signal versions such as OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEYOTEL_EXPORTER_OTLP_CERTIFICATE

The same configuration covers metrics, logs and traces.

Controlling metric cardinality

Every attribute on a metric is a label, and labels cost storage. These switches decide which ones are attached:

VariableAddsDefault
OTEL_METRICS_INCLUDE_SESSION_IDsession.id, plus ccr.session.id on cloud sessionstrue
OTEL_METRICS_INCLUDE_VERSIONapp.versionfalse
OTEL_METRICS_INCLUDE_ACCOUNT_UUIDuser.account_uuid and user.account_idtrue
OTEL_METRICS_INCLUDE_ENTRYPOINTapp.entrypointfalse
OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTESYour OTEL_RESOURCE_ATTRIBUTES keys as datapoint labelstrue
OTEL_METRICS_INCLUDE_REPOSITORYvcs.* repository identity on metrics and events (v2.1.269+)false

Dynamic headers

If your collector needs short-lived tokens, point otelHeadersHelper in settings at a script that prints a JSON object of string headers. It only applies to the HTTP protocols; with grpc, only the static header variables are used.

{ "otelHeadersHelper": "/opt/acme/bin/otel-headers" }
#!/usr/bin/env bash
token=$(vault read -field=token secret/otel/claude-code)
printf '{"Authorization":"Bearer %s","X-Tenant":"acme-health"}\n' "$token"

The value can be an executable path (spaces allowed) or a command line with arguments; on Windows it always runs through the shell, so quote paths with spaces. It runs at startup and then every 29 minutes unless you change CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS.

If the helper fails or prints invalid output, nothing is exported until it recovers. You will see otelHeadersHelper failed; telemetry is not being exported once per interactive session, a note in /status, details in the debug log (--debug or /debug), and a message on stderr in -p sessions.

Team and cost-centre attributes

OTEL_RESOURCE_ATTRIBUTES adds your own keys to every metric datapoint and event, as well as the OTLP resource block, so you can group by them directly in most backends:

export OTEL_RESOURCE_ATTRIBUTES="department=engineering,team.id=payments,cost_centre=ENG-204"

Custom keys never override built-in attributes such as user.id or session.id (except the vcs.* keys, below). To keep them in the resource block only, set OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES=false.

Warning: The format is strict: comma-separated key=value, no spaces, and only US-ASCII without control characters, whitespace, double quotes, commas, semicolons or backslashes. Use underscores or camelCase (org.name=Acme_Health) or percent-encode (org.name=Acme%20Health). Quotes do not escape anything: org.name="Acme Health" gives a value containing the quote marks.

Traces (beta)

Tracing links each prompt to its API calls, tool runs and hooks as one trace. Enable it with CLAUDE_CODE_ENABLE_TELEMETRY=1 and CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 (ENABLE_ENHANCED_TELEMETRY_BETA also works), then choose a traces exporter. Traces reuse the common endpoint, protocol, header and mTLS settings.

VariablePurpose
OTEL_TRACES_EXPORTERotlp, console or none
OTEL_EXPORTER_OTLP_TRACES_PROTOCOLOverrides the generic protocol
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTOverrides the generic endpoint
OTEL_EXPORTER_OTLP_TRACES_HEADERSMerged with the generic headers
OTEL_TRACES_EXPORT_INTERVALSpan batch interval, default 5000 ms

Spans redact prompt text, tool details and tool content unless the matching content gates are on.

Context propagation.

  • Bash and PowerShell subprocesses receive a TRACEPARENT variable for the active tool execution span, so scripts can parent their own spans under Claude's trace.
  • Direct to the Anthropic API, each model request carries a W3C traceparent from the claude_code.llm_request span, and the API's traceresponse is recorded as a span link. HTTP MCP requests carry traceparent too. Nothing is sent to third-party providers.
  • Behind a custom ANTHROPIC_BASE_URL, traceparent (and the subprocess TRACEPARENT) is off by default because some proxies reject unknown headers. Set CLAUDE_CODE_PROPAGATE_TRACEPARENT=1 to turn it on.
  • In Agent SDK and -p sessions, an inbound TRACEPARENT/TRACESTATE in Claude Code's own environment makes its interaction spans children of your trace. Interactive sessions ignore inbound values so they do not inherit stray CI context.
  • With an inbound TRACEPARENT in those sessions, events also carry trace_id and span_id, even without a traces exporter. A record emitted during an interaction carries the interaction span's IDs; one emitted with no active interaction carries the inbound IDs.

Span tree.

claude_code.interaction                 one per prompt
├── claude_code.llm_request
├── claude_code.hook                    detailed beta tracing only
└── claude_code.tool
    ├── claude_code.tool.blocked_on_user
    ├── claude_code.tool.execution
    └── subagent llm_request / tool spans (when the Agent tool spawns one)

If a PreToolUse hook defers a tool call, the deferred call's spans rejoin the original turn's trace when the session resumes. llm_request, tool.execution and hook spans set status ERROR on failure; others end UNSET. Every span carries the standard attributes plus span.type.

Key span attributes.

SpanNotable attributes
claude_code.interactionuser_prompt (gated), user_prompt_length, interaction.sequence, parent.source (env or none, v2.1.268+), interaction.duration_ms
claude_code.llm_requestmodel, gen_ai.system (anthropic), gen_ai.request.model, query_source (detailed tracing only), query_source_safe (bounded, always present, v2.1.268+), agent_id, parent_agent_id, workflow.run_id, workflow.name (gated), speed, effort (v2.1.274+), llm_request.context, duration_ms, ttft_ms, first_content_ms, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, request_id, gen_ai.response.id, client_request_id, attempt, success, status_code, error, error_class, response.has_tool_call, stop_reason, gen_ai.response.finish_reasons. Each retry adds a gen_ai.request.attempt span event
claude_code.tooltool_name, tool_name_safe (no user-chosen names; most MCP tools become mcp_other), bash_command_class and bash_argv0 (from a fixed list), duration_ms, result_tokens, agent_id, parent_agent_id, workflow.*, tool_use_id, gen_ai.tool.call.id; with tool details on, file_path, full_command, skill_name, subagent_type
claude_code.tool.blocked_on_userduration_ms, decision, source
claude_code.tool.executionduration_ms, tool_use_id, gen_ai.tool.call.id, success, error (category, or full message with tool details), error_class
claude_code.hookhook_event, hook_name, num_hooks, hook_definitions (gated), duration_ms, num_success, num_blocking, num_non_blocking_error, num_cancelled

Note that input_tokens excludes cache reads and writes, which are reported separately.

The tool.output span event. With OTEL_LOG_TOOL_CONTENT=1, successful Read and Bash calls (and, from v2.1.283, MCP tools, WebFetch and WebSearch) add a tool.output event to the tool span. Edit and Write need OTEL_LOG_TOOL_DETAILS=1 as well. Attributes are content (Read output, or what Write wrote), output (Bash's combined output, or the text an MCP tool or web tool returned, with images shown as placeholders), diff (Edit), plus file_path and bash_command with tool details. Nothing is recorded for errors, non-text Reads (images, PDFs, unchanged re-reads), other tools, or web calls moved to the background. Truncated attributes come with <attribute>_truncated and <attribute>_original_length.

Detailed beta tracing. The claude_code.hook span and a set of extra content attributes only appear with ENABLE_BETA_TRACING_DETAILED=1 and BETA_TRACING_ENDPOINT, which also redirects logs and traces to that endpoint. Both are ignored in project and local settings, and interactive CLI sessions need your organisation allowlisted (SDK and -p sessions do not). The extra attributes, which are not part of the stable schema:

AttributeSpanGate
new_contextinteraction (the prompt), llm_request (new messages and tool results)OTEL_LOG_USER_PROMPTS
new_contexttool (the result)OTEL_LOG_TOOL_CONTENT
system_reminders, system_prompt_preview (first 500 chars), user_system_prompt (once per session), response.model_outputllm_requestOTEL_LOG_USER_PROMPTS
tool_inputtoolOTEL_LOG_TOOL_DETAILS

With prompts logged, a claude_code.system_prompt event carries the complete system prompt the first time each distinct one is sent and again after compaction.

Example set-ups

Console output while debugging:

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=console
export OTEL_METRIC_EXPORT_INTERVAL=2000

Prometheus scrape at http://localhost:9464/metrics:

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=prometheus

With prometheus as the only metrics exporter, the USD, tokens and s units are dropped so the scrape stays valid Prometheus text (combinations like otlp,prometheus keep them). On a self-hosted environment runner, the session binds port 9464 only at capacity one; above that, the runner re-exposes session metrics on its own /metrics.

Metrics and events to different backends:

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics.acme-health.internal:4318
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.acme-health.internal:4317

For metrics only, leave OTEL_LOGS_EXPORTER unset; for events only, leave OTEL_METRICS_EXPORTER unset. Several exporters can be combined, for example OTEL_METRICS_EXPORTER=console,otlp.

Cloud sessions and Claude Tag

Cloud sessions, including Claude Tag channel sessions, run in cloud environments, so laptop settings do not reach them. Configure their telemetry in one of two places:

  • Server-managed settings env block. This reaches users' machines and cloud sessions, but not Claude Tag channel sessions.
  • The cloud environment's variables. Applies only to sessions in that environment, and is the only route to Claude Tag sessions.

Anyone using an environment can read its variables, so do not put a collector token there, and a network secret will not help because Claude Code's own telemetry export never receives one. If the collector needs a credential, configure the whole export in server-managed settings, where setting a credential also strips endpoints set elsewhere.

Other constraints:

  • Exports go through the session's network, so the collector's domain must be reachable at the environment's access level; add it to the environment allowlist if not. No server-managed setting can add domains.
  • Claude Tag channels run in organisation-level environments, so change the shared environment set as default or pinned to the channel.
  • Cowork does not receive server-managed settings and is configured separately.

Attribution. Cloud sessions carry session.id, ccr.session.id (the session's CLAUDE_CODE_REMOTE_SESSION_ID) and organization.id by default. Set OTEL_METRICS_INCLUDE_ENTRYPOINT=true and Claude Tag sessions show app.entrypoint of claude-in-slack. Set OTEL_RESOURCE_ATTRIBUTES alongside the other variables, not in the environment's setup script, whose exports end before Claude Code starts. In Claude Tag channel sessions Claude acts as the organisation's shared identity, so user.* does not tell you who tagged it.

Attributes on everything

Standard attributes

AttributeMeaningControlled by
session.idSession identifierOTEL_METRICS_INCLUDE_SESSION_ID (on)
ccr.session.idCloud session identifiersame
app.versionClaude Code versionOTEL_METRICS_INCLUDE_VERSION (off)
app.entrypointHow it was launched: cli, sdk-cli, sdk-ts, sdk-py, claude-vscode, claude-in-slackOTEL_METRICS_INCLUDE_ENTRYPOINT (off)
organization.idOrganisation UUID when signed inalways, when available
user.account_uuid, user.account_idAccount UUID and tagged ID (such as user_01...) when signed inOTEL_METRICS_INCLUDE_ACCOUNT_UUID (on)
user.idRandom anonymous install ID stored in ~/.claude.json; not derived from your accountalways
user.emailSign-in email, or the cloud session's credentialsalways, when available
terminal.typeSuch as iTerm.app, vscode, cursor, tmuxwhen detected
Your OTEL_RESOURCE_ATTRIBUTES keysTeam, cost centre and so onOTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES (on)
vcs.repository.url.full, vcs.owner.name, vcs.repository.name, vcs.provider.nameRepository identityOTEL_METRICS_INCLUDE_REPOSITORY (off)

In sessions signed in to a Claude apps gateway with /login, user.id is the IdP subject, user.email the signed-in email, user.groups the comma-separated IdP groups, and every export carries identity.source: gateway-oidc. This identity is applied last, so user.* and identity.* keys in OTEL_RESOURCE_ATTRIBUTES are ignored. Claude apps gateway configuration covers Desktop and Cowork sessions through a gateway.

Events (never metrics, to avoid unbounded cardinality) also carry prompt.id, workspace.host_paths (Desktop workspace folders), and, for agents inside a workflow run, workflow.run_id (prefixed wf_) and workflow.name (user-authored names become custom without tool details; v2.1.202+).

Repository attributes

With OTEL_METRICS_INCLUDE_REPOSITORY=true (v2.1.269+), Claude Code derives repository identity once per session from the origin remote. HTTPS and SSH remotes on GitHub, GitLab and Bitbucket Cloud give identical results.

AttributeExample
vcs.repository.url.fullhttps://github.com/acme-health/claims-api (no .git)
vcs.owner.nameacme-health; omitted for single-segment paths
vcs.repository.nameclaims-api
vcs.provider.namegithub, gitlab, bitbucket or gitea when recognised

Values are lowercased and never include credentials, query strings or fragments. They are omitted with no origin, a non-URL remote, or when the only repository is your home directory. A vcs.* key in OTEL_RESOURCE_ATTRIBUTES overrides the derived value; declaring vcs.repository.url.full stops Claude Code reading the remote at all, which is the fix when a self-hosted server gives HTTPS and SSH clones different paths. For cloud sessions, set the variable on the cloud environment and allow your collector's domain. Anthropic's own telemetry drops all vcs.* keys.

Metrics

MetricWhat it countsUnitExtra attributes
claude_code.session.countSessions startednonestart_type: fresh, resume, continue, or agents_view (the claude agents dashboard process, not a conversation)
claude_code.lines_of_code.countLines added or removednonetype (added, removed), model
claude_code.pull_request.countPRs or MRs created via shell or MCPnonenone
claude_code.commit.countGit commits creatednonenone
claude_code.cost.usageEstimated cost per API requestUSDsee below
claude_code.token.usageTokens per API requesttokenstype (input, output, cacheRead, cacheCreation) plus the cost attributes
claude_code.code_edit_tool.decisionAccept or reject of Edit, Write, NotebookEditnonetool_name, decision, source, language (unknown for unrecognised extensions)
claude_code.active_time.totalActive (non-idle) timestype: user (keyboard) or cli (tools and responses)

Cost and token attributes: model; query_source (main, subagent, auxiliary); speed (fast only when fast mode was used); effort (low, medium, high, xhigh, max, absent when none was sent); and attribution fields agent.name, skill.name, plugin.name, marketplace.name, mcp_server.name, mcp_tool.name.

Attribution redaction without OTEL_LOG_TOOL_DETAILS:

  • agent.name: built-in and official-marketplace agents verbatim; other user-defined agents become custom.
  • skill.name: built-in, bundled, user-defined and official-marketplace skills verbatim; third-party plugin skills become third-party.
  • plugin.name: official-marketplace verbatim; third-party becomes third-party.
  • marketplace.name: only ever emitted for official-marketplace plugins.
  • mcp_server.name and mcp_tool.name: built-in, claude.ai-proxied and official-registry servers verbatim; user-configured become custom. Present only on requests that consumed an MCP tool result (before v2.1.222 they stuck to every later request, so expect a step down in dashboards after upgrading).

Before v2.1.273 these stayed redacted even with tool details on. The token input type excludes cache reads and writes.

Events

Events need OTEL_LOGS_EXPORTER set. Every event carries the standard attributes plus event.name, event.timestamp (ISO 8601) and event.sequence.

Correlation

AttributeUse
prompt.idUUID shared by everything one user prompt caused. Filter by it to see the prompt, its API calls and its tool results together
event.sequence0-based counter per Claude Code process, not per session. It survives /clear (which issues a new session.id), and a resumed session takes the resuming process's numbers, so sort by event.timestamp first and use the sequence only to break ties
message.uuidTranscript message UUID (~/.claude/projects/*/*.jsonl) on assistant_response, api_response_body and most user_prompt events (v2.1.214+; v2.1.274+ on response bodies)
request_idServer request ID from the request-id header, or x-amzn-requestid on Bedrock (v2.1.282+). On the API events and responses; matches the span attribute
client_request_idClient-generated x-client-request-id, on api_request and api_error for first-party connections. Useful for timeouts that never got a server ID (v2.1.214+)

Joining events to transcripts (message.uuid, request_id as requestId, tool_use_id) works, but the transcript format is internal and changes between versions, so treat any such join as version-specific.

Conversation and API events

EventWhenAttributes beyond the common set
claude_code.user_promptA prompt is submitted, including turns Claude Code starts itselfprompt_length; prompt and prompt_text (same value, both redacted unless prompts are logged; prompt_text exists for backends that nest dotted names, v2.1.287+); message.uuid; command_name (built-ins as typed; custom, plugin and MCP become custom or mcp without tool details); command_source (builtin, custom, mcp)
claude_code.assistant_responseA response contains text (thinking and tool-use blocks excluded)response_length; response (redacted by default); model; request_id; message.uuid; query_source
claude_code.api_requestEach API requestmodel, cost_usd, cost_usd_micros (integer), duration_ms, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, request_id, client_request_id, speed, query_source, effort, attribution fields
claude_code.api_errorA request fails (after retries)model, error, status_code (absent for non-HTTP), duration_ms, attempt, request_id, client_request_id, speed, query_source, effort, attribution fields
claude_code.api_refusalA response ends with stop_reason: "refusal" (not an HTTP error, so api_error does not fire)model, request_id, query_source, speed, attempt, effort, server_fallback_hop (true when the server already retried on another model), has_category, has_explanation, category (cyber, bio, frontier_llm, reasoning_extraction; only with tool details), attribution fields
claude_code.api_retries_exhaustedA request fails after more than one attempt, alongside the final api_errormodel, error, status_code, total_attempts, total_retry_duration_ms, speed
claude_code.api_request_bodyEach attempt, with raw bodies onbody (inline) or body_ref (file mode), body_length, body_truncated, model, query_source, request_body_id (v2.1.274+). Thinking in earlier turns is redacted
claude_code.api_response_bodyEach successful response, with raw bodies onbody or body_ref, body_length, body_truncated, model, query_source, request_id, request_body_id, message.id, message.uuid. Thinking is redacted
claude_code.compactionCompaction finishestrigger (auto, manual), success, duration_ms, pre_tokens, post_tokens, error, precompute_reuse on manual runs (hit, miss_custom_instructions, miss_hook, miss_not_ready)
claude_code.subagent_completedA subagent returnsagent_type (custom names become custom without tool details), agent.source, is_built_in, is_async, total_tokens (final request only, not a run total), total_tool_uses, duration_ms, model, final_model, model_swapped, plugin_id_hash, plugin.name
claude_code.at_mentionAn @ mention resolves (some early exits log nothing)mention_type (file, directory, agent, mcp_resource, peer), success
claude_code.feedback_surveyA session survey is shown or answeredevent_type, appearance_id, survey_type, response, enabled_via_override (boolean)

In raw-body file mode, each successful response also appends a line to <dir>/index.jsonl with timestamp, session_id, query_source, model, request_id, message_id, message_uuid, request_file and response_file (v2.1.274+). Request files are <uuid>.request.json; response files are <request_id>.response.json.

For subagent cost, use the token and cost counters filtered to query_source of subagent rather than subagent_completed, whose total_tokens covers only the final request. That category also includes agent-based hooks, which emit no subagent event.

Tool and permission events

claude_code.tool_result: a tool finished (rejected calls do not produce one). Attributes: tool_name, tool_use_id (matches hook payloads), success, duration_ms, error_type, decision_type (always accept), decision_source (config, hook, user_permanent, user_temporary), tool_input_size_bytes, tool_result_size_bytes, mcp_server_scope. With tool details: error (full message), tool_parameters, tool_input (values over 512 characters truncated, about 4K characters in total), and for a successful git commit from Bash or PowerShell, vcs.ref.head.revision, vcs.ref.head.name and vcs.ref.head.type (v2.1.269+).

tool_parameters contents by tool:

  • Bash: bash_command, full_command, timeout, description, dangerouslyDisableSandbox, plus git_commit_id and git_branch after a successful commit. The desktop workspace Bash tool reports only the first three.
  • MCP tools: mcp_server_name, mcp_tool_name.
  • Skill: skill_name.
  • Agent (or legacy Task): subagent_type.

claude_code.tool_decision: a permission decision. Attributes: tool_name (user-configured MCP tools always show as mcp_tool), tool_use_id, decision (accept, reject), tool_source (builtin, mcp, sdk_host_builtin_mcp; v2.1.214+), source, and tool_parameters with tool details (same shape as above, minus commit fields; useful for seeing exactly what was rejected).

What source means:

ValueMeaning
configDecided without prompting: settings rules, managed policy, --allowedTools/--disallowedTools, the permission mode, a session grant from an earlier prompt, or an inherently safe tool. Also used when the prompt mechanism itself fails (an invalid canUseTool or --permission-prompt-tool result, or the input stream closing)
hookA PreToolUse or PermissionRequest hook decided
user_permanentThe user chose "Yes, and don't ask again". In the interactive CLI only that choice reports this; later matches report config. In SDK and -p sessions later matches report it too
user_temporaryOne-time "Yes", or a rest-of-session grant on a file prompt. Same interactive/SDK split
user_abortPrompt dismissed without an answer, including an interrupt while an SDK permission request is pending
user_rejectThe user chose "No". Interactive deny-rule matches report config; in SDK and -p sessions they report user_reject

claude_code.permission_mode_changed: from_mode, to_mode (default, plan, acceptEdits, auto, bypassPermissions), trigger (shift_tab, exit_plan_mode, auto_gate_denied, auto_opt_in; absent for SDK or bridge changes).

Session, auth and extension events

EventWhenAttributes
claude_code.auth/login or /logout completesaction, success, auth_method, error_category (never the raw message), status_code
claude_code.mcp_server_connectionAn MCP server connects, disconnects or failsstatus, transport_type, server_scope, duration_ms, error_code, is_plugin, plugin_id_hash, plugin.name (third-party redacted); with tool details, server_name and error
claude_code.internal_errorAn unexpected internal error is caught (not on Bedrock, Agent Platform or Foundry, or with DISABLE_ERROR_REPORTING)error_name, error_code. Never the message or stack
claude_code.plugin_installedclaude plugin install or /plugin finishesmarketplace.is_official, install.trigger (cli, ui); for third-party marketplaces plugin.name, plugin.version and marketplace.name need tool details
claude_code.plugin_loadedOnce per enabled plugin at startupplugin.name, marketplace.name, plugin.version, plugin.scope (official, community, org, user-local, default-bundle), enabled_via (default-enable, org-policy, admin-install, seed-mount, user-install), plugin_id_hash, has_hooks, has_mcp, host_owned_mcp, skill_path_count, command_path_count, agent_path_count, safe_mode
claude_code.skill_activatedA skill runs, by Claude or as a / commandskill.name (custom and third-party become custom_skill), invocation_trigger (user-slash, claude-proactive, nested-skill), skill.source, skill.kind (workflow), plugin.name, marketplace.name
claude_code.hook_registeredOnce per configured hook at startuphook_event, hook_type (command, prompt, mcp_tool, http, agent), hook_source (userSettings, projectSettings, localSettings, flagSettings, policySettings, pluginHook), safe_mode, hook_matcher (gated), plugin.name, plugin_id_hash
claude_code.hook_execution_startHooks begin for an eventhook_event, hook_name (such as PreToolUse:Write), num_hooks, managed_only, hook_source (policySettings or merged), safe_mode, hook_definitions (needs detailed tracing and tool details)
claude_code.hook_execution_completeAll hooks for an event finishThe start attributes plus num_success, num_blocking, num_non_blocking_error, num_cancelled, total_duration_ms, and from v2.1.280 stdout_chars, additional_context_chars, system_message_chars, initial_user_message_chars, num_outputs_persisted
claude_code.hook_plugin_metricsAn official-marketplace plugin hook reports metricsplugin_id (name@marketplace), hook_event, up to 20 plugin keys matching ^[a-z][a-z0-9_]{0,39}$ with boolean or number values

plugin_id_hash is a deterministic hash of plugin name and marketplace (for plugins synced from claude.ai, the marketplace name claude.ai reports, or synced), so you can count distinct third-party plugins without recording names. It and plugin.name go only to your exporter. safe_mode reflects --safe-mode (v2.1.169+), in which plugins report inventory but do not load.

Retention sweep event

claude_code.retention_sweep (v2.1.227+) is emitted once per run of the clean-up that deletes data older than cleanupPeriodDays. The sweep runs in the background at most once per session; if another session on the machine swept in the last 24 hours, this one waits at least 10 minutes, so short sessions emit nothing. claude -p --bare never sweeps.

AttributeMeaning
resultcomplete or skipped
period_daysRetention in force (30 if unset)
used_defaulttrue when no readable source sets cleanupPeriodDays
skip_reasonuser_source_disabled (user settings excluded and nothing else sets it), settings_unknowable (a settings file could not be read), settings_invalid_key_set (validation errors and the key is set)
transcripts_deletedTop-level transcript files removed
transcripts_exempted_desktopOld transcripts kept under the Desktop and Cowork rule (v2.1.248+)
session_files_deletedTranscripts plus per-session companion files
artifacts_deletedTotal items removed; treat as a floor
files_retained_freshFiles still in retention; also a floor
files_past_cutoffOld files the sweep could not delete, plus stale skills/synced/ or plugins/synced/ folders
error_countErrors while listing or deleting

A cleanupPeriodDays in managed settings lets the sweep run even when a lower-priority settings file is broken; a broken managed-settings.json still pauses it unless another managed source supplies the value. Counters are only present on complete results.

Managed settings resolved event

claude_code.managed_settings_resolved (v2.1.274+) records which managed policy a session ended up with: at startup, whenever the policy or its helper changes, and when a policy stops the session. It is how I find laptops on the wrong policy source or with a broken policy helper.

AttributeMeaning
managed_settings.triggerstartup, change (only when something differs from the last event), or refused
error.typeOn refused only: helper_failed, policy_invalid, provider_not_allowed (v2.1.285+), consent_rejected, force_refresh_failed, gateway_rejected (a Claude apps gateway returned 403), version_below_minimum (outside requiredMinimumVersion/requiredMaximumVersion), _OTHER
managed_settings.sourcesArray of sources delivering policy keys, highest priority first: remote, plist, hklm, file, parent (an embedding host), hkcu
managed_settings.source_behaviorfirst-wins or merge
managed_settings.helper.stateok; a failure such as bad_path, not_a_file, exit_nonzero, timed_out, oversize, parse_failed, envelope_invalid, schema_rejected; or none
managed_settings.helper.appliedoutput or none
managed_settings.helper.entry, managed_settings.helper.pathpolicyHelper and its configured path, when one is selected
managed_settings.resolved_sha256With OTEL_LOG_MANAGED_SETTINGS=1: digest of the unredacted policy (keys sorted, no whitespace). Same digest, same policy
managed_settings.settings, managed_settings.settings_truncatedWith the opt-in: the policy's shape as JSON with values redacted, cut at 8 KB

Redaction keeps names, booleans, numbers and fixed-choice strings (such as permissions.defaultMode), and replaces every other string, URL, command and env value with "[REDACTED]". Permission rules keep their tool name, as in Bash([REDACTED]), for built-in tools and mcp__ references. OTEL_LOG_MANAGED_SETTINGS cannot be enabled from project or local settings, and server-managed settings can set it without the approval dialog. In an untrusted folder, the refusal event is not exported.

Turning the data into answers

Usage and cost

QuestionUse
Who uses it and how oftenclaude_code.session.count, claude_code.active_time.total
Where tokens goclaude_code.token.usage by type, user, team, model, skill.name, plugin.name, agent.name
Outputclaude_code.lines_of_code.count by model, claude_code.commit.count, claude_code.pull_request.count
Spend by team or featureclaude_code.cost.usage with your resource attributes and the attribution fields

Cost metrics are estimates; your provider's billing is authoritative. Each streaming response is counted exactly once, even when a gateway streams usage across several frames (before v2.1.214, multi-frame usage inflated cost and token counts).

model is available on the token, cost and lines-of-code metrics. Commits have no model, so approximate by joining to the token or cost metric on session.id, filtering that side to query_source of main so subagent and auxiliary calls do not skew the attribution.

Good alerts to start with: cost spikes, unusual token consumption, and unusually high session counts from a single user.

Spotting retry exhaustion

Claude Code retries internally and emits one api_error when it gives up, with attempt holding the total. CLAUDE_CODE_MAX_RETRIES defaults to 10 and caps at 15; CLAUDE_CODE_RETRY_WATCHDOG (v2.1.199+) raises the default and removes the cap. So an exhausted transient error shows attempt of 11 by default and at most 16 without the watchdog. Lower values mean a non-retryable error (such as a 400) or a cause with its own smaller budget, such as failing to load AWS or Google Cloud credentials (retried at most twice). To tell recovery from a stall, check for a later api_request in the same session.id.

Tool patterns and performance

From tool_result events: most-used tools, success rates, average duration, and error types per tool. From api_request and tool events: latency hot spots.

Mapping to GenAI semantic conventions

Claude Code does not set gen_ai.usage.*. Its input_tokens (span, event, and the metric's input type) excludes cache tokens, whereas the conventions' gen_ai.usage.input_tokens includes them. To compute the convention value, add input, cache read and cache creation. Cache reads map to gen_ai.usage.cache_read.input_tokens and cache creation to gen_ai.usage.cache_write.input_tokens (older conventions call it gen_ai.usage.cache_creation.input_tokens).

Security auditing

OTel events are Claude Code's audit trail. Send them via the logs exporter to any SIEM with an OTLP receiver, or through an OpenTelemetry Collector.

Who did it

Events carry user.email, user.account_uuid, user.account_id and organization.id when signed in with a Claude account (or in a cloud session with its own credentials), plus user.id and session.id. Claude Code does not act under a service account: in a developer's session, MCP calls, Bash commands and edits are attributed to that developer, or to their IdP identity in a Claude apps gateway session. Claude Tag channel sessions use the organisation's shared identity.

With a direct API key, Bedrock, Agent Platform or Foundry there is no Claude account, so only user.id and session.id are populated. Add identity yourself, per user, through managed settings or a launch wrapper:

export OTEL_RESOURCE_ATTRIBUTES="enduser.id=priya.shah@acme-health.co.uk,enduser.directory_id=EMP-10442"

MCP activity

Turn on the logs exporter and OTEL_LOG_TOOL_DETAILS=1 for full detail:

EventWhat you get for MCP
mcp_server_connectionConnects, disconnects and failures with server_name, transport_type, server_scope and error detail
tool_resultEach call, with server and tool names in tool_parameters and arguments in tool_input
tool_decisionAllowed or denied, and whether config, a hook or the user decided

Without the flag, tool_name is the literal mcp_tool for user-configured servers, arguments are omitted, and connection events drop server_name and the error message but keep is_plugin, plugin_id_hash and a redacted plugin.name.

Detection cheat sheet

What you want to detectEventAttributes
A tool call allowed or denied, and by whattool_decisiondecision, source, tool_name, tool_parameters
Permission mode escalationpermission_mode_changedfrom_mode, to_mode, trigger
A policy hook blocked somethinghook_execution_completehook_event, num_blocking
Sign-in and auth failuresauthaction, success, error_category
MCP connects and failuresmcp_server_connectionstatus, server_name, is_plugin, error_code
Plugin installs and their sourceplugin_installedplugin.name, marketplace.name, marketplace.is_official
Commands run and files touchedtool_result or tool_decision, with tool detailstool_parameters, tool_input
Policy source, helper health, refusalsmanaged_settings_resolvedtrigger, sources, source_behavior, helper.state, error.type

Claude Code emits raw events only. Baselining, anomaly detection, cross-session correlation and alerting are your SIEM's job.

Egress paths, controls and evidence

Path off the machineManaged controlsEvents
Bash and PowerShellsandbox.enabled, sandbox.failIfUnavailable, sandbox.allowUnsandboxedCommands, sandbox.network.allowManagedDomainsOnly, sandbox.network.allowedDomainstool_decision, tool_result
MCP serversallowedMcpServers, allowManagedMcpServersOnly, deniedMcpServers, managed MCPmcp_server_connection, tool_decision, tool_result
HooksallowManagedHooksOnly, allowedHttpHookUrlshook_registered, hook_execution_start, hook_execution_complete
PluginsstrictKnownMarketplaces, disableSideloadFlags, syncClaudeAiPlugins, syncClaudeAiSkillsplugin_installed, plugin_loaded
WebFetchpermissions.deny, allowManagedPermissionRulesOnlytool_decision, tool_result
Uploads to claude.ai, such as Artifactpermissions.deny, enableArtifacttool_decision, tool_result
Remote ControldisableRemoteControlnone dedicated
Local transcript retentioncleanupPeriodDaysretention_sweep

The settings reference documents each key. Caveats: allowedHttpHookUrls merges across settings files, so developers can add to an empty managed list (allowManagedHooksOnly decides what runs); hook events fire once per hook event, not per hook, and an HTTP hook's URL appears only in hook_definitions, which needs detailed tracing. For what Claude Code itself sends to Anthropic, see Data usage.

Warning: OTEL_LOG_TOOL_DETAILS=1 puts commands, server names and tool input in your telemetry, which can be as sensitive as the session itself. Only enable it where your collector is approved to hold that content.

Verifying retention

Set cleanupPeriodDays in managed settings, collect retention_sweep, and cast period_days and the counters from strings to numbers before comparing:

What you seeMeaning
result = skippedSweep paused; read skip_reason
used_default = true, or period_days not your valueYour managed value is not applied
error_count > 0Errors while deleting; old data may remain
files_past_cutoff > 0Old files survived (or stale synced folders were found); read with error_count
No eventNot a failure by itself: nobody launched Claude Code, a session is still open, or it ended before the sweep finished

The sweep does not cover every path; The .claude directory lists what remains and how to clear it.

Shipping events to a SIEM

Events only, full tool detail, straight to an OTLP receiver:

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_LOG_TOOL_DETAILS": "1",
    "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": "http/protobuf",
    "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "https://siem-ingest.acme-health.co.uk:4318/v1/logs",
    "OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer siem_ingest_44a0"
  }
}

Send a prompt and look for claude_code.user_prompt in the SIEM; if nothing appears, check a --debug-file log for [3P telemetry] errors.

Choosing backends

  • Metrics: time-series databases for rates and aggregates; columnar stores for complex queries and unique-user counts; full observability platforms for visualisation and alerting. If you need DAU, WAU or MAU, pick something good at distinct counts.
  • Events: log aggregation for search; columnar stores for structured analysis; observability platforms to correlate with metrics.
  • Traces: a distributed tracing system for waterfalls and latency, or a platform that links traces to metrics and logs.

Resource attributes

Every export carries:

  • service.name: claude-code for terminal sessions, claude-code-desktop for Code tab sessions. If your pipelines filter on claude-code, add the Desktop name too.
  • service.version: Claude Code version (or the Desktop version for Code tab sessions).
  • os.type, os.version, host.arch, and wsl.version under WSL.
  • Meter name com.anthropic.claude_code.

Anthropic publishes a Claude Code monitoring guide on GitHub (anthropics/claude-code-monitoring-guide) with Docker Compose, Prometheus and OpenTelemetry set-ups and report templates for measuring ROI. AWS publishes Bedrock-specific monitoring guidance in its "guidance for Claude Code with Amazon Bedrock" sample repository.

Privacy summary

  • Export is opt-in and goes only to the endpoint you configure. Anthropic's own operational telemetry is separate; see Data usage.
  • Metrics and events never contain raw file contents or code snippets. Trace spans are a separate path governed by OTEL_LOG_TOOL_CONTENT.
  • With OAuth sign-in, user.email is included, sent only to your endpoint. Filter it in your backend if that matters to you.
  • Prompts are recorded as length only unless OTEL_LOG_USER_PROMPTS=1. Then the prompt appears in both prompt and prompt_text on user_prompt events, so a collector rule that drops it must name both:
processors:
  attributes/strip-prompts:
    actions:
      - key: prompt
        action: delete
      - key: prompt_text
        action: delete

With tracing, the interaction span's user_prompt also carries it, and detailed tracing adds messages, tool results, system reminders, system prompt text and model output.

  • Responses are length only unless OTEL_LOG_ASSISTANT_RESPONSES=1 (or it falls back to the prompts flag). Under detailed tracing, response.model_output follows the prompts flag regardless.
  • Tool arguments need OTEL_LOG_TOOL_DETAILS=1. Then tool_parameters (untruncated full_command included) and tool_input appear, plus real command and attribution names. Arguments can contain secrets, so redact in the backend.
  • Tool output in spans needs OTEL_LOG_TOOL_CONTENT=1, truncated per attribute at the content limit.
  • Raw API bodies need OTEL_LOG_RAW_API_BODIES set in your shell, user settings or managed settings (never project or local). They contain the full conversation; extended thinking is always redacted. Use file:<dir> for untruncated files and ship that directory with a log collector rather than the telemetry stream.