From 5a93447294054e621644792a78fcaa12b25b5958 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 15:50:25 +0100 Subject: [PATCH] fix: prevent claude-cli oauth downgrade (#654) (thanks @radek-paclt) --- CHANGELOG.md | 2 +- src/agents/auth-profiles.test.ts | 53 ++++++++++++++++++++++++++++++++ src/agents/auth-profiles.ts | 5 +++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d71c43bf..4604a2a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ - Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj. - Build: Docker build cache layer (#605) — thanks @zknicker. -- Auth: enable OAuth token refresh for Claude CLI credentials (`anthropic:claude-cli`) with bidirectional sync back to Claude Code storage (file on Linux/Windows, Keychain on macOS). This allows long-running agents to operate autonomously without manual re-authentication. +- Auth: enable OAuth token refresh for Claude CLI credentials (`anthropic:claude-cli`) with bidirectional sync back to Claude Code storage (file on Linux/Windows, Keychain on macOS). This allows long-running agents to operate autonomously without manual re-authentication (#654 — thanks @radek-paclt). ## 2026.1.8 diff --git a/src/agents/auth-profiles.test.ts b/src/agents/auth-profiles.test.ts index b2ecc2b7b..a330f8a61 100644 --- a/src/agents/auth-profiles.test.ts +++ b/src/agents/auth-profiles.test.ts @@ -946,6 +946,59 @@ describe("external CLI credential sync", () => { } }); + it("does not downgrade store oauth to token when CLI lacks refresh token", async () => { + const agentDir = fs.mkdtempSync( + path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-"), + ); + try { + await withTempHome( + async (tempHome) => { + const claudeDir = path.join(tempHome, ".claude"); + fs.mkdirSync(claudeDir, { recursive: true }); + // CLI has token-only credentials (no refresh token) + fs.writeFileSync( + path.join(claudeDir, ".credentials.json"), + JSON.stringify({ + claudeAiOauth: { + accessToken: "cli-token-access", + expiresAt: Date.now() + 30 * 60 * 1000, + }, + }), + ); + + const authPath = path.join(agentDir, "auth-profiles.json"); + // Store already has OAuth credentials with refresh token + fs.writeFileSync( + authPath, + JSON.stringify({ + version: 1, + profiles: { + [CLAUDE_CLI_PROFILE_ID]: { + type: "oauth", + provider: "anthropic", + access: "store-oauth-access", + refresh: "store-refresh", + expires: Date.now() + 60 * 60 * 1000, + }, + }, + }), + ); + + const store = ensureAuthProfileStore(agentDir); + // Keep oauth to preserve auto-refresh capability + const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; + expect(cliProfile.type).toBe("oauth"); + expect((cliProfile as { access: string }).access).toBe( + "store-oauth-access", + ); + }, + { prefix: "clawdbot-home-" }, + ); + } finally { + fs.rmSync(agentDir, { recursive: true, force: true }); + } + }); + it("updates codex-cli profile when Codex CLI refresh token changes", async () => { const agentDir = fs.mkdtempSync( path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"), diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index 6c62f5782..cf6cd40a4 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -716,6 +716,11 @@ function syncExternalCliCredentials( } } + // Avoid downgrading from oauth to token-only credentials. + if (existing?.type === "oauth" && claudeCreds.type === "token") { + shouldUpdate = false; + } + if (shouldUpdate && !isEqual) { store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds; mutated = true;