[BUG] Apps gateway serves Claude Desktop an `otlpEndpoint` pointing at its own bearer-gated OTLP ingest but no `otlpHeaders`, so every Desktop telemetry flush is rejected with `missing_token`
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?
Summary
A Claude apps gateway with telemetry.forward_to configured and a desktop block in a managed policy serves Claude Desktop a GET /user/bootstrap response containing otlpEndpoint set to listen.public_url — the gateway's own origin — and no otlpHeaders.
The gateway's OTLP ingest endpoints require a bearer token (POST /v1/metrics, /v1/logs, /v1/traces, per the gateway protocol reference). otlpHeaders is the only mechanism Claude Desktop has for attaching credentials to its OTLP exports. Because bootstrap populates the endpoint but not the headers, Desktop posts unauthenticated to an endpoint that mandates authentication, and 100% of Desktop telemetry is dropped at the door.
No client-side or MDM setting can fix this: the credential the endpoint wants is a per-user device-flow token with (in our case) a 1-hour TTL, and MDM otlpHeaders values are necessarily static.
Claude Code CLI telemetry through the same gateway works correctly — it authenticates with its session bearer and the gateway relays to forward_to. Desktop is the only affected client.
Environment
- Gateway server:
claude2.1.220, self-hosted (ECS Fargate behind an ALB) - Claude Desktop on Windows 11, managed via
HKLM\SOFTWARE\Policies\Claude - Auth: OIDC,
session.ttl_hours: 1 - Upstream: Amazon Bedrock
- Telemetry destination: OTLP/HTTP collector reached via
telemetry.forward_to
Notes / supporting detail
- The gateway protocol reference documents the telemetry endpoints as
POST /v1/metrics,/v1/logs,/v1/traces(bearer), and states that when connected to a gateway the client sends telemetry there and ignoresOTEL_EXPORTER_OTLP_*env vars — so the env-var path documented for CLI clients isn't available as a workaround either. - The gateway config reference notes that
telemetry.forward_to+listen.public_urlenables telemetry and that the CLI stampsuser.id/user.email/user.groupsfrom the JWT. Desktop instead receivesenduser.idas a resource attribute, so even once ingest is unblocked, Desktop and CLI telemetry arrive under different attribute names and don't aggregate on the same dashboards without a collector-side rename. - For contrast, AWS's reference implementation of a third-party bootstrap server (
aws-solutions-library-samples/guidance-for-claude-code-with-amazon-bedrock, PRs #669/#670) returnsotlpEndpointtogether withotlpHeaderscarrying per-user identity, and points the endpoint at a collector rather than at the bootstrap server itself. That appears to be the intended shape of a bootstrap response; the first-party gateway does neither.
Why there is no client-side workaround
An MDM otlpHeaders value cannot substitute for the missing one. Per the bootstrap server docs:
When a bootstrap response is available, it is the effective configuration. The MDM profile supplies the trust anchor (bootstrapUrl, optionalbootstrapOidc, and thebootstrapEnabledopt-out), and Claude Desktop does not consult MDM for any key the bootstrap server is permitted to set. A bootstrap-settable key that your response omits is treated as unset, not inherited from MDM, so return every key you want applied.
otlpHeaders is marked MDM + Bootstrap in the configuration reference, so it is bootstrap-settable and an MDM value for it is ignored while bootstrapUrl is configured. Combined with the gateway not emitting the key, there is no path by which Desktop can obtain an OTLP credential. The only ways out are both unattractive:
- Terminate the OTLP paths at the load balancer ahead of the gateway and accept them unauthenticated, since no credential can be delivered to distinguish legitimate clients.
- Abandon bootstrap for Desktop (
bootstrapEnabled: false) and configure the whole client through MDM, which restoresotlpHeadersbut gives up per-group policy, the gateway-derived model list, and per-user config delivery — the reasons to run a bootstrap server at all.
Neither preserves both authenticated telemetry ingest and gateway-delivered policy, which is why this reads as a gap rather than a configuration mistake.
What Should Happen?
One of:
- Bootstrap includes a usable credential — e.g.
otlpHeaders: {"Authorization": "Bearer <session token>"}. This looks like the natural fix: the response already carriesexpiresAtand Desktop refetches bootstrap periodically (~30 min in our observation), comfortably inside a 1-hoursession.ttl_hours, so the token would rotate on the existing refresh cycle. - The gateway accepts Desktop's exports without a bearer on a distinguishable basis, or supports a second inbound auth mode for the OTLP paths (a static ingest token configurable in
gateway.yaml). There is currently no such key —telemetry.forward_to[].headersis outbound-only andaccess_controlis CIDR-based. - Bootstrap omits
otlpEndpointwhen it can't supply a credential, so Desktop falls back to the MDM-configured endpoint instead of silently failing against the gateway.
At minimum, the gateway should not advertise an endpoint it will then reject, and the mismatch should be visible: today it is silent from the operator's perspective unless someone reads the ingest logs, and silent from the user's perspective entirely.
Error Messages/Logs
The bootstrap response:
{
"inferenceProvider": "gateway",
"inferenceGatewayBaseUrl": "https://claude-gateway.example.internal",
"inferenceGatewayAuthScheme": "sso",
"otlpEndpoint": "https://claude-gateway.example.internal",
"otlpProtocol": "http/json",
"otlpResourceAttributes": { "enduser.id": "user@example.com" },
"inferenceModels": [ "...5 models..." ],
"expiresAt": 1785254860
}
Note `otlpEndpoint` is present and `otlpHeaders` is absent.
Every subsequent Desktop flush is refused:
{"ts":"2026-07-28T14:13:09.918Z","evt":"auth.denied","request_id":"...","reason":"missing_token","path":"/v1/logs","client_ip":"..."}
{"ts":"2026-07-28T14:35:22.104Z","evt":"auth.denied","request_id":"...","reason":"missing_token","path":"/v1/metrics","client_ip":"..."}
Meanwhile `desktop_bootstrap.serve` events for the same user return 200 throughout, so this isn't a policy-matching or opt-in problem — bootstrap is working, it just hands over an unusable telemetry configuration.
Steps to Reproduce
- Configure a gateway with both
listen.public_urlandtelemetry.forward_to(this is the documented pair that enables telemetry), plus a managed policy carrying adesktopblock soGET /user/bootstrapserves instead of 404ing:
```yaml
listen:
public_url: https://claude-gateway.example.internal
telemetry:
forward_to:
metrics: true
logs: true
managed:
policies:
- match: {}
desktop: {}
```
- Point Claude Desktop at it with
bootstrapUrl+bootstrapEnabledvia managed configuration and sign in. Bootstrap succeeds — the user gets the model list and gateway inference works.
- Watch the gateway logs as Desktop flushes telemetry.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.220
Platform
AWS Bedrock
Operating System
Windows
Terminal/Shell
Other
Additional Information
_No response_
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Independent reproduction on a different platform and gateway version — confirming this is not environment-specific.
Environment
claude2.1.218, self-hosted ECS Fargate behind an internal ALB (imported public ACM cert)session.ttl_hours: 12global.anthropic.*inference profiles)forward_to: http://localhost:4318, metrics onlySo: Windows/2.1.220/MDM/1h TTL in the original report, macOS/2.1.218/in-app-field/12h TTL here. Same failure.
Confirmation of the root cause
otlpHeadersappears zero times in the 2.1.218 gateway binary. The bootstrap response builder emits only:Desktop's own config schema does define
otlpHeaders(alongsideotlpEndpoint,otlpProtocol,otlpResourceAttributes,otlpContentCapture,otlpTracesEnabled,otlpDesktopLogLevel), so the field exists on the client — the gateway simply never populates it.Direct reproduction against the running gateway:
Observed impact over 48h
| Path | Result | Count |
|---|---|---|
|
/v1/logs|auth.denied/missing_token| 108 ||
/v1/metrics|auth.denied/missing_token| 40 ||
/v1/logs,/v1/metrics,/v1/traces| authenticated successfully | 0 |Zero successful telemetry posts, ever. Meanwhile 57
/v1/messagescalls in the same window returned 200, and the ADOT sidecar loggedEverything is ready. Begin running and processing data.and then received nothing for the following ~21 hours while the gateway was actively serving inference. The collector is healthy; nothing reaches it.One additional detail worth adding: Desktop is authenticated at the time it fails.
Correlating audit events by hour,
desktop_bootstrap.serve(bearer-authenticated, 200) andauth.deniedon the OTLP paths occur in the same hours:Desktop is therefore holding a valid session bearer — it successfully authenticates to
/user/bootstrapminutes before and after posting telemetry unauthenticated. This suggests the fix need not involve minting a separate credential or plumbing a static header: Desktop already has the token it needs and simply isn't attaching it to OTLP exports. Having the client reuse its session bearer for the OTLP paths (whenotlpEndpointis same-origin with the gateway) would resolve it without touching the bootstrap contract, and would sidestep the static-vs-1h-TTL problem noted in the original report.Either fix works from an operator's perspective; this one just looks cheaper and avoids putting a rotating credential in a config payload.
Second-order effect, confirming the original note
The
enduser.idvsuser.id/user.emaildivergence matters more than it first appears: CloudWatch's Coding Agent Insights dashboards segment onuser.email, so even after ingest is unblocked, Desktop telemetry won't populate the per-developer views that CLI telemetry does — without a collector-side attribute rename. For anyone deploying the gateway to non-engineering users (Cowork/Desktop rather than CLI), the practical outcome today is no client telemetry at all, and the fallback is server-side data only: the gateway'sinferenceaudit events for adoption, and/v1/organizations/spend_limits/effectivefor per-user cost.Happy to supply the full audit extract or test a fix against this deployment.
My organization is running into this issue as well ^
Reproduced on gateway server 2.1.226 (this issue was filed against 2.1.220), so it appears to still be unfixed.
Same symptoms:
GET /user/bootstrapsucceeds (desktop_bootstrap.serve) and inference through/v1/messagesworks fine, butevery Desktop telemetry flush is rejected:
Claude Code CLI telemetry through the exact same gateway and the same
telemetry.forward_toconfig works correctly — metricsarrive at our collector with
user.id/user.email/user.groupsstamped from the session JWT. Desktop is the only affectedclient, which matches your analysis.
One additional note for whoever picks this up: even once ingest is unblocked, Desktop's
enduser.idresource attribute won'taggregate with the CLI's
user.email/user.groupson the same dashboards. For CloudWatch Coding Agent Insights specifically,the built-in dashboards depend on that attribute shape, so a collector-side rename would be required — which means fixing the
missing credential alone may not be sufficient to get Desktop and CLI usage into one view.
Our organization is running into this issue as well.