diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d1da8300..c9a60007f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,7 @@
- Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes.
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
- Auth/Doctor: migrate Anthropic OAuth configs from `anthropic:default` → `anthropic:` and surface a doctor hint on refresh failures. Thanks @RandyVentures for PR #361. (#363)
+- Auth: delete legacy `auth.json` after migration to prevent stale OAuth token overwrites. Thanks @reeltimeapps for PR #368.
- Gateway/CLI: stop forcing localhost URL in remote mode so remote gateway config works. Thanks @oswalpalash for PR #293.
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
diff --git a/README.md b/README.md
index 4baf9aee5..83e8620f3 100644
--- a/README.md
+++ b/README.md
@@ -453,5 +453,6 @@ Thanks to all clawtributors:
-
-
+
+
+
diff --git a/src/agents/auth-profiles.test.ts b/src/agents/auth-profiles.test.ts
index d9bfe938c..bf52a1931 100644
--- a/src/agents/auth-profiles.test.ts
+++ b/src/agents/auth-profiles.test.ts
@@ -1,8 +1,13 @@
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
import { describe, expect, it } from "vitest";
import {
type AuthProfileStore,
calculateAuthProfileCooldownMs,
+ ensureAuthProfileStore,
resolveAuthProfileOrder,
} from "./auth-profiles.js";
@@ -280,6 +285,51 @@ describe("resolveAuthProfileOrder", () => {
});
});
+describe("ensureAuthProfileStore", () => {
+ it("migrates legacy auth.json and deletes it (PR #368)", () => {
+ const agentDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), "clawdbot-auth-profiles-"),
+ );
+ try {
+ const legacyPath = path.join(agentDir, "auth.json");
+ fs.writeFileSync(
+ legacyPath,
+ `${JSON.stringify(
+ {
+ anthropic: {
+ type: "oauth",
+ provider: "anthropic",
+ access: "access-token",
+ refresh: "refresh-token",
+ expires: Date.now() + 60_000,
+ },
+ },
+ null,
+ 2,
+ )}\n`,
+ "utf8",
+ );
+
+ const store = ensureAuthProfileStore(agentDir);
+ expect(store.profiles["anthropic:default"]).toMatchObject({
+ type: "oauth",
+ provider: "anthropic",
+ });
+
+ const migratedPath = path.join(agentDir, "auth-profiles.json");
+ expect(fs.existsSync(migratedPath)).toBe(true);
+ expect(fs.existsSync(legacyPath)).toBe(false);
+
+ // idempotent
+ const store2 = ensureAuthProfileStore(agentDir);
+ expect(store2.profiles["anthropic:default"]).toBeDefined();
+ expect(fs.existsSync(legacyPath)).toBe(false);
+ } finally {
+ fs.rmSync(agentDir, { recursive: true, force: true });
+ }
+ });
+});
+
describe("auth profile cooldowns", () => {
it("applies exponential backoff with a 1h cap", () => {
expect(calculateAuthProfileCooldownMs(1)).toBe(60_000);
diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts
index 0eeda809f..20400693a 100644
--- a/src/agents/auth-profiles.ts
+++ b/src/agents/auth-profiles.ts
@@ -11,6 +11,7 @@ import lockfile from "proper-lockfile";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveOAuthPath } from "../config/paths.js";
import type { AuthProfileConfig } from "../config/types.js";
+import { createSubsystemLogger } from "../logging.js";
import { resolveUserPath } from "../utils.js";
import { resolveClawdbotAgentDir } from "./agent-paths.js";
import { normalizeProviderId } from "./model-selection.js";
@@ -29,6 +30,8 @@ const AUTH_STORE_LOCK_OPTIONS = {
stale: 30_000,
} as const;
+const log = createSubsystemLogger("agents/auth-profiles");
+
export type ApiKeyCredential = {
type: "api_key";
provider: string;
@@ -350,14 +353,20 @@ export function ensureAuthProfileStore(agentDir?: string): AuthProfileStore {
saveJsonFile(authPath, store);
}
- // Delete legacy auth.json after successful migration to prevent stale tokens
- // from being re-migrated and overwriting fresh credentials (fixes #363)
- if (legacy !== null) {
+ // PR #368: legacy auth.json could get re-migrated from other agent dirs,
+ // overwriting fresh OAuth creds with stale tokens (fixes #363). Delete only
+ // after we've successfully written auth-profiles.json.
+ if (shouldWrite && legacy !== null) {
const legacyPath = resolveLegacyAuthStorePath(agentDir);
try {
fs.unlinkSync(legacyPath);
- } catch {
- // Ignore if already deleted or permission issues
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") {
+ log.warn("failed to delete legacy auth.json after migration", {
+ err,
+ legacyPath,
+ });
+ }
}
}