Skip to content

Commit 75fff82

Browse files
committed
fix(auth): show claude-cli oauth reauth guidance
1 parent 2c90b14 commit 75fff82

3 files changed

Lines changed: 50 additions & 6 deletions

File tree

src/agents/auth-profiles/oauth-refresh-failure.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,11 @@ describe("oauth refresh failure hints", () => {
9696
"Failed to authenticate. API Error: 401 Invalid authentication credentials",
9797
),
9898
).toEqual({
99-
provider: null,
99+
provider: "claude-cli",
100100
reason: null,
101101
});
102+
expect(buildOAuthRefreshFailureLoginCommand("claude-cli")).toBe(
103+
"claude auth login && openclaw models auth login --provider anthropic --method cli --set-default",
104+
);
102105
});
103106
});

src/agents/auth-profiles/oauth-refresh-failure.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,22 @@ function isOAuthRefreshFailureMessage(message: string): boolean {
4545
lower.includes("oauth token refresh failed") ||
4646
lower.includes("access token could not be refreshed") ||
4747
lower.includes("authentication session could not be refreshed automatically") ||
48-
// Claude CLI subprocess emits its own 401 message when its stored OAuth
49-
// token expires. OpenClaw strips ANTHROPIC_API_KEY before spawning it, so
50-
// a 401 here is unambiguously an OAuth-expiry signal — recognize it so the
51-
// re-auth hint reaches channel replies instead of the generic failure text.
52-
(lower.includes("failed to authenticate") && lower.includes("401"))
48+
isClaudeCliOAuth401Message(lower)
49+
);
50+
}
51+
52+
function isClaudeCliOAuth401Message(lowercaseMessage: string): boolean {
53+
return (
54+
lowercaseMessage.includes("failed to authenticate") &&
55+
lowercaseMessage.includes("api error") &&
56+
/\b401\b/u.test(lowercaseMessage)
5357
);
5458
}
5559

5660
function extractOAuthRefreshFailureProvider(message: string): string | null {
61+
if (isClaudeCliOAuth401Message(message.toLowerCase())) {
62+
return "claude-cli";
63+
}
5764
const provider = message.match(OAUTH_REFRESH_FAILURE_PROVIDER_RE)?.[1]?.trim();
5865
return provider && provider.length > 0 ? provider : null;
5966
}
@@ -151,6 +158,14 @@ export function buildOAuthRefreshFailureLoginCommand(
151158
options?: { profileId?: string | null },
152159
): string {
153160
const sanitizedProvider = sanitizeOAuthRefreshFailureProvider(provider);
161+
if (sanitizedProvider === "claude-cli") {
162+
return [
163+
"claude auth login",
164+
formatCliCommand(
165+
"openclaw models auth login --provider anthropic --method cli --set-default",
166+
),
167+
].join(" && ");
168+
}
154169
const sanitizedProfileId = sanitizeOAuthRefreshFailureProfileId(options?.profileId);
155170
return sanitizedProvider
156171
? formatCliCommand(

src/auto-reply/reply/agent-runner-execution.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7537,6 +7537,32 @@ describe("runAgentTurnWithFallback", () => {
75377537
}
75387538
});
75397539

7540+
it("surfaces Claude CLI OAuth 401 failures with claude-cli reauth guidance", async () => {
7541+
state.runEmbeddedAgentMock.mockRejectedValueOnce(
7542+
new FailoverError(
7543+
"Failed to authenticate. API Error: 401 Invalid authentication credentials",
7544+
{
7545+
reason: "auth",
7546+
provider: "claude-cli",
7547+
model: "claude-sonnet-4-6",
7548+
status: 401,
7549+
},
7550+
),
7551+
);
7552+
7553+
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
7554+
const result = await runAgentTurnWithFallback(createMinimalRunAgentTurnParams());
7555+
7556+
expect(result.kind).toBe("final");
7557+
if (result.kind === "final") {
7558+
expect(result.payload.text).toBe(
7559+
"⚠️ Model login failed on the gateway for claude-cli. Please try again. If this keeps happening, re-auth with `claude auth login && openclaw models auth login --provider anthropic --method cli --set-default` in a terminal.",
7560+
);
7561+
expect(result.payload.text).not.toBe(PROVIDER_AUTHENTICATION_ERROR_USER_MESSAGE);
7562+
expect(result.payload.text).not.toBe(GENERIC_RUN_FAILURE_TEXT);
7563+
}
7564+
});
7565+
75407566
it("surfaces direct provider auth guidance for missing API keys", async () => {
75417567
state.runEmbeddedAgentMock.mockRejectedValueOnce(
75427568
new Error(

0 commit comments

Comments
 (0)