CLAUDE_CODE_PROPAGATE_TRACEPARENT=1 does not inject traceparent header into outbound LLM requests

Status Fixed / completed
Maintainer reply None cached
Activity 4 comments · opened Sep 14, 2026 · closed Sep 18, 2026

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_id than the client spans
  • No traceparent header appears on the outbound HTTP request (verified by checking http.request.header.traceparent on 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.

View original on GitHub ↗

4 Comments

joshreini1 · 1 month ago

Related prior issues closed due to inactivity (not resolved): #58219, #66199. The flag CLAUDE_CODE_PROPAGATE_TRACEPARENT appears to have been added since those were filed, but does not inject the header on the wire.

errmakov · 29 days ago

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, pointing ANTHROPIC_BASE_URL at a local stub that logs the traceparent header of each model request, and OTEL_EXPORTER_OTLP_ENDPOINT at a second stub that logs OTLP exports.

All runs set CLAUDE_CODE_ENABLE_TELEMETRY=1 and CLAUDE_CODE_PROPAGATE_TRACEPARENT=1:

| Tracing settings | Spans exported | traceparent on POST /v1/messages |
| --- | --- | --- |
| OTEL_TRACES_EXPORTER=console, no CLAUDE_CODE_ENHANCED_TELEMETRY_BETA | no | no |
| ENHANCED_TELEMETRY_BETA=1, OTEL_TRACES_EXPORTER=otlp, no OTEL_EXPORTER_OTLP_PROTOCOL | no | no |
| ENHANCED_TELEMETRY_BETA=1, otlp, protocol http/json or http/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 an otlp exporter needs OTEL_EXPORTER_OTLP_PROTOCOL or OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, since Claude Code has no default protocol. Your list shows neither, but since your claude_code.llm_request spans 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 was 00-4894d4fe5926391191309882d13ee931-e0fad4020b824837-01, and the exported claude_code.llm_request span had trace ID 4894d4fe… and span ID e0fad4020b824837.

A few things would still explain the disjoint traces:

  1. Version. Which claude --version is this? The changelog doesn't say when model-request propagation arrived, so an older build may not have it.
  2. Something in between. Gateways and load balancers often drop unknown headers or start their own trace (AWS load balancers and API Gateway, for example, use X-Amzn-Trace-Id).
  3. The server's propagator. The server has to extract W3C tracecontext; if it's set to B3, X-Ray or Jaeger only, it creates a new root span even when the header arrives.
  4. The check itself. OpenTelemetry records http.request.header.<name> attributes only for headers you explicitly configure for capture, so a missing http.request.header.traceparent doesn'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 hi in the same shell with ANTHROPIC_BASE_URL=http://127.0.0.1:18766 added. The request fails, but the stub prints each path with its traceparent. The /api/hello connectivity check never carries one; look at the /v1/messages lines. The model request only goes to this local stub, which prints and discards it.

python3 -c '
import http.server
class H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        print(self.path, "traceparent:", self.headers.get("traceparent"), flush=True)
        self.send_response(400); self.send_header("Content-Length", "0"); self.end_headers()
    do_HEAD = do_GET = do_POST
    def log_message(self, *a): pass
http.server.HTTPServer(("127.0.0.1", 18766), H).serve_forever()'

If /v1/messages shows a traceparent, the header is lost downstream (2–4). If it shows None while 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=1 and CLAUDE_CODE_USE_BEDROCK=1, 2.1.273 also sent it on the Bedrock invoke-with-response-stream request to a custom ANTHROPIC_BEDROCK_BASE_URL. That may be intended for gateways, but the page doesn't say so.

joshreini1 · 27 days ago

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 | traceparent on POST /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_request spans reached the collector, the header must have been on the wire. That does not hold on this older build. In a single run I got POST /v1/traces on the OTLP stub and traceparent: None on /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.traceparent attribute 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:

  • Something in between dropping the header: no, the header was never sent. The stub is a local listener with nothing between it and the client.
  • The server-side propagator: no. I separately confirmed the gateway I was pointing at does extract W3C tracecontext. While fixing this same class of bug in another client, once that client sent the header the gateway server spans adopted the client trace_id and set parent_span_id to the client span.
  • CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, which was set in my config alongside ENHANCED_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.

errmakov · 27 days ago

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.