fix(claude-cli): surface re-auth hint when subprocess OAuth token expires#97669
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 1:07 PM ET / 17:07 UTC. Summary PR surface: Source +88, Tests +188. Total +276 across 4 files. Reproducibility: yes. source-level reproduction is high-confidence: current main does not classify the Claude CLI 401 text as an OAuth refresh failure, and the PR body includes real Claude CLI 2.1.196 401 terminal output fed through OpenClaw’s classifier and reply functions. I did not rerun a live expired-token Claude CLI session during this read-only review. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land one canonical branch that preserves existing OAuth-refresh behavior, adds Claude CLI message and structured 401 coverage, emits the correct Claude/OpenClaw re-auth command, and then resolves the duplicate candidate PRs and the linked issue. Do we have a high-confidence way to reproduce the issue? Yes, source-level reproduction is high-confidence: current main does not classify the Claude CLI 401 text as an OAuth refresh failure, and the PR body includes real Claude CLI 2.1.196 401 terminal output fed through OpenClaw’s classifier and reply functions. I did not rerun a live expired-token Claude CLI session during this read-only review. Is this the best way to solve the issue? Yes, this is a strong fix shape: it reuses the existing OAuth-refresh failure path and adds the structured FailoverError path instead of adding channel-specific copy. The remaining question is canonical branch selection among overlapping candidate PRs, not a concrete defect in this patch. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 5b06eba9fe9e. Label changesLabel justifications:
Evidence reviewedPR surface: Source +88, Tests +188. Total +276 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (6 earlier review cycles)
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Follow-up fix pushed for the failing auto-reply assertion:
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Updated the PR body to include the required @clawsweeper re-review |
|
@clawsweeper re-review |
|
Follow-up pushed and PR body refreshed to match the contributor template. Addressed the structured Fresh validation:
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Triggered a new CI run with an empty commit because I do not have permission to rerun the failed job directly. The previous |
98d1a4f to
bb7a3b8
Compare
…ires When the claude-cli subprocess emits a 401 "Failed to authenticate. API Error: 401 Invalid authentication credentials" because its stored OAuth token has expired, the error was surfaced as the generic provider-auth copy instead of the targeted re-auth hint. Two fixes: 1. Extend isOAuthRefreshFailureMessage to recognise the claude-cli subprocess 401 error shape (requires "claude-cli" prefix + 401 + auth failure text). extractOAuthRefreshFailureProvider returns "claude-cli" for this pattern; classifyOAuthRefreshFailureReason maps it to "revoked" so the "expired" copy is shown. 2. Reorder both classification sites in buildExternalRunFailureReply and the runAgentTurnWithFallback catch block so the OAuth-refresh check runs before classifyProviderRequestError. The typed 401 branch in classifyProviderRequestError would otherwise intercept the FailoverError before the OAuth hint path is ever reached. Fixes openclaw#97553
--provider claude-cli is not a registered provider id; the correct command to re-authenticate the claude subprocess is: openclaw models auth login --provider anthropic --method cli Add a special-case branch in buildOAuthRefreshFailureLoginCommand so that a 'claude-cli' sanitized provider emits the two-flag form instead of the generic --provider <id> template. Update the regression test expectation to match.
bb7a3b8 to
c8ad090
Compare
|
Maintainer review: this is the selected canonical fix for #97553 Why (Head SHA: c8ad090):
Readiness:
Merge-ready from my side. |
…ires (openclaw#97669) * fix(claude-cli): surface re-auth hint when subprocess OAuth token expires When the claude-cli subprocess emits a 401 "Failed to authenticate. API Error: 401 Invalid authentication credentials" because its stored OAuth token has expired, the error was surfaced as the generic provider-auth copy instead of the targeted re-auth hint. Two fixes: 1. Extend isOAuthRefreshFailureMessage to recognise the claude-cli subprocess 401 error shape (requires "claude-cli" prefix + 401 + auth failure text). extractOAuthRefreshFailureProvider returns "claude-cli" for this pattern; classifyOAuthRefreshFailureReason maps it to "revoked" so the "expired" copy is shown. 2. Reorder both classification sites in buildExternalRunFailureReply and the runAgentTurnWithFallback catch block so the OAuth-refresh check runs before classifyProviderRequestError. The typed 401 branch in classifyProviderRequestError would otherwise intercept the FailoverError before the OAuth hint path is ever reached. Fixes openclaw#97553 * fix(claude-cli): use correct auth login command format in re-auth hint --provider claude-cli is not a registered provider id; the correct command to re-authenticate the claude subprocess is: openclaw models auth login --provider anthropic --method cli Add a special-case branch in buildOAuthRefreshFailureLoginCommand so that a 'claude-cli' sanitized provider emits the two-flag form instead of the generic --provider <id> template. Update the regression test expectation to match. * test(claude-cli): expect CLI auth re-login command * fix(claude-cli): classify structured OAuth auth failures * test(claude-cli): add real http oauth expiry proof * test(claude-cli): add proof output to real HTTP server OAuth test * test(claude-cli): add proof console.log to real HTTP server test * test(claude-cli): keep real HTTP OAuth proof output explicit * fix(agents): include Claude CLI reauth step * test(claude-cli): expect full cli reauth command * test(claude-cli): align reauth hint with terminal wording * chore: retrigger claude-cli CI after opengrep fetch failure
…ires (openclaw#97669) * fix(claude-cli): surface re-auth hint when subprocess OAuth token expires When the claude-cli subprocess emits a 401 "Failed to authenticate. API Error: 401 Invalid authentication credentials" because its stored OAuth token has expired, the error was surfaced as the generic provider-auth copy instead of the targeted re-auth hint. Two fixes: 1. Extend isOAuthRefreshFailureMessage to recognise the claude-cli subprocess 401 error shape (requires "claude-cli" prefix + 401 + auth failure text). extractOAuthRefreshFailureProvider returns "claude-cli" for this pattern; classifyOAuthRefreshFailureReason maps it to "revoked" so the "expired" copy is shown. 2. Reorder both classification sites in buildExternalRunFailureReply and the runAgentTurnWithFallback catch block so the OAuth-refresh check runs before classifyProviderRequestError. The typed 401 branch in classifyProviderRequestError would otherwise intercept the FailoverError before the OAuth hint path is ever reached. Fixes openclaw#97553 * fix(claude-cli): use correct auth login command format in re-auth hint --provider claude-cli is not a registered provider id; the correct command to re-authenticate the claude subprocess is: openclaw models auth login --provider anthropic --method cli Add a special-case branch in buildOAuthRefreshFailureLoginCommand so that a 'claude-cli' sanitized provider emits the two-flag form instead of the generic --provider <id> template. Update the regression test expectation to match. * test(claude-cli): expect CLI auth re-login command * fix(claude-cli): classify structured OAuth auth failures * test(claude-cli): add real http oauth expiry proof * test(claude-cli): add proof output to real HTTP server OAuth test * test(claude-cli): add proof console.log to real HTTP server test * test(claude-cli): keep real HTTP OAuth proof output explicit * fix(agents): include Claude CLI reauth step * test(claude-cli): expect full cli reauth command * test(claude-cli): align reauth hint with terminal wording * chore: retrigger claude-cli CI after opengrep fetch failure
Summary
FailoverErrormetadata whereprovider: "claude-cli"is not embedded in the display message.claude auth login && openclaw models auth login --provider anthropic --method cli.Linked context
Fixes #97553.
Related: #97561 overlaps the same root cause; this branch now covers the structured
FailoverErrorpath called out by ClawSweeper.Maintainer request: not directly requested by a maintainer.
Real behavior proof (required for external PRs)
Behavior addressed: After PR #97669, a real Claude Code CLI auth/OAuth 401 envelope is classified through OpenClaw's
claude-clistructured OAuth failure path, and the reply path surfaces the targeted re-auth hint:claude auth login && openclaw models auth login --provider anthropic --method cli.Real environment tested: OpenClaw PR branch
fix/claude-cli-oauth-expiry-detectionat775e22e9da; localclaudebinary2.1.196 (Claude Code); Linux shell; isolated temporaryHOME,XDG_CONFIG_HOME,XDG_CACHE_HOME, andXDG_STATE_HOME. The Claude CLI subprocess used an invalid non-secret OAuth bearer value only to force a real Anthropic API 401. No real user~/.claude, Claude login state, API key, or OAuth secret was read, modified, or deleted.Exact steps or command run after this patch:
Evidence after fix:
The real
claudesubprocess returned exit status1, empty stderr, and stdout JSONL containing real 401/authentication failure records:{"type":"system","subtype":"init","cwd":"<repo>","session_id":"<session-id>","model":"claude-sonnet-4-6","apiKeySource":"none","claude_code_version":"2.1.196","memory_paths":{"auto":"<tmp-home>/.claude/projects/<repo-slug>/memory/"}} {"type":"system","subtype":"status","status":"requesting","session_id":"<session-id>"} {"type":"system","subtype":"api_retry","attempt":1,"max_retries":10,"retry_delay_ms":"<redacted>","error_status":401,"error":"authentication_failed","session_id":"<session-id>"} {"type":"system","subtype":"api_retry","attempt":2,"max_retries":10,"retry_delay_ms":"<redacted>","error_status":401,"error":"authentication_failed","session_id":"<session-id>"} {"type":"assistant","message":{"model":"<synthetic>","role":"assistant","content":[{"type":"text","text":"Failed to authenticate. API Error: 401 Invalid bearer token"}]},"session_id":"<session-id>","error":"authentication_failed","request_id":"<request-id>"} {"type":"result","subtype":"success","is_error":true,"api_error_status":401,"num_turns":1,"result":"Failed to authenticate. API Error: 401 Invalid bearer token","session_id":"<session-id>","total_cost_usd":0}OpenClaw's real exported functions then produced:
{ "extractedMessage": "Failed to authenticate. API Error: 401 Invalid bearer token", "failoverReason": "auth", "failoverStatus": 401, "oauthFromMessage": null, "oauthFromError": { "provider": "claude-cli", "reason": "revoked" }, "loginCommand": "claude auth login && openclaw models auth login --provider anthropic --method cli", "replyText": "⚠️ Model login expired on the gateway for claude-cli. Re-auth with `claude auth login && openclaw models auth login --provider anthropic --method cli`, then try again.", "replyIsError": true }Observed result after fix: The real Claude Code 401 stdout envelope is reduced to
Failed to authenticate. API Error: 401 Invalid bearer token; OpenClaw classifies it asFailoverError(provider=claude-cli, reason=auth, status=401), maps it to OAuth failure{ provider: "claude-cli", reason: "revoked" }, builds the full CLI re-auth command, and emits the targeted reply text instead of the generic provider-auth copy.What was not tested: I did not use or mutate a real user's stored Claude login, and I did not produce a naturally expired stored Claude OAuth session. A fabricated
<tmp-home>/.claude/.credentials.jsonwas not accepted as logged in by Claude Code2.1.196, and OpenClaw-managed Claude CLI runs intentionally scrubCLAUDE_CODE_OAUTH_TOKEN/CLAUDE_CONFIG_DIRbefore spawn, so this proof uses the accepted fallback shape: real Claude CLI 401 stdout bytes from an isolated environment, fed through OpenClaw's real classifier, command builder, and reply assembly functions. No PR body update, PR comment, review, push, or global auth mutation was performed.Redacted raw terminal output
Redaction legend:
<tmp-home>is the isolated temporary HOME;<repo>is the PR worktree;<session-id>,<uuid>,<request-id>, and retry delay values are redacted runtime identifiers;<invalid non-secret OAuth bearer>is not a real secret.Tests and validation
node scripts/run-vitest.mjs src/agents/auth-profiles/oauth-refresh-failure.test.ts src/auto-reply/reply/agent-runner-execution.test.ts -- --run -t "claude-cli"node_modules/.bin/oxlint src/agents/auth-profiles/oauth-refresh-failure.ts src/agents/auth-profiles/oauth-refresh-failure.test.ts src/auto-reply/reply/agent-runner-execution.test.tsRisk checklist
Did user-visible behavior change? (
Yes/No) Yes. A specific Claude CLI 401 now gets a targeted re-auth hint.Did config, environment, or migration behavior change? (
Yes/No) No.Did security, auth, secrets, network, or tool execution behavior change? (
Yes/No) Yes, auth failure classification and recovery guidance changed.What is the highest-risk area? Misclassifying unrelated provider 401 failures as Claude CLI OAuth expiry.
How is that risk mitigated? Structured classification requires
FailoverError,provider: "claude-cli",reason: "auth",status: 401, plus the Claude authentication failure wording in the message or raw error.Current review state
Next action: ClawSweeper/maintainer re-review after the structured metadata fix.
Waiting on: maintainer decision on overlap with #97561; this branch now addresses the gap noted by ClawSweeper.
Bot/reviewer comments addressed: handled the raw structured
claude-cliFailoverErrorpath where provider is not embedded in message text.AI-assisted; implementation and validation reviewed before pushing.