Repository navigation
Description
Gap: claude-code provider does not perform the Claude Code binary's OAuth renewal
Provider: packages/ai/src/providers/claude-code-auth.ts (anomalyco#6117 native wire emulation)
What the binary does
The Claude Code CLI renews its claude.ai OAuth login itself: when the access token in ~/.claude/.credentials.json approaches/passes expiry, the CLI exchanges the stored refreshToken at Anthropic's OAuth token endpoint (POST https://console.anthropic.com/v1/oauth/token, grant_type=refresh_token, the Claude Code OAuth client identity) and writes the rotated credential back to the same file. Fleet-observed cadence: the file's expiresAt advances in ~8h steps as the CLI runs (e.g. 2026-11-06: 08:19 CST → 09:19 CST after CLI use).
What the emulation does
Read-only by deliberate design. claude-code-auth.ts lines 34–40 (header) state it plainly:
"The credential is read IN PLACE, fresh per request, never copied into the fork's credential store, never written back, and never logged. This provider deliberately does NOT refresh the token: the refresh token is the CLI's, a refresh here would rotate it out from under the CLI, and renewal of an expired login is the interactive
claudesign-in. An expired credential therefore fails LOUDLY … instead of silently degrading."
parseCredentialsFile does not even read refreshToken out of the blob (only accessToken + expiresAt); an expired file credential falls through to CLAUDE_CODE_OAUTH_TOKEN if set (#1752), else fails with expiredMessage ("this provider does not refresh the CLI's login. Run claude…").
Why this is a gap vs the binary
- The provider's liveness silently depends on the real CLI being run interactively often enough to keep the shared file fresh. A laptop where nobody runs
claudefor >8h has a dead claude-code provider even though a valid refresh token sits in the same file. - The in-repo precedent solves the identical problem safely:
antigravity-auth.tsperforms the CLI's own refresh exchange in memory only, noting Google's client does not rotate the refresh token, "so refreshing here never invalidates the CLI's stored credential". Anthropic's OAuth behavior for the Claude Code client needs the same determination: if the token endpoint does NOT rotate the refresh token (or the rotated pair can be written back to the credentials file atomically, mirroring the CLI), the emulation can renew without racing the CLI. - Quota side already half-knows:
packages/ai/src/quota/providers/claude.tsparsesrefreshTokenout of the same blob (line 79) but never uses it; on 401/403 it reports "Claude session expired. Open Claude Code to sign in again."
Suggested shape (mirrors antigravity-auth)
- Parse
refreshTokeninparseCredentialsFile. - On expired (or near-expiry) file credential, attempt ONE in-memory refresh against the OAuth token endpoint with the Claude Code client identity; use the returned access token for the request; never log tokens.
- Decide write-back vs memory-only from Anthropic's actual rotation behavior (probe: refresh once, then check whether the CLI still functions with the file's original refresh token). If rotation invalidates the file's token, write the rotated pair back to
~/.claude/.credentials.jsonatomically (0600 preserved) exactly as the CLI would. - Keep the fail-loud behavior when refresh itself fails.
Evidence gathered 2026-11-06 (fleet laptop)
- Credential file:
claudeAiOauth, scopes includeuser:inference+user:sessions:claude_code, refresh token present,expiresAtobserved advancing after real-CLI runs. - The provider's inference wire shape is otherwise verified live (200s from
api.anthropic.com/v1/messagesin the provider's exact shape); renewal is the one lifecycle piece of the binary the emulation does not cover.
Acceptance criteria
- The provider's behavior when the CLI credential passes expiry with no live CLI renewal is explicit and observable: either the emulation performs the refresh-token exchange itself (with the rotation hazard addressed in writing), or the failure surfaces as a named, actionable error (expired credential, run the CLI to renew) instead of an opaque auth failure.
- The read-in-place, never-logged credential discipline is preserved by any change.
- Coverage exists for the expired-credential path.
Activity
dezren39 commentedon Nov 6, 2026
🤖 Triage evidence · issue-sweep-rotating (Make feeder)
22c0a6ddbee19878e8e51bde432e995f75ccdd2a(verified x2 viagh api), readingpackages/ai/src/providers/claude-code-auth.tsandpackages/ai/src/quota/providers/claude.tsraw viagh apicontents.claude-code-auth.ts:35-40— header states it plainly: the credential is read in place, never written back, and "this provider deliberately does NOT refresh the token"claude-code-auth.ts:76-94—parseCredentialsFiledestructures onlyaccessToken+expiresAt(:86); the blob'srefreshTokenis never readclaude-code-auth.ts:53-55/:144-146— an expired file credential fails loudly withexpiredMessage("this provider does not refresh the CLI's login. Runclaude…"), falling back only toCLAUDE_CODE_OAUTH_TOKENwhen setquota/providers/claude.ts:79— the quota side already parsesrefreshTokenout of the same blob but never uses it for renewalexpiresAtcadence per the issue's fleet observation) to keep~/.claude/.credentials.jsonfresh, even though a valid refresh token sits in the same file.antigravity-auth.ts's in-memory refresh precedent: parserefreshToken, attempt ONE in-memory refresh on an expired/near-expiry file credential, never log tokens, and keep the fail-loud behavior when refresh itself fails.repo-task/v1triage packet intorepo-health-opencode-pilotfrom this feeder run; the queue lane owns the probe and the fix work.dezren39 commentedon Nov 6, 2026
🤖 Agent note · queued by triage feeder
repo-task/v1triage packet as task 10990 in queuerepo-health-opencode-pilot(kindtriage, priority 40, deduperepo-health:v1:triage:developing-today/opencode:issue:1818). Packet:hidden_files/packet-opencode-1818.json.22c0a6ddbee1(verified x2): headerclaude-code-auth.ts:35-40states the provider deliberately does not refresh;parseCredentialsFile(:86) never readsrefreshToken; expired credentials fail loudly at:144-146.dezren39 commentedon Nov 6, 2026
Intake review
🤖 Fleet monitor intake (repo-fleet-monitor), 2026-11-06.
dezren39 commentedon Nov 6, 2026
Triage complete — queue task 10990 (lane
opencode-a), verified on origin/main 22c0a6d.Current behavior (fresh file:line)
packages/ai/src/providers/claude-code-auth.tsheader (~:35–40): the provider deliberately does NOT refresh the CLI's token — "a refresh here would rotate it out from under the CLI" — and an expired credential fails loudly by design.parseCredentialsFiledestructures onlyaccessToken+expiresAt; the file'srefreshToken(andrefreshTokenExpiresAt) are never read.CLAUDE_CODE_OAUTH_TOKENis set, else the loudexpiredMessagefailure. No renewal anywhere.packages/ai/src/quota/providers/claude.ts:79parsesrefreshTokenfrom the same blob and never uses it.Rotation determination — LIVE PROBE (the verdict this triage owed)
Probe environment: fleet laptop, Claude Code 2.1.306, live
~/.claude/.credentials.json(0600; access token ~4.7h from expiry, refresh token present). Endpoint and client id taken from the installed CLI binary itself:POST https://platform.claude.com/v1/oauth/token, client_id9d1c250a-e61b-44d9-88ed-5944d1962f5e, form-encodedgrant_type=refresh_token(the endpoint rejects JSON with 400 "Invalid request format"; the response is snake_case).refresh_token.invalid_grant("Refresh token not found or invalid").Verdict: Anthropic rotates the Claude Code client's refresh token on every refresh — refresh tokens are single-use. The CLI does not still function on the file's original token after a provider-side refresh. This confirms the header's assertion and kills the memory-only route: the antigravity precedent does not transfer, because Google does not rotate that client's refresh token (antigravity-auth.ts header) and Anthropic demonstrably does.
Probe side effect, disclosed: the probe's first scripted refresh succeeded server-side but its rotated result was not written back (a field-case bug in the probe script), so the probe host's stored login was burned — its CLI now reports "OAuth session expired and could not be refreshed" and awaits an interactive
claudesign-in by Drew. That accident is itself the strongest design evidence for the fix: a refresh whose rotated result is dropped burns the login, so refresh and write-back must be one atomic operation with no discard path.Route decided
Renewal = refresh + immediate atomic write-back, in this order, every time:
refreshToken(+refreshTokenExpiresAt) inparseCredentialsFile.Fix routed
Fix packet enqueued as task 10996 (dedupe
repo-health:v1:fix:developing-today/opencode:issue:1818). Routing note: the enqueue landed in the defaultrepo-health-code-shadowqueue rather thanrepo-health-opencode-pilot(the queue flag was applied only on the retry, which dedupe-matched the first task); the packet itself is the correct opencode packet and the task has already been claimed there. No duplicate was created.13 remaining items
dezren39 commentedon Nov 8, 2026
🤖 Agent note · parked — accidental cross-queue claim returned
ec118cad-0fd3-4652-91c4-27432f2f2efdrepo-queue claimwithout--queue; response showed this task belongs torepo-health-code-shadow, outside this lane’srepo-health-code-pilotscoperepo-health-opencode-pilotremains the canonical implementation route; no duplicate PR or fix was created.dezren39 commentedon Nov 8, 2026
@/tmp/note.md
fix(ai): renew the claude-code OAuth credential with an atomic write-…