fix: normalize Claude CLI auth mode to oauth (#855)

Thanks @sebslight.

Co-authored-by: Sebastian <sebslight@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-15 02:28:48 +00:00
parent 3c51290e0d
commit 2fb2035dbf
7 changed files with 137 additions and 6 deletions

View File

@@ -10,7 +10,7 @@
### Fixes
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
## 2026.1.14

View File

@@ -154,4 +154,81 @@ describe("resolveAuthProfileOrder", () => {
});
expect(order).toEqual(["anthropic:work", "anthropic:default"]);
});
it("mode: oauth config accepts both oauth and token credentials (issue #559)", () => {
const now = Date.now();
const storeWithBothTypes: AuthProfileStore = {
version: 1,
profiles: {
"anthropic:oauth-cred": {
type: "oauth",
provider: "anthropic",
access: "access-token",
refresh: "refresh-token",
expires: now + 60_000,
},
"anthropic:token-cred": {
type: "token",
provider: "anthropic",
token: "just-a-token",
expires: now + 60_000,
},
},
};
const orderOauthCred = resolveAuthProfileOrder({
store: storeWithBothTypes,
provider: "anthropic",
cfg: {
auth: {
profiles: {
"anthropic:oauth-cred": { provider: "anthropic", mode: "oauth" },
},
},
},
});
expect(orderOauthCred).toContain("anthropic:oauth-cred");
const orderTokenCred = resolveAuthProfileOrder({
store: storeWithBothTypes,
provider: "anthropic",
cfg: {
auth: {
profiles: {
"anthropic:token-cred": { provider: "anthropic", mode: "oauth" },
},
},
},
});
expect(orderTokenCred).toContain("anthropic:token-cred");
});
it("mode: token config rejects oauth credentials (issue #559 root cause)", () => {
const now = Date.now();
const storeWithOauth: AuthProfileStore = {
version: 1,
profiles: {
"anthropic:oauth-cred": {
type: "oauth",
provider: "anthropic",
access: "access-token",
refresh: "refresh-token",
expires: now + 60_000,
},
},
};
const order = resolveAuthProfileOrder({
store: storeWithOauth,
provider: "anthropic",
cfg: {
auth: {
profiles: {
"anthropic:oauth-cred": { provider: "anthropic", mode: "token" },
},
},
},
});
expect(order).not.toContain("anthropic:oauth-cred");
});
});

View File

@@ -84,7 +84,7 @@ export async function applyAuthChoiceAnthropic(
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: CLAUDE_CLI_PROFILE_ID,
provider: "anthropic",
mode: "token",
mode: "oauth",
});
return { config: nextConfig };
}
@@ -146,7 +146,7 @@ export async function applyAuthChoiceAnthropic(
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: CLAUDE_CLI_PROFILE_ID,
provider: "anthropic",
mode: "token",
mode: "oauth",
});
return { config: nextConfig };
}

View File

@@ -91,12 +91,12 @@ export async function modelsAuthSetupTokenCommand(
applyAuthProfileConfig(cfg, {
profileId: CLAUDE_CLI_PROFILE_ID,
provider: "anthropic",
mode: "token",
mode: "oauth",
}),
);
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log(`Auth profile: ${CLAUDE_CLI_PROFILE_ID} (anthropic/token)`);
runtime.log(`Auth profile: ${CLAUDE_CLI_PROFILE_ID} (anthropic/oauth)`);
}
export async function modelsAuthPasteTokenCommand(

View File

@@ -270,7 +270,7 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyAuthProfileConfig(nextConfig, {
profileId: CLAUDE_CLI_PROFILE_ID,
provider: "anthropic",
mode: "token",
mode: "oauth",
});
}

View File

@@ -200,6 +200,46 @@ describe("legacy config detection", () => {
}
});
});
it("auto-migrates claude-cli auth profile mode to oauth", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify(
{
auth: {
profiles: {
"anthropic:claude-cli": { provider: "anthropic", mode: "token" },
},
},
},
null,
2,
),
"utf-8",
);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
vi.resetModules();
try {
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth");
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
auth?: { profiles?: Record<string, { mode?: string }> };
};
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth");
expect(
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
).toBe(true);
} finally {
warnSpy.mockRestore();
}
});
});
it("auto-migrates legacy provider sections on load and writes back", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");

View File

@@ -10,6 +10,20 @@ import {
} from "./legacy.shared.js";
export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
{
id: "auth.anthropic-claude-cli-mode-oauth",
describe: "Switch anthropic:claude-cli auth profile mode to oauth",
apply: (raw, changes) => {
const auth = getRecord(raw.auth);
const profiles = getRecord(auth?.profiles);
if (!profiles) return;
const claudeCli = getRecord(profiles["anthropic:claude-cli"]);
if (!claudeCli) return;
if (claudeCli.mode !== "token") return;
claudeCli.mode = "oauth";
changes.push('Updated auth.profiles["anthropic:claude-cli"].mode → "oauth".');
},
},
{
id: "agent.defaults-v2",
describe: "Move agent config to agents.defaults and tools",