[FEATURE] customHeadersHelper: fetch ANTHROPIC_CUSTOM_HEADERS from a command (like otelHeadersHelper)

Status Open
Reported on v2.1.283
Maintainer reply None cached
Activity 0 comments · opened Sep 28, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

ANTHROPIC_CUSTOM_HEADERS often carries secrets, not just static metadata:

  • Many LLM gateways authenticate with their own key in a custom header while Authorization keeps the Anthropic credential (e.g. cf-aig-authorization for Cloudflare AI Gateway, x-portkey-api-key for Portkey).
  • Organizations often issue short-lived tokens (SSO / OIDC) that must be refreshed during long sessions.
  • Security policies commonly forbid storing secrets in plain text on disk, so the value has to come from a keychain, password manager, or vault at runtime.

Today the only way to keep such a header out of settings.json is to set ANTHROPIC_CUSTOM_HEADERS in the environment at launch. That doesn't hold up with background sessions. A background session gets ANTHROPIC_CUSTOM_HEADERS from the environment of whichever client dispatches or respawns it, and only when that client's working directory matches the session's. The value is not persisted in the job state, and the daemon's own environment is not used. As a result, the header silently goes missing when:

  • a session is dispatched or respawned by a client that doesn't have the header (an IDE, a script, a launchd or supervisor job, or any entry point that bypasses the wrapper), even if the daemon itself was started with it;
  • a session is respawned or resumed from a different directory (claude respawn, claude --bg --resume, or Agent View), even if the client has the header;
  • the daemon restarts and the session is later respawned by a client without the header.

Not persisting the secret and not leaking it across sessions are both the right calls, and neither should be reverted. But it means that every entry point has to carry the secret, and some paths drop it anyway.

Proposed Solution

A settings key that runs a command and uses its output as extra HTTP headers on model API requests, the same way otelHeadersHelper works for OTel exports and headersHelper works for MCP servers:

{
  "customHeadersHelper": "secret-store read example-headers"
}
  • stdout is parsed the same way as ANTHROPIC_CUSTOM_HEADERS (Name: value per line), or as a JSON object like otelHeadersHelper.
  • The result is merged into model API requests after ANTHROPIC_CUSTOM_HEADERS.
  • The helper is refreshed on a TTL (like CLAUDE_CODE_API_KEY_HELPER_TTL_MS) and re-run on 401.
  • It is evaluated inside each session and worker process, so foreground, --bg, claude agents, and daemon-spawned sessions behave the same.
  • The same trust and policy rules as other helpers apply (e.g. disallowed from project settings unless trusted, can be disabled by managed settings).

Alternative Solutions

Current workaround is a shell wrapper that sets the header at launch:

ANTHROPIC_CUSTOM_HEADERS="X-Example-Token: Bearer $(secret-store read example-token)" exec claude "$@"

This breaks for background sessions as described above. None of the existing options can carry a secret header either:

| Option | Why it doesn't fit |
|---|---|
| env.ANTHROPIC_CUSTOM_HEADERS in settings.json | Stores the secret in plain text on disk |
| --settings '<json>' | Works, but the value is persisted in plain text in the background job state (jobs/<id>/state.json, daemon/roster.json) and appears in argv |
| --settings <file> | The path is kept for respawns, so the file has to stay on disk indefinitely |
| apiKeyHelper | Its output goes into Authorization / X-Api-Key, which is already taken by the Anthropic credential |
| MCP headersHelper, otelHeadersHelper | Don't apply to model API requests |
| processWrapper (corporate launcher) | Works for daemon-spawned processes, but foreground claude still needs a separate shell wrapper, and the secret lives in every process's environment. It is a process-level workaround for what is a per-request header concern |

A smaller change that would also solve this: an option to send apiKeyHelper output in a configurable header name instead of Authorization / X-Api-Key.

This is not a duplicate of #4276 (env var expansion in settings.json). Expansion still resolves from the process environment, so it has the same propagation problem.

Priority

Medium - Would be very helpful

Feature Category

Configuration and settings

Use Case Example

  1. A team routes Claude Code through an LLM gateway that requires a per-user token in a custom header. Each developer keeps the token in the OS keychain.
  2. A developer opens claude agents and dispatches sessions across several repositories.
  3. Later the daemon restarts, or the developer resumes a session from another directory. The respawned sessions send requests without the header, and the gateway rejects them with 401.
  4. Today the only reliable fix is putting the token in settings.json in plain text, which the security policy forbids.
  5. With customHeadersHelper, every session process reads the token from the keychain itself, so the header is present no matter how the session was started, and token rotation just works.

Additional Context

Environment: Claude Code 2.1.283, macOS, native install

Reproduction:

  1. Put ANTHROPIC_BASE_URL in ~/.claude/settings.json, pointing at any endpoint that logs request headers.
  2. In /tmp/a, run ANTHROPIC_CUSTOM_HEADERS='X-Example-Token: Bearer dummy' claude --bg "hello". The request carries X-Example-Token.
  3. Run claude daemon stop --any.
  4. In /tmp/b, run ANTHROPIC_CUSTOM_HEADERS='X-Example-Token: Bearer dummy' claude respawn <id>, then attach and send a prompt. The request arrives without X-Example-Token.
  5. Repeat step 4 in /tmp/a, once with and once without ANTHROPIC_CUSTOM_HEADERS. The header is present only when the respawning client is in the original directory and has the variable set.

View original on GitHub ↗