fix: read codex keychain credentials

This commit is contained in:
Peter Steinberger
2026-01-11 23:38:59 +00:00
parent 3a8bfc0a5d
commit 1f95d7fc8b
2 changed files with 129 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
import { execSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
@@ -11,7 +12,7 @@ import { resolveUserPath } from "../utils.js";
const log = createSubsystemLogger("agents/auth-profiles");
const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json";
const CODEX_CLI_AUTH_RELATIVE_PATH = ".codex/auth.json";
const CODEX_CLI_AUTH_FILENAME = "auth.json";
const CLAUDE_CLI_KEYCHAIN_SERVICE = "Claude Code-credentials";
const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code";
@@ -67,7 +68,70 @@ function resolveClaudeCliCredentialsPath(homeDir?: string) {
}
function resolveCodexCliAuthPath() {
return path.join(resolveUserPath("~"), CODEX_CLI_AUTH_RELATIVE_PATH);
return path.join(resolveCodexHomePath(), CODEX_CLI_AUTH_FILENAME);
}
function resolveCodexHomePath() {
const configured = process.env.CODEX_HOME;
const home = configured
? resolveUserPath(configured)
: resolveUserPath("~/.codex");
try {
return fs.realpathSync.native(home);
} catch {
return home;
}
}
function computeCodexKeychainAccount(codexHome: string) {
const hash = createHash("sha256").update(codexHome).digest("hex");
return `cli|${hash.slice(0, 16)}`;
}
function readCodexKeychainCredentials(): CodexCliCredential | null {
if (process.platform !== "darwin") return null;
const codexHome = resolveCodexHomePath();
const account = computeCodexKeychainAccount(codexHome);
try {
const secret = execSync(
`security find-generic-password -s "Codex Auth" -a "${account}" -w`,
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
).trim();
const parsed = JSON.parse(secret) as Record<string, unknown>;
const tokens = parsed.tokens as Record<string, unknown> | undefined;
const accessToken = tokens?.access_token;
const refreshToken = tokens?.refresh_token;
if (typeof accessToken !== "string" || !accessToken) return null;
if (typeof refreshToken !== "string" || !refreshToken) return null;
// No explicit expiry stored; treat as fresh for an hour from last_refresh or now.
const lastRefreshRaw = parsed.last_refresh;
const lastRefresh =
typeof lastRefreshRaw === "string" || typeof lastRefreshRaw === "number"
? new Date(lastRefreshRaw).getTime()
: Date.now();
const expires = Number.isFinite(lastRefresh)
? lastRefresh + 60 * 60 * 1000
: Date.now() + 60 * 60 * 1000;
log.info("read codex credentials from keychain", {
source: "keychain",
expires: new Date(expires).toISOString(),
});
return {
type: "oauth",
provider: "openai-codex" as OAuthProvider,
access: accessToken,
refresh: refreshToken,
expires,
};
} catch {
return null;
}
}
function readClaudeCliKeychainCredentials(): ClaudeCliCredential | null {
@@ -290,6 +354,9 @@ export function writeClaudeCliCredentials(
}
export function readCodexCliCredentials(): CodexCliCredential | null {
const keychain = readCodexKeychainCredentials();
if (keychain) return keychain;
const authPath = resolveCodexCliAuthPath();
const raw = loadJsonFile(authPath);
if (!raw || typeof raw !== "object") return null;