Skip to content

[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 #82092

Description

@k-brooks

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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 Regression: File Write Protection Not Re-Enabled After /compact Command #669/It still ask for Git User Details #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": "[email protected]" },
  "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:

    listen:
      public_url: https://claude-gateway.example.internal
    telemetry:
      forward_to:
        - url: http://localhost:4318
          metrics: true
          logs: true
    managed:
      policies:
        - match: {}
          desktop: {}
  2. 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.

  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstaleIssue is inactive

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions