CLAUDE_CODE_PROPAGATE_TRACEPARENT=1 does not inject traceparent header into outbound LLM requests
Problem
When CLAUDE_CODE_PROPAGATE_TRACEPARENT=1 is set, Claude Code emits client-side OTLP spans (claude_code.interaction, claude_code.llm_request) but does not inject a W3C traceparent header into the outbound HTTP requests to the LLM API. This means any OTLP-capable server (an API gateway, an observability proxy, etc.) creates its own trace rather than becoming a child span of Claude Code's client trace.
Observed behavior
CLAUDE_CODE_ENABLE_TELEMETRY=1+CLAUDE_CODE_PROPAGATE_TRACEPARENT=1+OTEL_TRACES_EXPORTER=otlp- Client spans export correctly via OTLP
- Server spans from the upstream API have a different
trace_idthan the client spans - No
traceparentheader appears on the outbound HTTP request (verified by checkinghttp.request.header.traceparenton the server span — absent)
Expected behavior
With CLAUDE_CODE_PROPAGATE_TRACEPARENT=1, the outbound HTTP request to the LLM API should carry a traceparent header derived from the active span context, per the W3C Trace Context spec. The server then creates a child span linked to the client's trace, producing a single connected trace.
Why this matters
Without traceparent propagation, client and server spans are disjoint traces. The only way to correlate them is by content + timing, which is fragile and breaks in concurrent workloads. This is the standard mechanism for distributed tracing across service boundaries, and the flag name suggests it's intended to work.
4 Comments
Related prior issues closed due to inactivity (not resolved): #58219, #66199. The flag
CLAUDE_CODE_PROPAGATE_TRACEPARENTappears to have been added since those were filed, but does not inject the header on the wire.On the client side this works for me, so the header is probably being lost after it leaves Claude Code. I tested 2.1.273 with
claude -p, pointingANTHROPIC_BASE_URLat a local stub that logs thetraceparentheader of each model request, andOTEL_EXPORTER_OTLP_ENDPOINTat a second stub that logs OTLP exports.All runs set
CLAUDE_CODE_ENABLE_TELEMETRY=1andCLAUDE_CODE_PROPAGATE_TRACEPARENT=1:| Tracing settings | Spans exported |
traceparentonPOST /v1/messages|| --- | --- | --- |
|
OTEL_TRACES_EXPORTER=console, noCLAUDE_CODE_ENHANCED_TELEMETRY_BETA| no | no ||
ENHANCED_TELEMETRY_BETA=1,OTEL_TRACES_EXPORTER=otlp, noOTEL_EXPORTER_OTLP_PROTOCOL| no | no ||
ENHANCED_TELEMETRY_BETA=1,otlp, protocolhttp/jsonorhttp/protobuf| yes (POST /v1/traces) | yes ||
ENHANCED_TELEMETRY_BETA=1,OTEL_TRACES_EXPORTER=console| yes | yes |(Without
CLAUDE_CODE_PROPAGATE_TRACEPARENT, a custom base URL gets no header, as documented.)So span export and the header go together. Tracing needs
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1, and anotlpexporter needsOTEL_EXPORTER_OTLP_PROTOCOLorOTEL_EXPORTER_OTLP_TRACES_PROTOCOL, since Claude Code has no default protocol. Your list shows neither, but since yourclaude_code.llm_requestspans do reach the collector, both must be set somewhere, and then the header should be on the wire. The header carries the right context: in a run I checked span by span, it was00-4894d4fe5926391191309882d13ee931-e0fad4020b824837-01, and the exportedclaude_code.llm_requestspan had trace ID4894d4fe…and span IDe0fad4020b824837.A few things would still explain the disjoint traces:
claude --versionis this? The changelog doesn't say when model-request propagation arrived, so an older build may not have it.X-Amzn-Trace-Id).tracecontext; if it's set to B3, X-Ray or Jaeger only, it creates a new root span even when the header arrives.http.request.header.<name>attributes only for headers you explicitly configure for capture, so a missinghttp.request.header.traceparentdoesn't prove the header wasn't received. The trace ID mismatch is the stronger signal.To see what your client sends with your exact environment, start this stub, then run
claude -p hiin the same shell withANTHROPIC_BASE_URL=http://127.0.0.1:18766added. The request fails, but the stub prints each path with itstraceparent. The/api/helloconnectivity check never carries one; look at the/v1/messageslines. The model request only goes to this local stub, which prints and discards it.If
/v1/messagesshows atraceparent, the header is lost downstream (2–4). If it showsNonewhile spans still reach your collector, please share the version and the telemetry variables, since that would be a client bug.One docs detail for maintainers: Monitoring says the header "is not sent to third-party providers", but with
CLAUDE_CODE_PROPAGATE_TRACEPARENT=1andCLAUDE_CODE_USE_BEDROCK=1, 2.1.273 also sent it on the Bedrockinvoke-with-response-streamrequest to a customANTHROPIC_BEDROCK_BASE_URL. That may be intended for gateways, but the page doesn't say so.Thanks for the detailed testing, this settles it. Your first question was the right one: it is a version difference. Propagation works on current builds, and my report was filed against a much older one.
I ran your stub with identical environment variables, varying only the Claude Code version:
| Version | Spans exported |
traceparentonPOST /v1/messages|| --- | --- | --- |
| 2.1.19 | yes | None |
| 2.1.273 | yes |
00-aa816a9fa3a2094ac9bc3dcf002110bf-18b9069980225f35-01|Same env in both runs:
CLAUDE_CODE_ENABLE_TELEMETRY=1,CLAUDE_CODE_PROPAGATE_TRACEPARENT=1,CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1,OTEL_TRACES_EXPORTER=otlp,OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf. I was on 2.1.19, so model-request propagation landed somewhere between that and 2.1.273. Happy for this to be closed as fixed.Two notes for anyone who hits this later.
Span export and header injection were independent on 2.1.19. You reasoned that because my
claude_code.llm_requestspans reached the collector, the header must have been on the wire. That does not hold on this older build. In a single run I gotPOST /v1/traceson the OTLP stub andtraceparent: Noneon/v1/messages. So "spans are arriving" is not sufficient to rule out the client, at least across versions.You are right about my evidence being weak. I cited an absent
http.request.header.traceparentattribute on the server span. As you note, OpenTelemetry only records that attribute for headers explicitly configured for capture, so its absence proved nothing. The trace ID mismatch was the real signal and the local stub is the correct instrument. I should have reached for it first.For completeness, I ruled out the other candidates for my setup:
tracecontext. While fixing this same class of bug in another client, once that client sent the header the gateway server spans adopted the clienttrace_idand setparent_span_idto the client span.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, which was set in my config alongsideENHANCED_TELEMETRY_BETA=1: not the cause. Removing it changed nothing on 2.1.19.Your docs observation looks worth a separate look, since the Bedrock behavior you saw contradicts the "not sent to third-party providers" line on the Monitoring page.
Thanks for running the stub on both versions, that pins it down. Good catch on 2.1.19 too: spans reaching the collector without the header means my "spans ⇒ header" reasoning only holds on current builds, so the stub check is the one to use across versions.
Since you opened the issue, you can close it as completed from your side.