[BUG] gRPC OTLP exporter ignores otelHeadersHelper-generated headers (request metadata not injected)

Resolved 💬 2 comments Opened Apr 28, 2026 by sproctor42 Closed Jun 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When Claude Code is configured with the otelHeadersHelper setting (per the monitoring documentation) AND the OTLP exporter transport is gRPC, the headers produced by the helper are not injected as gRPC call metadata on outbound RPCs. The OTLP exporter object initializes successfully, the helper script runs and returns valid headers (Authorization: Bearer …, x-goog-user-project: …, etc.), but the actual gRPC unary requests to the configured endpoint go out unauthenticated because the headers never reach the gRPC client's per-call metadata.

The same otelHeadersHelper setting works correctly for the HTTP/protobuf exporter — headers from the helper are visibly attached to outbound HTTP requests. The divergence is gRPC-specific.

The practical effect for users targeting an authenticated gRPC OTLP receiver (for example, GCP's telemetry.googleapis.com over OTLP/gRPC) is that every request returns:

7 PERMISSION_DENIED: Method doesn't allow unregistered callers
(callers without established identity).

…even though the helper successfully produced a valid bearer token and the Authorization header is exactly what the receiver would accept on the HTTP path.

A search of existing issues did not surface a report specifically describing this gRPC-vs-HTTP exporter divergence around otelHeadersHelper.

What Should Happen?

The behavior of otelHeadersHelper should be transport-agnostic. Whatever headers the helper returns should be:

  • Attached as HTTP headers when OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf.
  • Attached as gRPC call metadata when OTEL_EXPORTER_OTLP_PROTOCOL=grpc.

If this is intentionally not supported on gRPC, the documentation should state the limitation explicitly and recommend the workaround (e.g., a local OpenTelemetry Collector that bridges authentication via googleclientauth or another channel-credential-aware extension).

Error Messages/Logs

The gRPC server-side observation is the clean signal: any logging gRPC OTLP receiver (Reproducer B above) shows that metadata-from-helper is missing on every request.

The documented analysis in `opentelemetry-collector-contrib`'s `googleclientauth` extension repository (and Google Cloud Observability's published OTLP setup guidance) explicitly relies on **gRPC channel credentials** for authentication, not on application-injected headers. This corroborates that the gRPC client library does not consume application-set headers as call metadata.

Steps to Reproduce

The repro requires (a) Claude Code with telemetry enabled, gRPC transport, and otelHeadersHelper configured, and (b) a gRPC OTLP receiver that lets you observe what metadata each export RPC carries. Reproducer B below is the cleaner one for a maintainer to verify since it has no cloud dependency.

Reproducer B (recommended — no cloud dependency)

Run a local gRPC OTLP server that logs every incoming request's metadata to stdout (any small Go/Python OTLP receiver wired to print metadata.MD on each export call). Configure Claude Code to point at that server with OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port> and OTEL_EXPORTER_OTLP_PROTOCOL=grpc, and configure otelHeadersHelper to return any unique sentinel header (e.g., X-Test-Sentinel: claude-code-bug-repro).

Expected: the sentinel header appears in the gRPC server's per-RPC metadata.

Observed: the sentinel header does not appear; each RPC arrives with only the gRPC defaults (e.g., :authority, :path, content-type, user-agent, te, grpc-accept-encoding).

Reproducer A (production-context observation)

The original observation that surfaced this bug, included for completeness:

  1. Configure ~/.claude/settings.json with telemetry enabled, gRPC transport, an OTLP endpoint that requires auth, and an otelHeadersHelper script:

``json
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_EXPORTER_OTLP_ENDPOINT": "<your-authenticated-grpc-otlp-endpoint>",
"OTEL_METRICS_INCLUDE_SESSION_ID": "true",
"OTEL_METRICS_INCLUDE_VERSION": "true"
},
"otelHeadersHelper": "/path/to/your/headers-helper.sh"
}
``

  1. The helper script should produce a valid header set on stdout, for example:

``bash
#!/usr/bin/env bash
TOKEN=$(your-token-mint-command 2>/dev/null)
PROJECT=$(your-project-resolve-command 2>/dev/null)
printf '{"Authorization": "Bearer %s", "x-goog-user-project": "%s"}' "$TOKEN" "$PROJECT"
``

  1. Start a Claude Code session and observe outbound exports failing at the gRPC receiver with an unauthenticated-caller error (or, on GCP, 7 PERMISSION_DENIED: Method doesn't allow unregistered callers).

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

Unknown

Claude Code Version

2.1.119

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Other

Additional Information

Security considerations

This is filed as a functional bug; the behavior does not cross a security boundary: receivers that enforce authentication reject the request with PERMISSION_DENIED, and no protective control is defeated.

Suspected technical cause (hypothesis)

This subsection is user speculation, not assertion — included only to give a triager a head start.

The canonical gRPC authentication pattern uses channel credentials or per-RPC credentials managed by the gRPC auth stack (e.g., grpc.credentials.combineChannelCredentials with a Google auth credential), not application-layer header injection. The gRPC client library does not consume application-set headers as call metadata — call metadata must be attached via the gRPC API (Metadata / CallCredentials / interceptors).

The observed behavior is consistent with Claude Code's gRPC OTLP exporter being initialized with the helper-produced headers stored on the exporter object, but the actual makeUnaryRequest calls in the underlying gRPC export path sending unary requests without converting those headers into gRPC metadata. Both PeriodicExportingMetricReader and the log exporter exhibit the symptom — every export RPC is unauthenticated regardless of what the helper returns.

Context for the dropdown selections

  • Claude Model picked as Opus: the actual model used was Claude Opus 4.7 (1M context, model identifier claude-opus-4-7[1m], default effort level xhigh). The dropdown's Opus option is the closest available match.
  • Is this a regression? picked as I don't know: otelHeadersHelper and the gRPC OTLP exporter have likely had this divergence since otelHeadersHelper was introduced. The HTTP-exporter path appears to work correctly with the helper, so the gap is on the gRPC code path specifically — but no version in which gRPC + otelHeadersHelper worked correctly has been identified.
  • Operating System: Windows 11 Pro 10.0.26200.
  • Terminal/Shell picked as Other: tested in Git Bash for Windows (mingw64), which is not in the dropdown list. terminal.type: "mingw64" is reported by the SDK.

Workaround

Deploy a local OpenTelemetry Collector that exposes an otlp receiver on localhost (any free port; OTLP/gRPC's IANA default is 4317) and uses the otlphttp exporter to forward to the authenticated upstream, with the googleclientauth extension — or another channel-credential-aware auth extension — handling outbound auth. Point Claude Code at the local collector via OTEL_EXPORTER_OTLP_ENDPOINT. The local collector's gRPC receiver does not require auth (localhost trust boundary), and the upstream HTTP path uses canonical gRPC/HTTP channel credentials that work correctly.

Reviewer environment notes

  • All test data has been de-identified before inclusion in this report.
  • The bug is observable by any reproducer that logs incoming gRPC metadata, independent of any particular cloud provider — see Reproducer B above. GCP is mentioned only because that is the upstream against which this symptom was originally encountered in production-like configuration.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗