Skip to content

fix(providers): mirror CLAUDE_API_KEY to ANTHROPIC_API_KEY for the Claude subprocess#1941

Open
uinstinct wants to merge 1 commit into
coleam00:devfrom
uinstinct:fix/claude-api-key-anthropic-mapping
Open

fix(providers): mirror CLAUDE_API_KEY to ANTHROPIC_API_KEY for the Claude subprocess#1941
uinstinct wants to merge 1 commit into
coleam00:devfrom
uinstinct:fix/claude-api-key-anthropic-mapping

Conversation

@uinstinct

Copy link
Copy Markdown

Summary

  • Problem: CLAUDE_API_KEY set in .env (solo path, no per-user credential store) passes Archon's own credential validation but is never translated to ANTHROPIC_API_KEY — the only variable the Claude Code CLI / Agent SDK authenticate with. The key never reaches the Claude subprocess.
  • Why it matters: In Docker (no claude /login state in the container) every Claude query fails with a non-retryable Claude Code auth error. On hosts where claude /login ran, queries silently use subscription auth — the operator thinks they are on metered API billing but consume subscription quota. The Docker deployment doc explicitly offers CLAUDE_API_KEY as "Option B", so the documented path is broken.
  • What changed: buildSubprocessEnv() now mirrors CLAUDE_API_KEYANTHROPIC_API_KEY on the returned env copy, but only when CLAUDE_API_KEY is set, ANTHROPIC_API_KEY is not already set, and no CLAUDE_CODE_OAUTH_TOKEN is present. Added 5 unit tests; annotated the CLAUDE_API_KEY entry in .env.example.
  • What did NOT change (scope boundary): No change to delivery.ts (the per-user path already sets both vars), process.env is never mutated (only the returned copy), hasExplicitTokens / log semantics are untouched, and the requestOptions.env merge order at the call site is unchanged (per-user delivered credentials still override the base env).

UX Journey

Before

Solo .env with CLAUDE_API_KEY=sk-ant-... (no OAuth token, no claude /login):

  User              Archon                       Claude subprocess
  ────              ──────                       ─────────────────
  sends message ──▶ boot: hasClaudeCredentials=true (OK, no warning)
                    buildSubprocessEnv()
                    env = { ...process.env }      (CLAUDE_API_KEY only,
                                                   no ANTHROPIC_API_KEY)
                    query(options.env) ─────────▶ CLI reads ANTHROPIC_API_KEY
                                                   → not present → AUTH FAIL
                    errorClass 'auth'
                    shouldRetry=false
  ❌ "Claude Code   ◀── fails immediately
     auth error"

After

Same solo .env:

  User              Archon                       Claude subprocess
  ────              ──────                       ─────────────────
  sends message ──▶ buildSubprocessEnv()
                    env = { ...process.env }
                    *if CLAUDE_API_KEY && !ANTHROPIC_API_KEY && !OAUTH:*
                    *  env.ANTHROPIC_API_KEY = CLAUDE_API_KEY*
                    query(options.env) ─────────▶ CLI reads ANTHROPIC_API_KEY
                                                   → present → AUTHENTICATES
  ✅ streams reply  ◀── response

OAuth path unchanged: if CLAUDE_CODE_OAUTH_TOKEN is set, no API key is injected and the subprocess continues to use subscription auth exactly as before.

Architecture Diagram

Before

server/index.ts (boot validation: accepts CLAUDE_API_KEY)
        │
        ▼
providers/claude/provider.ts
  buildSubprocessEnv() ── returns { ...process.env } ──▶ SDK query(options.env)
                                                          (no ANTHROPIC_API_KEY)

core/credentials/delivery.ts  (per-user path: sets BOTH vars — not on solo path)

After

server/index.ts (boot validation: unchanged)
        │
        ▼
providers/claude/provider.ts
  [~] buildSubprocessEnv() ── mirrors CLAUDE_API_KEY → ANTHROPIC_API_KEY ══▶ SDK query(options.env)
                              (additive, OAuth-guarded; copy only)              (ANTHROPIC_API_KEY present)

core/credentials/delivery.ts  (unchanged — solo path now matches its both-vars behavior)

Connection inventory:

From To Status Notes
providers/claude buildSubprocessEnv() SDK query() options.env modified now includes ANTHROPIC_API_KEY in the API-key-alone case
server/index.ts boot validation providers/claude unchanged
core/credentials/delivery.ts providers/claude unchanged per-user path already correct

Label Snapshot

  • Risk: risk: low
  • Size: size: XS
  • Scope: providers (closest available list value; package is @archon/providers)
  • Module: providers:claude

Change Metadata

  • Change type: bug
  • Primary scope: providers (packages/providers/src/claude)

Linked Issue

Validation Evidence (required)

bun run validate (run with NODE_OPTIONS=--max-old-space-size=8192 — the ESLint full-repo pass OOMs at the default heap on a low-memory container; this is an environment limit, not a code issue):

$ bun run check:bundled         → ok
$ bun run check:bundled-skill   → ok
$ bun run check:bundled-schema  → ok
$ bun run type-check            → all 10 packages Exited with code 0
$ bun run lint --max-warnings 0 → ok (0 warnings)
$ bun run format:check          → All matched files use Prettier code style!
$ bun run test                  → all packages 0 fail

@archon/providers (the touched package) including the 5 new tests:

$ cd packages/providers && bun test src/claude/provider.test.ts
 80 pass
 0 fail
 159 expect() calls
  • Evidence provided: full bun run validate output (commands above), targeted provider test run.
  • Skipped commands: none. (Lint required a raised Node heap locally; CI memory is unaffected.)

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? Yes — the change copies an already-present secret (CLAUDE_API_KEY) to a second key (ANTHROPIC_API_KEY) inside the subprocess env copy only. process.env is not mutated. No secret is logged, transmitted, or persisted; the value already lived in the same process env. The copy is gated so it only occurs when the operator supplied an API key and no OAuth token, i.e. exactly the case where they intend API-key auth.
  • File system access scope changed? No

Compatibility / Migration

  • Backward compatible? Yes — strictly additive and OAuth-guarded. The only configuration whose behavior changes is API-key-alone (currently broken). Deployments with CLAUDE_CODE_OAUTH_TOKEN, with an explicit ANTHROPIC_API_KEY, or with global auth are byte-for-byte unchanged.
  • Config/env changes? No (only a clarifying comment added to .env.example).
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios: traced all five env permutations through sendQuery → SDK options.env (the 5 new tests). Confirmed precedence explicit ANTHROPIC_API_KEY > CLAUDE_CODE_OAUTH_TOKEN > CLAUDE_API_KEY. Verified the per-user delivery path (requestOptions.env) still overrides the base env.
  • Edge cases checked: API key alone (maps); explicit ANTHROPIC_API_KEY present (not clobbered); OAuth token present (no injection); nothing set (no injection); requestOptions.env override (wins).
  • What was not verified: live end-to-end against a real Anthropic API key / real Docker container (no credentials available in this environment). The behavior is exercised at the exact integration point (options.env handed to query()).

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: only the Claude provider subprocess env (packages/providers/src/claude/provider.ts). Every Claude-backed workflow/conversation benefits in the API-key-alone case; all other auth modes unaffected.
  • Potential unintended effects: none expected — the OAuth guard prevents silently flipping subscription deployments to API-key billing; the !ANTHROPIC_API_KEY guard prevents overriding an explicitly chosen key.
  • Guardrails/monitoring for early detection: existing using_global_auth / using_explicit_tokens boot log; auth failures still surface as Claude Code auth error.

Rollback Plan (required)

  • Fast rollback command/path: single-commit revert — git revert <commit> (the change is one isolated commit touching provider.ts, provider.test.ts, .env.example).
  • Feature flags or config toggles: none. To opt out of the mapping, set an explicit ANTHROPIC_API_KEY or use CLAUDE_CODE_OAUTH_TOKEN.
  • Observable failure symptoms: Claude queries returning Claude Code auth error despite a valid CLAUDE_API_KEY would indicate regression.

Risks and Mitigations

  • Risk: an operator running both CLAUDE_API_KEY and CLAUDE_CODE_OAUTH_TOKEN expecting API-key billing.
    • Mitigation: OAuth token intentionally takes precedence (matches pre-existing CLI behavior, which sees only the OAuth token); documented in .env.example. Changing this would silently alter billing for existing deployments, so it is deliberately out of scope.

…aude subprocess

CLAUDE_API_KEY is Archon's env var name, but the Claude Code CLI and Agent
SDK authenticate only via ANTHROPIC_API_KEY. In the solo .env path,
buildSubprocessEnv() returned process.env untranslated, so the key never
reached the subprocess: Docker queries failed with a non-retryable auth
error, and hosts with `claude /login` silently used subscription auth.

Mirror CLAUDE_API_KEY -> ANTHROPIC_API_KEY on the returned env copy, only
when CLAUDE_API_KEY is set, ANTHROPIC_API_KEY is not already set, and no
CLAUDE_CODE_OAUTH_TOKEN is present. Precedence: explicit ANTHROPIC_API_KEY
> CLAUDE_CODE_OAUTH_TOKEN > CLAUDE_API_KEY. Strictly additive: only the
currently-broken API-key-alone config changes behavior. Mirrors the
per-user delivery.ts path, which already sets both vars.

Closes coleam00#1940
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8a2786e-2e86-444f-bbaa-1bc58b8c12b1

📥 Commits

Reviewing files that changed from the base of the PR and between d72c20c and d9438d5.

📒 Files selected for processing (3)
  • .env.example
  • packages/providers/src/claude/provider.test.ts
  • packages/providers/src/claude/provider.ts
 _______________________________________________________________________________________________________________________________________________________________________________
< The first 90% of the code accounts for the first 90% of the development time. The remaining 10% of the code accounts for the other 90% of the development time. - Tom Cargill >
 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use oxc to improve the quality of JavaScript and TypeScript code reviews.

Add a configuration file to your project to customize how CodeRabbit runs oxc.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLAUDE_API_KEY in .env never reaches the Claude subprocess (solo path) — auth fails in Docker, silently falls back to subscription on hosts

1 participant