[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`

Status Closed — not planned
Reported on v2.1.220
Maintainer reply None cached
Activity 17 comments · opened Jul 28, 2026 · closed Oct 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?

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: claude 2.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 ignores OTEL_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_url enables telemetry and that the CLI stamps user.id/user.email/user.groups from the JWT. Desktop instead receives enduser.id as 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) returns otlpEndpoint together with otlpHeaders carrying 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, optional bootstrapOidc, and the bootstrapEnabled opt-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:

  1. 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.
  2. Abandon bootstrap for Desktop (bootstrapEnabled: false) and configure the whole client through MDM, which restores otlpHeaders but 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:

  1. Bootstrap includes a usable credential — e.g. otlpHeaders: {"Authorization": "Bearer <session token>"}. This looks like the natural fix: the response already carries expiresAt and Desktop refetches bootstrap periodically (~30 min in our observation), comfortably inside a 1-hour session.ttl_hours, so the token would rotate on the existing refresh cycle.
  2. 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[].headers is outbound-only and access_control is CIDR-based.
  3. Bootstrap omits otlpEndpoint when 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

  1. Configure a gateway with both listen.public_url and telemetry.forward_to (this is the documented pair that enables telemetry), plus a managed policy carrying a desktop block so GET /user/bootstrap serves instead of 404ing:

```yaml
listen:
public_url: https://claude-gateway.example.internal
telemetry:
forward_to:

metrics: true
logs: true
managed:
policies:

  • match: {}

desktop: {}
```

  1. Point Claude Desktop at it with bootstrapUrl + bootstrapEnabled via managed configuration and sign in. Bootstrap succeeds — the user gets the model list and gateway inference works.
  1. 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_

View original on GitHub ↗

16 Comments

bluedoors · 2 months ago

Independent reproduction on a different platform and gateway version — confirming this is not environment-specific.

Environment

  • Gateway server: claude 2.1.218, self-hosted ECS Fargate behind an internal ALB (imported public ACM cert)
  • Claude Desktop 1.24012.11 on macOS (Darwin 25.6.0), configured via the in-app "Bootstrap config URL" field (non-MDM machine)
  • Auth: OIDC (Auth0), session.ttl_hours: 12
  • Upstream: Amazon Bedrock (global.anthropic.* inference profiles)
  • Telemetry destination: ADOT collector sidecar in the same task, forward_to: http://localhost:4318, metrics only

So: 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

otlpHeaders appears zero times in the 2.1.218 gateway binary. The bootstrap response builder emits only:

...telemetryEnabled && {
  otlpEndpoint: publicUrl,
  otlpProtocol: "http/json",
  otlpResourceAttributes: { "enduser.id": claims.email ?? claims.sub }
}

Desktop's own config schema does define otlpHeaders (alongside otlpEndpoint, otlpProtocol, otlpResourceAttributes, otlpContentCapture, otlpTracesEnabled, otlpDesktopLogLevel), so the field exists on the client — the gateway simply never populates it.

Direct reproduction against the running gateway:

POST /v1/logs  (no auth)       -> 401 {"type":"error","error":{"type":"authentication_error","message":"invalid token"}}
POST /v1/logs  (bogus bearer)  -> 401  (identical — confirms it's the credential, not the path)

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/messages calls in the same window returned 200, and the ADOT sidecar logged Everything 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) and auth.denied on the OTLP paths occur in the same hours:

2026-08-05 01:00  auth.denied              117
2026-08-05 01:00  desktop_bootstrap.serve    2
2026-08-05 00:00  auth.denied               13
2026-08-05 00:00  desktop_bootstrap.serve    3

Desktop is therefore holding a valid session bearer — it successfully authenticates to /user/bootstrap minutes 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 (when otlpEndpoint is 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.id vs user.id/user.email divergence matters more than it first appears: CloudWatch's Coding Agent Insights dashboards segment on user.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's inference audit events for adoption, and /v1/organizations/spend_limits/effective for per-user cost.

Happy to supply the full audit extract or test a fix against this deployment.

michael-ujifusa-psc · 2 months ago

My organization is running into this issue as well ^

knziiy · 2 months ago

Reproduced on gateway server 2.1.226 (this issue was filed against 2.1.220), so it appears to still be unfixed.

  • Gateway: self-hosted on ECS Fargate (Express Mode), Amazon Bedrock upstream, Amazon Cognito as the OIDC IdP
  • Region: ap-northeast-1
  • Claude Desktop on macOS

Same symptoms: GET /user/bootstrap succeeds (desktop_bootstrap.serve) and inference through /v1/messages works fine, but
every Desktop telemetry flush is rejected:

{"evt":"auth.denied","reason":"missing_token","path":"/v1/metrics","client_ip":"..."}
{"evt":"auth.denied","reason":"missing_token","path":"/v1/logs","client_ip":"..."}

Claude Code CLI telemetry through the exact same gateway and the same telemetry.forward_to config works correctly — metrics
arrive at our collector with user.id / user.email / user.groups stamped from the session JWT. Desktop is the only affected
client, which matches your analysis.

One additional note for whoever picks this up: even once ingest is unblocked, Desktop's enduser.id resource attribute won't
aggregate with the CLI's user.email / user.groups on 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.

nparekh30 · 2 months ago

Our organization is running into this issue as well.

abdulmateen59 · 2 months ago

Same issue here! Experiencing the exact same thing. Hope this gets resolved soon.

adiospeds · 2 months ago

I can confirm I am also seeing the same issue -
The Claude apps gateway server version for me is 2.1.226 and
Claude desktop version is Claude 1.28929.0 (d1a6bc) 2026-08-11T18:58:06.000Z

Error seeing in gateway logs is as follows :

{"ts":"2026-08-12T10:11:18.433Z","evt":"auth.denied","request_id":"c457cddc-a893-49bc-97cf-b9263be87e16","reason":"missing_token","path":"/v1/logs","client_ip":"xxxxxxxx"}
{"ts":"2026-08-12T10:11:20.747Z","evt":"auth.denied","request_id":"2b311416-94e0-47dc-b71f-2073a1aaf971","reason":"missing_token","path":"/v1/metrics","client_ip":"xxxxxxxxx"}

i have not even enabled logs - but it seems claude desktop is trying to send the logs
But is by telemetry config from gateway.yaml file:

telemetry:
  forward_to:
    - url: https://monitoring.us-east-1.amazonaws.com
      headers:
        Authorization: "Bearer ${OTEL_TOKEN}"
      # Per-signal opt-in. Default: metrics only.
      metrics: true
      logs: false
      traces: false

With the same config cli works fine and all metrics are being sent properly - so this is only an issue with claude desktop.
But for cli I noticed that even for cli in the ~/.claude/remote-settings.json file, logs and traces exporter is set to "oltp" instead of "none". even though I have set logs: false and traces: false in gateway.yaml.

    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_TRACES_EXPORTER": "otlp",

Why are we sending the logs when admin are opting out and then droping them off at gateway level ?

adiospeds · 2 months ago

Seems like it was fixed in the 2.1.229 changelog: https://code.claude.com/docs/en/changelog#2-1-229
Fixed OpenTelemetry export from Claude Desktop sessions being rejected by the Desktop-managed gateway when that gateway is also the telemetry endpoint - can everyone pls check @k-brooks , @bluedoors , @michael-ujifusa-psc, @nparekh30, @knziiy, @abdulmateen59 .
Will also check on my side.

knziiy · 2 months ago

Re-tested on gateway server 2.1.229 — same symptom still reproduces. The 2.1.229 changelog entry ("Fixed OpenTelemetry export from Claude Desktop sessions being rejected by the Desktop-managed gateway when that gateway is also the telemetry endpoint") does not appear to fix this case, or at least doesn't fix it for this configuration.

  • Gateway: self-hosted on ECS Fargate (Express Mode), Amazon Bedrock upstream, Amazon Cognito as the OIDC IdP
  • Region: ap-northeast-1
  • Claude Desktop 1.26832.0 on macOS, Cowork tab, claude-sonnet-5 selected

Gateway audit log, identical to the original report:

{"evt":"auth.denied","reason":"missing_token","path":"/v1/metrics","client_ip":"..."}
{"evt":"auth.denied","reason":"missing_token","path":"/v1/logs","client_ip":"..."}

No telemetry lands in CloudWatch for the session. I haven't captured the raw /user/bootstrap response body for this specific run, so I can't confirm otlpHeaders is still absent at the JSON level — but the reason code and affected paths are identical to the original report, so it looks like the same failure mode.

adiospeds · 2 months ago

Yeah I too redeployed the gateway with claude version 2.1.229 - but no luck still seeing the same issue for metrics and also seeing failed /v1/logs calls even though I have disabled sending of logs:

{"ts":"2026-08-13T11:46:23.776Z","evt":"auth.denied","request_id":"xx-1448-406f-ad31-xx","reason":"missing_token","path":"/v1/logs","client_ip":"xxx"}
{"ts":"2026-08-13T11:46:23.500Z","evt":"auth.denied","request_id":"xx-34d6-4ca6-a58c-xx","reason":"missing_token","path":"/v1/logs","client_ip":"xxx"}
{"ts":"2026-08-13T11:46:26.238Z","evt":"auth.denied","request_id":"xx-e403-492b-bb67-xx","reason":"missing_token","path":"/v1/logs","client_ip":"xxx"}
{"ts":"2026-08-13T11:46:25.938Z","evt":"auth.denied","request_id":"xx-0822-4114-9d90-xx","reason":"missing_token","path":"/v1/metrics","client_ip":"xxx"}
{"ts":"2026-08-13T11:46:28.947Z","evt":"auth.denied","request_id":"xx-e8a1-439f-a87c-xx","reason":"missing_token","path":"/v1/metrics","client_ip":"xxx"}
adiospeds · 2 months ago

@ashwin-ant , @ollie-anthropic , @hackyon-anthropic , @weishen-anthropic , @jarrah-anthropic , @carson-anthropic , @angie-anthropic , @wim-anthropic , @gmulley-anthropic , @derekl-anthropic , @vadim-anthropic , @alin-ant , @jason-anthropic , @joan-anthropic , @joel-at-anthropic , @np-anthropic , request you to please priortize/ look into this bug as it is blocking cowork adoption in our org.

knziiy · 2 months ago

@adiospeds

I can confirm that metrics from Claude Desktop (Cowork) are being successfully received by gateway version 2.1.226—not 2.1.229—after updating Claude Desktop on macOS to version Claude 1.30096.1 (194d93) 2026-08-13T21:28:02.000Z.

It may be that a client-side update is also required for the fix to take effect.

adiospeds · 2 months ago

I can see in their today changelog this issue has been addressed in the 3P section:
https://claude.com/docs/cowork/changelog#v1-30096-1

  • Added otlpAuthMode and otlpHeadersHelper, two ways to authenticate telemetry exports without static otlpHeaders: set otlpAuthMode to inference-credential to reuse the signed-in user’s inference token, or point otlpHeadersHelper at an executable that prints the collector headers as JSON.
  • Fixed OpenTelemetry exports being rejected when the configured gateway also serves as the telemetry collector endpoint.

@k-brooks , @bluedoors , @michael-ujifusa-psc , @nparekh30 , @knziiy , @abdulmateen59
Waiting for a majority before we can all this as closed. (Also feel free follow to tag me in future issues / testing so we can get more traction on such 3p Issues and build a community around it if required. I have followed you all)

After upgrading Claude desktop/cowork to: Claude 1.30096.1 (194d93) 2026-08-13T21:28:02.000Z
I see this issue has been fixed. I don't see the errors in gateway logs and the metrics are seen on the configured receiver.

<img width="456" height="339" alt="Image" src="https://github.com/user-attachments/assets/510b6e3e-ff19-40d2-a42c-6dc1064e7876" />

adiospeds · 2 months ago

Seems like it is not fully fixed ... for users who do not have claude code cli setup on their machines the claude desktop still continues to throw the same errors.
My Theory is that users with only Claude Cowork/ Claude code desktop have a "Claude Code-credentials-xxxxxxxx" like credential in their keychain - but users with claude code cli have a "Claude Code-credentials" and desktop is trying to pass the jwt token from "Claude Code-credentials" which is not existant for users with no claude code cli and hence get a {"ts":"2026-08-13T11:46:28.947Z","evt":"auth.denied","request_id":"xx-e8a1-439f-a87c-xx","reason":"missing_token","path":"/v1/metrics","client_ip":"xxx"} in the gateway logs since nothing is sent.

CC: @ashwin-ant

k-brooks · 1 month ago

Our use case has been resolved - Claude Code CLI, Cowork, and Desktop Code all emit telemetry to the Gateway successfully.

adiospeds · 1 month ago

Can you try configuring a new user with only Claude Desktop / Cowork (no claude code cli) and see if the metrics are being sent. ?

michael-ujifusa-psc · 1 month ago
I can see in their today changelog this issue has been addressed in the 3P section: https://claude.com/docs/cowork/changelog#v1-30096-1 Added otlpAuthMode and otlpHeadersHelper, two ways to authenticate telemetry exports without static otlpHeaders: set otlpAuthMode to inference-credential to reuse the signed-in user’s inference token, or point otlpHeadersHelper at an executable that prints the collector headers as JSON. Fixed OpenTelemetry exports being rejected when the configured gateway also serves as the telemetry collector endpoint. @k-brooks , @bluedoors , @michael-ujifusa-psc , @nparekh30 , @knziiy , @abdulmateen59 Waiting for a majority before we can all this as closed. (Also feel free follow to tag me in future issues / testing so we can get more traction on such 3p Issues and build a community around it if required. I have followed you all) After upgrading Claude desktop/cowork to: Claude 1.30096.1 (194d93) 2026-08-13T21:28:02.000Z I see this issue has been fixed. I don't see the errors in gateway logs and the metrics are seen on the configured receiver. <img alt="Image" width="456" height="339" src="https://private-user-images.githubusercontent.com/8983446/636120691-510b6e3e-ff19-40d2-a42c-6dc1064e7876.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODczMjEzMTIsIm5iZiI6MTc4NzMyMTAxMiwicGF0aCI6Ii84OTgzNDQ2LzYzNjEyMDY5MS01MTBiNmUzZS1mZjE5LTQwZDItYTQyYy02ZGMxMDY0ZTc4NzYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI2MDgyMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNjA4MjFUMTQwMzMyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NDFiNzRhMjg1ZmMwNTYyZmIzOTc5ZjYzNDU2YWMxYjBmYjdjMTZiZjVjNzA3YTcwM2E4ZmNkMjI1NjVhYTIxZiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmcmVzcG9uc2UtY29udGVudC10eXBlPWltYWdlJTJGcG5nIn0.fxaXuAijmw05mWuCJ_MSwCUErRWF87aH5CM2lE3qUcg">

Can confirm that the issue is resolved for our use case.

Showing cached comments. Read the full discussion on GitHub ↗