From 604de866efcac0eb4a02085f1fe6d8ca74879c85 Mon Sep 17 00:00:00 2001 From: Guitaraholic Date: Wed, 2 Sep 2026 14:14:33 +0100 Subject: [PATCH] Decode hex-encoded Claude Code Keychain credentials. Claude Code 2.1.x stores the Keychain item as hex JSON. Parsing it as UTF-8 JSON failed and skipped ~/.claude/.credentials.json, so usage limits reported unauthenticated while the CLI was logged in. --- .../src/bridge/provider-maintenance.test.ts | 21 +++++++++++ .../src/bridge/provider-maintenance.ts | 37 ++++++++++++++----- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/plugins/provider-claude-code/src/bridge/provider-maintenance.test.ts b/plugins/provider-claude-code/src/bridge/provider-maintenance.test.ts index 058b552154..8c41838af1 100644 --- a/plugins/provider-claude-code/src/bridge/provider-maintenance.test.ts +++ b/plugins/provider-claude-code/src/bridge/provider-maintenance.test.ts @@ -80,4 +80,25 @@ describe("Claude Code provider maintenance", () => { "https://claude.ai/install.sh", ); }); + + it("decodes hex-encoded Claude Code keychain credentials", () => { + const credentials = { + claudeAiOauth: { + accessToken: "sk-ant-test", + expiresAt: 1_788_350_959_477, + subscriptionType: "team", + rateLimitTier: "default_claude_max_5x", + }, + }; + const hex = Buffer.from(JSON.stringify(credentials), "utf8").toString( + "hex", + ); + expect(__testing.parseClaudeCredentials(hex)).toEqual( + credentials.claudeAiOauth, + ); + expect( + __testing.parseClaudeCredentials(JSON.stringify(credentials)), + ).toEqual(credentials.claudeAiOauth); + expect(__testing.parseClaudeCredentials("not-credentials")).toBeNull(); + }); }); diff --git a/plugins/provider-claude-code/src/bridge/provider-maintenance.ts b/plugins/provider-claude-code/src/bridge/provider-maintenance.ts index 1686e5cf49..3e87634696 100644 --- a/plugins/provider-claude-code/src/bridge/provider-maintenance.ts +++ b/plugins/provider-claude-code/src/bridge/provider-maintenance.ts @@ -257,21 +257,39 @@ async function readKeychainCredentials(): Promise { return null; } -async function readCredentials(): Promise { - let raw = await readKeychainCredentials(); - if (raw === null) { +function decodeCredentialBlob(raw: string): unknown { + try { + return JSON.parse(raw); + } catch { + if (raw.length === 0 || raw.length % 2 !== 0 || /[^0-9a-f]/i.test(raw)) { + return null; + } try { - raw = await fs.readFile( - path.join(os.homedir(), ".claude", ".credentials.json"), - "utf8", - ); + return JSON.parse(Buffer.from(raw, "hex").toString("utf8")); } catch { return null; } } +} + +function parseClaudeCredentials(raw: string): ClaudeCredentials | null { + const parsed = claudeCredentialsSchema.safeParse(decodeCredentialBlob(raw)); + return parsed.success ? parsed.data.claudeAiOauth : null; +} + +async function readCredentials(): Promise { + const keychain = await readKeychainCredentials(); + if (keychain !== null) { + const fromKeychain = parseClaudeCredentials(keychain); + if (fromKeychain !== null) return fromKeychain; + } try { - const parsed = claudeCredentialsSchema.safeParse(JSON.parse(raw)); - return parsed.success ? parsed.data.claudeAiOauth : null; + return parseClaudeCredentials( + await fs.readFile( + path.join(os.homedir(), ".claude", ".credentials.json"), + "utf8", + ), + ); } catch { return null; } @@ -524,4 +542,5 @@ export async function getClaudeProviderUsage(): Promise { export const __testing = { buildProviderInstallationRun: buildClaudeProviderInstallationRun, normalizeUsage, + parseClaudeCredentials, };