Skip to content

fix(claude-cli): surface re-auth hint when subprocess OAuth token expires#97669

Merged
sallyom merged 12 commits into
openclaw:mainfrom
Alix-007:fix/claude-cli-oauth-expiry-detection
Jul 7, 2026
Merged

fix(claude-cli): surface re-auth hint when subprocess OAuth token expires#97669
sallyom merged 12 commits into
openclaw:mainfrom
Alix-007:fix/claude-cli-oauth-expiry-detection

Conversation

@Alix-007

@Alix-007 Alix-007 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Surfaces a targeted Claude CLI re-auth hint when the subprocess OAuth token expires and returns a 401 authentication failure.
  • Handles both message-based failures and structured FailoverError metadata where provider: "claude-cli" is not embedded in the display message.
  • Emits the correct recovery command: claude auth login && openclaw models auth login --provider anthropic --method cli.
  • Review focus: the narrowness of the Claude CLI classification and precedence over generic provider-auth copy.

Linked context

Fixes #97553.

Related: #97561 overlaps the same root cause; this branch now covers the structured FailoverError path 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-cli structured 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-detection at 775e22e9da; local claude binary 2.1.196 (Claude Code); Linux shell; isolated temporary HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME, and XDG_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:

git -C <repo> log --oneline -1
# 775e22e9da test(claude-cli): expect full cli reauth command

command -v claude
claude --version
# <npm-global>/bin/claude
# 2.1.196 (Claude Code)

tmp=$(mktemp -d "$TMPDIR/claude-proof.XXXXXX")
env -i \
  HOME="$tmp" \
  XDG_CONFIG_HOME="$tmp/.config" \
  XDG_CACHE_HOME="$tmp/.cache" \
  XDG_STATE_HOME="$tmp/.local/state" \
  PATH="$PATH" \
  TERM=dumb \
  NO_COLOR=1 \
  CI=1 \
  CLAUDE_CODE_OAUTH_TOKEN="<invalid non-secret OAuth bearer>" \
  claude -p --output-format stream-json --include-partial-messages --verbose \
    --setting-sources user --model claude-sonnet-4-6 \
    <<< 'Reply with one word: ok' \
    > "$tmp/stdout.txt" 2> "$tmp/stderr.txt"

node --import tsx - <<'NODE'
import fs from 'node:fs';
import { extractCliErrorMessage } from './src/agents/cli-output.ts';
import { classifyFailoverReason } from './src/agents/embedded-agent-helpers.ts';
import { FailoverError, resolveFailoverStatus } from './src/agents/failover-error.ts';
import {
  buildOAuthRefreshFailureLoginCommand,
  classifyOAuthRefreshFailure,
  classifyOAuthRefreshFailureError,
} from './src/agents/auth-profiles/oauth-refresh-failure.ts';
import { buildKnownAgentRunFailureReplyPayload } from './src/auto-reply/reply/agent-runner-execution.ts';

const stdout = fs.readFileSync('<tmp-home>/stdout.txt', 'utf8');
const message = extractCliErrorMessage(stdout) ?? '';
const reason = classifyFailoverReason(message, { provider: 'claude-cli' }) ?? 'unknown';
const status = resolveFailoverStatus(reason);
const err = new FailoverError(message, {
  reason,
  provider: 'claude-cli',
  model: 'claude-sonnet-4-6',
  status,
});
const oauthFromMessage = classifyOAuthRefreshFailure(message);
const oauthFromError = classifyOAuthRefreshFailureError(err);
const payload = buildKnownAgentRunFailureReplyPayload({
  err,
  sessionCtx: { Provider: 'proof' },
  resolvedVerboseLevel: 'off',
});
console.log(JSON.stringify({
  extractedMessage: message,
  failoverReason: reason,
  failoverStatus: status,
  oauthFromMessage,
  oauthFromError,
  loginCommand: buildOAuthRefreshFailureLoginCommand(oauthFromError?.provider),
  replyText: payload?.text,
  replyIsError: payload?.isError,
}, null, 2));
NODE

Evidence after fix:

The real claude subprocess returned exit status 1, 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 as FailoverError(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.json was not accepted as logged in by Claude Code 2.1.196, and OpenClaw-managed Claude CLI runs intentionally scrub CLAUDE_CODE_OAUTH_TOKEN/CLAUDE_CONFIG_DIR before 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.

$ git -C <repo> status -sb
## fix/claude-cli-oauth-expiry-detection...fork/fix/claude-cli-oauth-expiry-detection

$ git -C <repo> log --oneline -1
775e22e9da test(claude-cli): expect full cli reauth command

$ command -v claude && claude --version
<npm-global>/bin/claude
2.1.196 (Claude Code)

$ tmp=$(mktemp -d "$TMPDIR/claude-proof.XXXXXX")
$ env -i HOME="$tmp" XDG_CONFIG_HOME="$tmp/.config" XDG_CACHE_HOME="$tmp/.cache" XDG_STATE_HOME="$tmp/.local/state" PATH="$PATH" TERM=dumb NO_COLOR=1 CI=1 CLAUDE_CODE_OAUTH_TOKEN="<invalid non-secret OAuth bearer>" claude -p --output-format stream-json --include-partial-messages --verbose --setting-sources user --model claude-sonnet-4-6 <<< 'Reply with one word: ok' > "$tmp/stdout.txt" 2> "$tmp/stderr.txt"
exit status: 1

$ sed -n '1,120p' "$tmp/stdout.txt"
{"type":"system","subtype":"init","cwd":"<repo>","session_id":"<session-id>","tools":"<elided>","mcp_servers":"<elided>","model":"claude-sonnet-4-6","permissionMode":"default","slash_commands":"<elided>","apiKeySource":"none","claude_code_version":"2.1.196","output_style":"default","agents":"<elided>","skills":"<elided>","plugins":"<elided>","analytics_disabled":false,"product_feedback_disabled":true,"uuid":"<uuid>","memory_paths":{"auto":"<tmp-home>/.claude/projects/<repo-slug>/memory/"},"fast_mode_state":"off"}
{"type":"system","subtype":"status","status":"requesting","uuid":"<uuid>","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>","uuid":"<uuid>"}
{"type":"system","subtype":"api_retry","attempt":2,"max_retries":10,"retry_delay_ms":"<redacted>","error_status":401,"error":"authentication_failed","session_id":"<session-id>","uuid":"<uuid>"}
{"type":"assistant","message":{"id":"<uuid>","container":null,"model":"<synthetic>","role":"assistant","stop_details":null,"stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":null,"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":null,"iterations":null,"speed":null},"content":[{"type":"text","text":"Failed to authenticate. API Error: 401 Invalid bearer token"}],"context_management":null},"parent_tool_use_id":null,"session_id":"<session-id>","uuid":"<uuid>","error":"authentication_failed","request_id":"<request-id>"}
{"type":"result","subtype":"success","is_error":true,"api_error_status":401,"duration_ms":3530,"duration_api_ms":0,"num_turns":1,"result":"Failed to authenticate. API Error: 401 Invalid bearer token","stop_reason":"stop_sequence","session_id":"<session-id>","total_cost_usd":0,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"completed","fast_mode_state":"off","uuid":"<uuid>"}

$ wc -c "$tmp/stderr.txt"
0 <tmp-home>/stderr.txt

$ find "$tmp" -maxdepth 3 -type f | sed "s#$tmp#<tmp-home>#"
<tmp-home>/.claude.json
<tmp-home>/.claude/backups/.claude.json.backup.<timestamp>
<tmp-home>/stdout.txt
<tmp-home>/stderr.txt

$ node - <<'NODE'
const fs = require('fs');
const text = fs.readFileSync('<tmp-home>/stdout.txt', 'utf8');
for (const [i,line] of text.trim().split(/\n/).entries()) {
  const obj = JSON.parse(line);
  console.log('LINE', i + 1, obj.type, obj.subtype || '', JSON.stringify({
    status: obj.status,
    error_status: obj.error_status,
    error: obj.error,
    is_error: obj.is_error,
    api_error_status: obj.api_error_status,
    result: obj.result,
    message: obj.message?.content?.[0]?.text,
    apiKeySource: obj.apiKeySource,
    claude_code_version: obj.claude_code_version,
    authMethod: obj.authMethod,
  }));
}
NODE
LINE 1 system init {"apiKeySource":"none","claude_code_version":"2.1.196"}
LINE 2 system status {"status":"requesting"}
LINE 3 system api_retry {"error_status":401,"error":"authentication_failed"}
LINE 4 system api_retry {"error_status":401,"error":"authentication_failed"}
LINE 5 assistant  {"error":"authentication_failed","message":"Failed to authenticate. API Error: 401 Invalid bearer token"}
LINE 6 result success {"is_error":true,"api_error_status":401,"result":"Failed to authenticate. API Error: 401 Invalid bearer token"}

$ node --import tsx - <<'NODE'
import fs from 'node:fs';
import { extractCliErrorMessage } from './src/agents/cli-output.ts';
import { classifyFailoverReason } from './src/agents/embedded-agent-helpers.ts';
import { FailoverError, resolveFailoverStatus } from './src/agents/failover-error.ts';
import {
  buildOAuthRefreshFailureLoginCommand,
  classifyOAuthRefreshFailure,
  classifyOAuthRefreshFailureError,
} from './src/agents/auth-profiles/oauth-refresh-failure.ts';
import { buildKnownAgentRunFailureReplyPayload } from './src/auto-reply/reply/agent-runner-execution.ts';

const stdout = fs.readFileSync('<tmp-home>/stdout.txt', 'utf8');
const message = extractCliErrorMessage(stdout) ?? '';
const reason = classifyFailoverReason(message, { provider: 'claude-cli' }) ?? 'unknown';
const status = resolveFailoverStatus(reason);
const err = new FailoverError(message, {
  reason,
  provider: 'claude-cli',
  model: 'claude-sonnet-4-6',
  status,
});
const oauthFromMessage = classifyOAuthRefreshFailure(message);
const oauthFromError = classifyOAuthRefreshFailureError(err);
const payload = buildKnownAgentRunFailureReplyPayload({
  err,
  sessionCtx: { Provider: 'proof' },
  resolvedVerboseLevel: 'off',
});
console.log(JSON.stringify({
  stdoutPath: '<tmp-home>/stdout.txt',
  extractedMessage: message,
  failoverReason: reason,
  failoverStatus: status,
  oauthFromMessage,
  oauthFromError,
  loginCommand: buildOAuthRefreshFailureLoginCommand(oauthFromError?.provider),
  replyText: payload?.text,
  replyIsError: payload?.isError,
}, null, 2));
NODE
{
  "stdoutPath": "<tmp-home>/stdout.txt",
  "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
}

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.ts
  • New regression coverage includes the structured provider metadata path requested by ClawSweeper.

Risk 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-cli FailoverError path where provider is not embedded in message text.

AI-assisted; implementation and validation reviewed before pushing.

@Alix-007
Alix-007 requested a review from a team as a code owner June 29, 2026 05:08
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 29, 2026
@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 6, 2026, 1:07 PM ET / 17:07 UTC.

Summary
The PR adds Claude CLI OAuth 401 classification and auto-reply coverage so expired Claude CLI subprocess auth surfaces a targeted re-auth command instead of generic provider-auth copy.

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.

  • Open same-issue candidate PRs: 3 open, 2 closed unmerged. Maintainers need to choose one canonical fix path before merging to avoid duplicate auth-classifier churn.

Stored data model
Persistent data-model change detected: vector/embedding metadata: src/auto-reply/reply/agent-runner-execution.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #97553
Summary: This PR is one of several candidate fixes for the same Claude CLI OAuth-expiry re-auth hint issue.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Mantis proof suggestion
A Telegram transcript would materially prove the user-visible re-auth hint on the affected channel surface. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis telegram live proof: verify that a claude-cli OAuth 401 failure surfaces the targeted re-auth hint in the chat transcript.

Risk before merge

  • [P1] Auth-provider failure classification changes user recovery guidance for Claude CLI 401s, so maintainers should be comfortable that the matcher is narrow enough and the command is the desired recovery path.
  • [P1] There are multiple open candidate PRs for the same canonical issue, so landing more than one could create duplicate/conflicting tests or follow-up churn.

Maintainer options:

  1. Land one canonical auth fix (recommended)
    After maintainer review, merge exactly one candidate that covers message and structured Claude CLI 401 failures and close the duplicate candidate PRs.
  2. Tighten before merge
    If maintainers are worried about false positives, require the chosen branch to make the Claude CLI matcher require both status 401 and authentication-failure wording in every path.
  3. Pause duplicate branches
    If no branch is clearly preferred, pause this PR and the sibling candidates until the auth-provider owner picks the permanent classification boundary.

Next step before merge

  • [P2] Manual review is needed to select the canonical Claude CLI OAuth-expiry branch among overlapping proof-positive PRs; there is no narrow automated repair finding on this branch.

Maintainer decision needed

  • Question: Which Claude CLI OAuth-expiry candidate branch should be the canonical fix for claude-cli auth failure shows generic error instead of re-auth hint when OAuth token is expired #97553?
  • Rationale: This PR appears correct and well proven, but two other open PRs target the same bug, so automation should not decide branch ownership or close duplicates before maintainer selection.
  • Likely owner: vincentkoc — Recent current-main history points to Vincent Koc on the affected auth/reply files and related runtime cleanup, making him the best available owner for canonical branch selection.
  • Options:
    • Use this PR as canonical (recommended): Land this branch if maintainers prefer its structured FailoverError coverage, real terminal proof, and current clean mergeability, then close the overlapping PRs as superseded.
    • Use another proof-positive PR: Choose fix: surface Claude CLI OAuth reauth hints #97894 instead if maintainers prefer its implementation shape or command-formatting details, then close this PR after that branch merges.
    • Pause for auth-owner direction: Hold all overlapping branches if maintainers want a different provider-auth classification policy or recovery command before merging any fix.

Security
Cleared: The diff changes auth failure classification and tests only; it does not add dependency, CI, install, secret-handling, or supply-chain changes.

Review details

Best 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 changes

Label justifications:

  • P2: This is a normal-priority user-facing auth-provider bug fix with limited blast radius to Claude CLI OAuth failure recovery copy.
  • merge-risk: 🚨 auth-provider: The PR changes how Claude CLI auth/status 401 failures are classified and which re-auth command users are told to run.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes redacted real Claude CLI 2.1.196 terminal output for a 401 auth failure and shows OpenClaw producing the targeted re-auth reply after the patch.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted real Claude CLI 2.1.196 terminal output for a 401 auth failure and shows OpenClaw producing the targeted re-auth reply after the patch.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The changed channel reply text is visible in Telegram/chat flows and can be demonstrated with a short Telegram transcript proof.
Evidence reviewed

PR surface:

Source +88, Tests +188. Total +276 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 2 89 1 +88
Tests 2 188 0 +188
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 277 1 +276

What I checked:

Likely related people:

  • vincentkoc: Current-main blame and recent log history for both affected files point at Vincent Koc refactor/release commits in this auth/reply area, so he is a strong routing candidate for branch selection and integration review. (role: recent area contributor; confidence: medium; commits: e311943700ae, e085fa1a3ffd; files: src/agents/auth-profiles/oauth-refresh-failure.ts, src/auto-reply/reply/agent-runner-execution.ts)
  • Mariano: Commit b77db8c added the OAuth-refresh failure classifier and auto-reply hint path this PR extends. (role: introduced OAuth refresh reply behavior; confidence: high; commits: b77db8c0b610; files: src/agents/auth-profiles/oauth-refresh-failure.ts, src/auto-reply/reply/agent-runner-execution.ts)
  • steipete: History sampling shows Peter Steinberger as the heaviest contributor across the central auth, auto-reply, and CLI-backend files, with multiple Claude CLI/runtime-adjacent commits. (role: adjacent owner; confidence: medium; commits: 89d7a24a3523, d0581ca66fd2; files: src/agents/cli-backends.ts, src/agents/auth-profiles/oauth-refresh-failure.ts, src/auto-reply/reply/agent-runner-execution.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (6 earlier review cycles)
  • reviewed 2026-07-02T14:00:55.324Z sha 9351aef :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T12:56:37.869Z sha 6f380ef :: needs maintainer review before merge. :: none
  • reviewed 2026-07-06T03:41:21.772Z sha 27e7cb1 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-06T03:49:50.856Z sha 94dffb4 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-06T04:31:57.305Z sha 98d1a4f :: needs maintainer review before merge. :: none
  • reviewed 2026-07-06T06:30:12.867Z sha bb7a3b8 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. labels Jun 29, 2026
@Alix-007

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@Alix-007

Copy link
Copy Markdown
Contributor Author

Follow-up fix pushed for the failing auto-reply assertion:

  • updated the focused auto-reply test to expect the actual Claude CLI re-login command emitted by the fix: openclaw models auth login --provider anthropic --method cli
  • verified locally: node scripts/run-vitest.mjs src/agents/auth-profiles/oauth-refresh-failure.test.ts src/auto-reply/reply/agent-runner-execution.test.ts -- --run
  • verified locally: oxlint src/auto-reply/reply/agent-runner-execution.test.ts

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@Alix-007

Copy link
Copy Markdown
Contributor Author

Updated the PR body to include the required What Problem This Solves and Evidence sections, and synchronized the evidence with the current openclaw models auth login --provider anthropic --method cli hint.

@clawsweeper re-review

@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jun 29, 2026
@Alix-007

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@Alix-007

Copy link
Copy Markdown
Contributor Author

Follow-up pushed and PR body refreshed to match the contributor template.

Addressed the structured claude-cli FailoverError gap: classification now handles provider: "claude-cli", reason: "auth", status: 401 even when the display message omits the provider name, and full runner-path coverage verifies the targeted re-auth hint.

Fresh 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"
  • 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.ts

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. labels Jun 29, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jun 29, 2026
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@Alix-007

Alix-007 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Triggered a new CI run with an empty commit because I do not have permission to rerun the failed job directly. The previous Scan changed paths (precise) failure was an external opengrep install fetch failure (Failed to fetch available versions from GitHub), not a code/test failure.

@Alix-007
Alix-007 force-pushed the fix/claude-cli-oauth-expiry-detection branch 6 times, most recently from 98d1a4f to bb7a3b8 Compare July 6, 2026 06:05
Alix-007 added 12 commits July 7, 2026 00:06
…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.
@Alix-007
Alix-007 force-pushed the fix/claude-cli-oauth-expiry-detection branch from bb7a3b8 to c8ad090 Compare July 6, 2026 16:06
@sallyom sallyom self-assigned this Jul 7, 2026
@sallyom

sallyom commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Maintainer review: this is the selected canonical fix for #97553

Why (Head SHA: c8ad090):

  • Covers both the raw Claude CLI 401 message and the structured FailoverError(provider="claude-cli", reason="auth", status=401) path.
  • Keeps the Claude CLI recovery command behavior in the shared OAuth failure helper, which is the narrowest/cleanest owner-boundary fix.
  • Preferred over the other viable overlapping PRs for that implementation shape.

Readiness:

  • Full local autoreview on c8ad09013c1f9c61401ecb5661238199e7c0e584 was clean: no accepted/actionable findings.
  • CI is green: gh pr checks reports 0 attention checks.
  • No new SDK, flags, config, env vars, migrations, protocol/API surfaces, or compatibility-breaking behavior.
  • Performance impact should be negligible; the added work is limited to failure classification/reply handling.

Merge-ready from my side.

@sallyom
sallyom merged commit fa84741 into openclaw:main Jul 7, 2026
96 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 8, 2026
…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
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
…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
@Alix-007

Alix-007 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Long time no see, @sallyom . Thanks, really appreciate the careful maintainer review and for selecting this as the canonical fix for #97553.

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

Labels

agents Agent runtime and tooling mantis: telegram-visible-proof Mantis should capture Telegram visible proof. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude-cli auth failure shows generic error instead of re-auth hint when OAuth token is expired

2 participants