[BUG] OTEL_EXPORTER_OTLP_HEADERS is not applied to the traces exporter (enhanced telemetry beta)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
With the enhanced telemetry beta enabled, spans are exported over OTLP but the Authorization header from OTEL_EXPORTER_OTLP_HEADERS is never attached to the request.
Any backend that requires authentication rejects the export. In my case that is a self-hosted Langfuse instance, which uses HTTP Basic auth on its OTLP endpoint — it returns 401 and the spans are silently dropped. There is no error surfaced by the CLI, so the failure looks identical to "tracing is not working at all".
I confirmed the header is missing by pointing the exporter at a local server that logs incoming request headers. Spans arrive (2043 bytes, correct claude_code.* span names and gen_ai.* attributes), but no Authorization header is present.
I tried four variations, all with the same result:
| Variation | Result |
|---|---|
| OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64>" | no Authorization header |
| OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Basic <base64>" (per-signal) | no Authorization header |
| Value percent-encoded: Authorization=Basic%20<base64> | no Authorization header |
| Combined with OTEL_EXPORTER_OTLP_TRACES_ENDPOINT instead of the common endpoint | no Authorization header |
What Should Happen?
The documentation states that traces use the common OTLP configuration:
Traces reuse the common OTLP configuration for endpoint, protocol, headers, and mTLS.
So the POST /v1/traces request should carry the Authorization header built from OTEL_EXPORTER_OTLP_HEADERS, the same way it is documented for metrics and logs.
Error Messages/Logs
Receiving server output (headers logged verbatim, Authorization absent):
POST /v1/traces bytes=2043
Content-Type: application/x-protobuf
User-Agent: OTel-OTLP-Exporter-JavaScript/0.208.0
For comparison, the same endpoint reached with curl and the identical Basic credential returns 200, and without it returns 401 — so the credential itself and the endpoint are correct:
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3200/api/public/otel/v1/traces \
-u "pk-...:sk-..." -H "Content-Type: application/x-protobuf" --data-binary ""
200
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3200/api/public/otel/v1/traces \
-H "Content-Type: application/x-protobuf" --data-binary ""
401
Steps to Reproduce
- Save a server that logs request headers and listens on
127.0.0.1:4318:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
self.rfile.read(length)
print(f"POST {self.path} bytes={length}", flush=True)
for name in ("Content-Type", "Authorization", "User-Agent"):
if self.headers.get(name):
print(f" {name}: {self.headers[name]}", flush=True)
self.send_response(200)
self.end_headers()
def log_message(self, *args):
pass
HTTPServer(("127.0.0.1", 4318), Handler).serve_forever()
- Run it, then run Claude Code with tracing pointed at it:
CLAUDE_CODE_ENABLE_TELEMETRY=1 \
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 \
OTEL_TRACES_EXPORTER=otlp \
OTEL_METRICS_EXPORTER=none \
OTEL_LOGS_EXPORTER=none \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic dGVzdDp0ZXN0" \
claude -p "Reply with exactly one word: pong"
- The server logs the
POST /v1/traceswithContent-TypeandUser-Agent, but noAuthorization.
Environment
- Claude Code 2.1.221
- macOS 24.6.0 (arm64)
Is this a regression?
I don't know — I have only used the traces beta on this version.
Additional context
A workaround is to put an OpenTelemetry Collector in front and let it attach the credential (otlphttp exporter with a headers: block). That works, and I verified it end to end. But it means every deployment that exports traces to an authenticated backend needs an extra process, which the documented configuration implies is unnecessary.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗