Skip to content

Feature Request: Enterprise OTel auth — mTLS env vars + dynamic-headers helper (parity with Claude Code) #3477

Closed

Description

@velimattiv

Summary

Copilot CLI's OpenTelemetry support (shipped via #2471) authenticates the OTLP
exporter via a static OTEL_EXPORTER_OTLP_HEADERS value only. This blocks
production enterprise deployments that need (a) mutual TLS to secured OTel
endpoints, or (b) automatic refresh of expiring auth tokens for Microsoft Entra
ID / managed-identity flows.

The peer product Claude Code already ships both. This request asks for parity.

The "parity with Claude Code" framing has precedent here — #2471 was titled
exactly that, and shipped Copilot CLI's OTel support in v1.0.4. This request
asks for the auth-side parity that the initial scope deferred.

What Copilot CLI has today

What Claude Code has and Copilot CLI doesn't

1. mTLS via env vars

Claude Code honours the standard OpenTelemetry mTLS configuration env vars.
From their monitoring docs:

Protocol Client certificate variables CA trust
http/protobuf, http/json CLAUDE_CODE_CLIENT_CERT, CLAUDE_CODE_CLIENT_KEY, optionally CLAUDE_CODE_CLIENT_KEY_PASSPHRASE NODE_EXTRA_CA_CERTS
grpc OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE (plus per-signal variants) OTEL_EXPORTER_OTLP_CERTIFICATE

The OTEL_EXPORTER_OTLP_CLIENT_* and OTEL_EXPORTER_OTLP_CERTIFICATE variables
are documented in the
OpenTelemetry specification.
The bundled @opentelemetry/exporter-trace-otlp-* packages already support
these at the SDK level — they just aren't surfaced from Copilot CLI.

2. Dynamic headers / token refresh

Claude Code lets administrators configure a script that returns headers as
JSON, run at startup and periodically thereafter (default every 29 minutes).
From their docs:

// .claude/settings.json
{
  "otelHeadersHelper": "/bin/generate_opentelemetry_headers.sh"
}
#!/bin/bash
# Mint a fresh Microsoft Entra Bearer for Azure Monitor every time the helper runs
echo "{\"Authorization\": \"Bearer $(az account get-access-token \
  --scope https://monitor.azure.com/.default --query accessToken -o tsv)\"}"

The interval is configurable via CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS.

Why this matters

Microsoft's recommended path for OTLP ingestion into Azure Monitor /
Application Insights uses Microsoft Entra Bearer tokens
(docs).
Entra access tokens are short-lived (60–90 min by default; up to 24h via
Configurable Token Lifetime policies). Without a refresh mechanism in Copilot
CLI, long-running sessions hit auth failures mid-session.

Today the workaround is for each enterprise to build a per-organisation
OTel-translation bridge that takes a static header from Copilot and exchanges
it for a refreshed Entra Bearer upstream. That is real operational overhead
for what is essentially missing config surface in Copilot CLI itself — the
underlying SDK already supports both auth modes.

The similar request on the VS Code side
(microsoft/vscode#304505)
asks for the same dynamic-headers capability, but the CLI is a distinct entry
point and warrants its own implementation.

Specific asks (small, well-scoped)

  1. Honour the standard OTel mTLS env vars in Copilot CLI's OTLP exporter —
    OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY,
    OTEL_EXPORTER_OTLP_CERTIFICATE, plus per-signal variants
    (OTEL_EXPORTER_OTLP_TRACES_* / _METRICS_* / _LOGS_*). The bundled
    @opentelemetry/exporter-trace-otlp-* packages implement these env vars
    per the OpenTelemetry spec — Copilot CLI needs a small wiring change to
    thread them into the exporter constructor.

  2. Add a dynamic-headers script hook — analogous to Claude Code's
    otelHeadersHelper. A config setting points at a script; the script
    outputs a JSON object of headers; runs at startup and on a configurable
    interval (suggested default: 29 minutes, matching Claude Code). HTTP
    transport only (matches Claude Code's design).

Why it's small

Both asks pass through existing OpenTelemetry SDK functionality. The first is
a small wiring change in the exporter setup. The second is a small wrapper
that periodically execs a script and rebuilds the exporter's headers —
Claude Code's implementation is the existing reference design.

Related

Activity

  1. added
    area:enterpriseGitHub Enterprise (GHE/GHES) support, org policies, and enterprise settings
    area:networkingProxy, SSL/TLS, certificates, corporate environments, and connectivity issues
    area:configurationConfig files, instruction files, settings, and environment variables
    on May 23, 2026
  2. added theissue type on May 23, 2026
  3. velimattiv commented on Jun 8, 2026

    @velimattiv
    Author

    Follow-up from the author — a worked example of the workaround this gap forces, from a live enterprise deployment.

    Since filing this, I've had to actually build the "per-organisation OTel-translation bridge" I described above as the workaround, to get Copilot CLI telemetry into Microsoft Azure Monitor / Log Analytics authenticated with Microsoft Entra ID for enterprise clients. Documenting it here both to strengthen the case and to give anyone else hitting this a replication recipe for any endpoint that needs protobuf-over-HTTP + a refreshed bearer.

    Two gaps compound here, not one. This issue (#3477) is the auth-refresh half; #2934 is the transport half. Azure Monitor's OTLP ingest is protobuf-only — it returns HTTP 415 to OTLP/HTTP JSON — and Entra access tokens are short-lived (60–90 min by default). Copilot CLI today offers only OTLP/HTTP JSON with a static OTEL_EXPORTER_OTLP_HEADERS. So the native exporter is unusable against Azure on both axes at once: wrong wire format and a header that's dead before a long session ends. Claude Code clears both — it emits http/protobuf natively and ships otelHeadersHelper — which is exactly why the parity framing in the original request is the right one.

    The shim we had to engineer. Because the OTLP exporter couldn't reach Azure, we fell back to Copilot's file exporter (COPILOT_OTEL_FILE_EXPORTER_PATH) and wrote a small local file-proxy / forwarder — a few hundred lines of zero-dependency Node — that does the job the exporter should do natively. The generic recipe, for anyone replicating against a protobuf+bearer endpoint:

    1. Tail the JSON-lines span file by a persisted byte-offset (store {offset, inode}; if the inode changes or the file shrank, reset to 0 — the CLI recreates the file across upgrades).
    2. Never truncate the file mid-session. Copilot holds the handle open; truncating corrupts concurrent writes and drops un-forwarded spans. We let container lifetime bound its growth instead.
    3. Transcode spans → the OTLP-logs protobuf shape the endpoint accepts (a hand-rolled varint / length-delimited encoder — ~60 lines, no protobufjs). Filter to the single span that carries authoritative token counts and exclude the parent rollup span, or you double-count 2×.
    4. Mint/refresh the Entra bearer out-of-band on a timer, and retry once on 401/403 with a forced re-mint. This is precisely what Feature Request: Enterprise OTel auth — mTLS env vars + dynamic-headers helper (parity with Claude Code) #3477 asks the CLI to do — reimplemented in user space because a static header silently starts dropping telemetry the instant it expires.
    5. POST application/x-protobuf to the ingest endpoint.

    The load-bearing, genuinely fiddly parts are the offset/inode bookkeeping (1) and the refresh-with-retry (4) — both are pure consequences of the two missing config surfaces, not anything intrinsic to the data.

    Why fixing this upstream is strictly better than the shim. The forwarder carries real, ongoing operational cost that exists only because the exporter can't be configured: a singleton-per-container lifecycle wired through session hooks, offset persistence across crashes, dedup on re-forward, demux of concurrent sessions, and a bespoke protobuf encoder we now own indefinitely. All of it evaporates the day Copilot CLI honours a dynamic-headers hook (#3477) and protobuf transport (#2934) — at which point we'd simply point the native exporter at Azure with an otelHeadersHelper-style script, exactly as we already do for Claude Code. The underlying @opentelemetry/exporter-* packages already support both modes; this remains a small wiring change that saves every enterprise from rebuilding the same bridge downstream.

    Happy to share the refresh-with-retry helper contract or the transcoder field-mapping if it's useful to whoever picks up the implementation.

  4. velimattiv commented on Jul 14, 2026

    @velimattiv
    Author

    The 2026-07-08 enterprise-managed OTel export moved the dial here, but did not close this issue.

    The enterprise-managed OpenTelemetry export is a real step — a managed telemetry block (endpoint, transport, resource attributes, and exporter headers) delivered via MDM / server-managed settings / managed-settings.json, applied to both Chat and the CLI agent host. That's most of what enterprise OTel needs.

    But the header it sets is still static — configured once, never refreshed — which is precisely the blocker this issue raises. The documented target for direct Azure Monitor / Log Analytics OTLP ingestion is a Microsoft Entra bearer that expires in ~60–90 min, so long sessions still fail mid-stream, and every enterprise is still forced to stand up a per-org bridge/collector that swaps the static header for a refreshed Entra token upstream. Eliminating that bridge — native OTLP → Azure Monitor with a refreshing credential — is the whole point of the dynamic-headers hook requested here (parity with Claude Code's otelHeadersHelper, ~29-min refresh).

    Now that the managed-settings surface exists, the natural fix is to let it carry a refresh mechanism — e.g. a managed setting pointing at a headers-helper script (or accepting the standard OTEL_EXPORTER_OTLP_HEADERS refresh pattern), run on an interval.

    Related: the VS Code Chat-side equivalent, microsoft/vscode#304505 ("Entra Id auth header support for OTel ingestion"), was closed as not planned — so this remains the live, cross-surface thread for the refresh gap. The 07-08 release adding managed headers without refresh is exactly why it's still needed.

  5. patniko commented on Oct 26, 2026

    @patniko
    Collaborator

    Thanks for the detailed request and the worked example. Both pieces are now available: Copilot CLI v1.0.61 added mTLS and private-CA support for OTLP export over HTTPS via the standard OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY and OTEL_EXPORTER_OTLP_CERTIFICATE variables (plus http/protobuf export via OTEL_EXPORTER_OTLP_PROTOCOL), and v1.0.100 added a telemetry headers command, configurable in user or enterprise-managed settings, that runs a helper you provide and periodically refreshes the OTLP authentication headers, which covers short-lived tokens such as Entra access tokens. See copilot help monitoring and copilot help environment for configuration details. If header refresh doesn't work for your Entra setup on the latest version, please reopen with details.

    Triaged as part of a Copilot SDK backlog review, with help from GitHub Copilot.

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

    area:configurationConfig files, instruction files, settings, and environment variablesarea:enterpriseGitHub Enterprise (GHE/GHES) support, org policies, and enterprise settingsarea:networkingProxy, SSL/TLS, certificates, corporate environments, and connectivity issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions