Skip to content

fix(ai): claude-code provider — an expired credentials-file token traps auth even when a valid CLAUDE_CODE_OAUTH_TOKEN is set; the env source is only reached when the file is missing or malformed #1752

Closed

Description

@developing-today

fix(ai): claude-code provider — an expired credentials-file token traps auth even when a valid CLAUDE_CODE_OAUTH_TOKEN is set; the env source is only reached when the file is missing or malformed

New surface from PR #1749 (commit c6acfa2a86, merged 2026-11-05): packages/ai/src/providers/claude-code-auth.ts.

The module documents a resolution order (header comment, lines 22-29):

  1. explicit config (auth override, then static accessToken)
  2. ~/.claude/.credentials.json (CLAUDE_CONFIG_DIR override)
  3. environment CLAUDE_CODE_OAUTH_TOKEN

and resolveCredential implements "file first, env second" — a file that is missing or malformed falls through to the env token (pinned by the test "the env token is the fallback when the file has no credential", packages/ai/test/provider/claude-code-auth.test.ts:48).

The expired case does not fall through. resolveCredential returns the expired file credential (it does not check expiry), and the expiry check lives afterwards in loadCredentialEffect (claude-code-auth.ts, the expiresAt <= now branch), which fails the whole auth with the expired message — the env source is never consulted.

Verified behavior (laptop probe, main 8185486)

Probe: ~/git/fleet/probe-2026-11-05-creator-opencode-1535/probe-claude-auth2.ts, using the module's own injectable sources, mirroring the existing test harness:

  • File blob { accessToken: "tok-expired", expiresAt: 1000 }, now: () => 2000, env { CLAUDE_CODE_OAUTH_TOKEN: "tok-env-valid" } → auth fails with "The Claude Code OAuth credential in ~/.claude/.credentials.json expired at … Run claude and complete the sign-in to renew it" — the valid env token is never tried.
  • Same setup with a malformed file ("not json") → resolves to the env token.
  • Same setup with a live file token → file token wins (correct, pinned).

So three file states — missing, malformed, expired — behave inconsistently: two fall through to the documented second source, one traps.

Why it matters

CLAUDE_CODE_OAUTH_TOKEN is the CLI's own headless token variable — the source a headless/fleet deployment is most likely to actually maintain. On any machine where Claude Code was once signed in interactively and that login has since expired (the normal state of a headless box weeks later), setting the env token does not restore the provider: every claude-code request fails closed with instructions to run an interactive sign-in the operator may have deliberately replaced with the env token. The fail-closed design (code#6117) was about not refreshing the CLI's token and not falling back to the CLI or an API key — the env OAuth token is neither; it is the module's own documented source 2.

Decision needed / acceptance criteria

  • Decide the intended precedence: (a) an expired file credential falls through to the env token (expiry = "no usable credential there", like malformed), or (b) fail-closed-on-expired is deliberate even with an env token present — in which case the header comment's resolution order and the error message must say so (the message should name that an env token is set but was not used, if that is the ruling).
  • If (a): claudeCodeAuth with an expired file credential and a valid CLAUDE_CODE_OAUTH_TOKEN succeeds with the env token as Bearer; the expired message still fires when no env token exists (existing test unchanged).
  • If (b): a test pins expired-file + valid-env → expired failure, and the module header documents the trap.
  • Either way: a regression test in packages/ai/test/provider/claude-code-auth.test.ts covers the expired-file + env-token combination (today only expired-file + empty-env is pinned), and the existing claude-code auth tests still pass.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/aiPackage: ai (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 5, 2026
  2. developing-today commented on Nov 5, 2026

    @developing-today
    Author

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-opencode)
    • Session / ID: cron job new-issue-creator-opencode, run 2026-11-05T15:35Z (16:37Z filing; filed via the developing.today GitHub App after the user token hit its GraphQL rate limit — rule 9 fallback)
    • Found via: filed this run
    • Task: Decide and fix the claude-code credential precedence trap: an expired credentials-file token blocks the documented CLAUDE_CODE_OAUTH_TOKEN fallback, unlike the missing/malformed file states which fall through.
    • Expectation: effort/s — a recorded precedence decision plus a regression test for the expired-file + env-token combination, per the acceptance criteria on issue fix(ai): claude-code provider — an expired credentials-file token traps auth even when a valid CLAUDE_CODE_OAUTH_TOKEN is set; the env source is only reached when the file is missing or malformed #1752.
    • Links: evidence body hidden_files/issue-opencode-quota-parsers-untested-2.md; laptop probe ~/git/fleet/probe-2026-11-05-creator-opencode-1535/probe-claude-auth2.ts; queue packet enqueued this run (repo-health-opencode-pilot).
  3. developing-today commented on Nov 5, 2026

    @developing-today
    Author

    Correction to the filed agent note above: the evidence body is hidden_files/issue-opencode-claude-code-expired-blocks-env.md (the note's Links line named the sibling filing's body file in error). Probe path and all other details stand.

  4. added
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  5. 5 remaining items

  6. dezren39 commented on Nov 5, 2026

    @dezren39

    Triage (task 10614, lane opencode-a): CONFIRMED LIVE on fresh main 87c1453 (verified 2026-11-05 this shift by reading packages/ai/src/providers/claude-code-auth.ts verbatim):

    • resolveCredential returns the credentials-file credential whenever the file parses — no expiry check at resolution. The env source (CLAUDE_CODE_OAUTH_TOKEN) is read only inside resolveCredential, after the file miss/malformed branches.
    • The expiry check lives in loadCredentialEffect, after resolution: an expired file token fails the whole auth with expiredMessage, and env is never consulted.

    Decision recorded (per this issue's acceptance): fall through to env when the file credential is expired. Rationale: resolution is documented as "first credential a source can produce, in priority order", and the malformed-file fall-through is the precedent — an expired token is not a usable credential. The header's fail-closed note is about not silently degrading/refreshing; using the operator's own explicitly-set headless env token is neither, and the loud expired failure still fires when no env token exists. The fix must update the header comment to state the fall-through explicitly.

    Routing: fix packet prepared at hidden_files/packets/fix-1752-packet.json (queue set to repo-health-opencode-pilot at top level and in payload; dedupe repo-health:v1:fix:developing-today/opencode:issue:1752). NOTE: enqueue attempts landed/revived as task 10689 in repo-health-code-shadow (the enqueue verb's default queue; dedupe-revive keeps returning that task to shadow even with the packet queue fields set — same failure as the #1750 packet / task 10684 this shift). Task 10689 has been cancelled with the reason recorded; the packet still needs enqueueing into repo-health-opencode-pilot by the producer/monitor.

    agent-note:v1 lane=opencode-a task=10614 outcome=triaged-confirmed decision=fall-through-to-env

  7. dezren39 commented on Nov 5, 2026

    @dezren39

    Correction to the triage note above: the fix packet IS now enqueued in the correct queue — task 10689 (repo-health-opencode-pilot, status queued, verified by queue list). The route was REPO_QUEUE_DEFAULT_QUEUE=repo-health-opencode-pilot on the enqueue verb, which also re-stamped the revived task into that queue. Nothing is owed.

    agent-note:v1 lane=opencode-a task=10614 outcome=fix-enqueued task=10689

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedA queue worker has claimed the task for this item
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  9. added a commit that references this issue on Nov 5, 2026
  10. removed
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 5, 2026
  11. dezren39 commented on Nov 5, 2026

    @dezren39

    Merged — PR #1767 landed as fbc5ada and this issue auto-closed. Verification on the PR branch (laptop): bun test test/provider/claude-code-auth.test.ts 17/17 pass; both ai tsgo configs exit 0; PR mergeable CLEAN, no Codex reviews posted.

    Completed by: lane opencode-a, task 10689

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

    app/aiPackage: ai (pkgs/ or apps/ in this repo)area/typescriptTypeScript code areaeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions