From f47467620ae623c20c736db3867e6e545cc9c7fc Mon Sep 17 00:00:00 2001 From: GodsBoy Date: Fri, 30 Jan 2026 13:55:22 +0200 Subject: [PATCH] fix: show actionable hint when setup-token lacks user:profile scope When Anthropic setup-token auth is used, the OAuth usage endpoint returns 403 because setup-token only grants user:inference scope, not user:profile. This change: 1. Returns a clear, actionable error message suggesting `claude login` (full OAuth) when the scope error occurs and no web session fallback is available, instead of the generic 'HTTP 403: ...' message. 2. Adds a post-onboard note during setup-token flow warning users that usage tracking in /status requires the full OAuth flow. 3. Adds a test for the no-fallback scope error path. Refs: - https://github.com/anthropics/claude-code/issues/16075 - https://github.com/anthropics/claude-code/issues/15243 - https://github.com/anthropics/claude-code/issues/12020 --- src/commands/auth-choice.apply.anthropic.ts | 15 ++++++ src/infra/provider-usage.fetch.claude.ts | 10 ++++ src/infra/provider-usage.test.ts | 52 +++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/src/commands/auth-choice.apply.anthropic.ts b/src/commands/auth-choice.apply.anthropic.ts index b28b8ebee..adb5dde1d 100644 --- a/src/commands/auth-choice.apply.anthropic.ts +++ b/src/commands/auth-choice.apply.anthropic.ts @@ -55,6 +55,21 @@ export async function applyAuthChoiceAnthropic( provider, mode: "token", }); + + // setup-token only grants user:inference scope; usage tracking (/status) + // requires user:profile which is only available via the full OAuth flow. + // See: https://github.com/anthropics/claude-code/issues/16075 + await params.prompter.note( + [ + "Note: `claude setup-token` does not include the user:profile scope.", + "Usage tracking in /status will not be available with this token.", + "To enable usage tracking, run `claude login` (full browser OAuth)", + "on a machine with a browser, then use `openclaw models auth paste-token`", + "to import the resulting token.", + ].join("\n"), + "Usage tracking limitation", + ); + return { config: nextConfig }; } diff --git a/src/infra/provider-usage.fetch.claude.ts b/src/infra/provider-usage.fetch.claude.ts index 654962c93..db0c5e69a 100644 --- a/src/infra/provider-usage.fetch.claude.ts +++ b/src/infra/provider-usage.fetch.claude.ts @@ -135,6 +135,16 @@ export async function fetchClaudeUsage( const web = await fetchClaudeWebUsage(sessionKey, timeoutMs, fetchFn); if (web) return web; } + + // No web session key fallback available — return an actionable error so users + // know how to fix usage tracking. + return { + provider: "anthropic", + displayName: PROVIDER_LABELS.anthropic, + windows: [], + error: + "setup-token missing user:profile scope — run `claude login` (full OAuth) to enable usage tracking", + }; } const suffix = message ? `: ${message}` : ""; diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index 5bc6ba575..6535f4c8c 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -387,4 +387,56 @@ describe("provider usage loading", () => { else process.env.CLAUDE_AI_SESSION_KEY = cookieSnapshot; } }); + + it("returns actionable error when scope is missing and no web session key", async () => { + const cookieSnapshot = process.env.CLAUDE_AI_SESSION_KEY; + const webCookieSnapshot = process.env.CLAUDE_WEB_SESSION_KEY; + const webCookie2Snapshot = process.env.CLAUDE_WEB_COOKIE; + delete process.env.CLAUDE_AI_SESSION_KEY; + delete process.env.CLAUDE_WEB_SESSION_KEY; + delete process.env.CLAUDE_WEB_COOKIE; + try { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = + typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("api.anthropic.com/api/oauth/usage")) { + return makeResponse(403, { + type: "error", + error: { + type: "permission_error", + message: "OAuth token does not meet scope requirement user:profile", + }, + }); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "anthropic", token: "sk-ant-oauth-1" }], + fetch: mockFetch, + }); + + expect(summary.providers).toHaveLength(1); + const claude = summary.providers[0]; + expect(claude?.provider).toBe("anthropic"); + expect(claude?.windows).toHaveLength(0); + expect(claude?.error).toContain("setup-token missing user:profile scope"); + expect(claude?.error).toContain("claude login"); + } finally { + if (cookieSnapshot === undefined) delete process.env.CLAUDE_AI_SESSION_KEY; + else process.env.CLAUDE_AI_SESSION_KEY = cookieSnapshot; + if (webCookieSnapshot === undefined) delete process.env.CLAUDE_WEB_SESSION_KEY; + else process.env.CLAUDE_WEB_SESSION_KEY = webCookieSnapshot; + if (webCookie2Snapshot === undefined) delete process.env.CLAUDE_WEB_COOKIE; + else process.env.CLAUDE_WEB_COOKIE = webCookie2Snapshot; + } + }); });