Auth 信息同步与管理
CC 用 macOS Keychain + 明文 fallback 存 OAuth;Codex 用 auth.json(可选 keyring);harness 只读解析即可拿到账号身份与套餐
Auth 信息同步与管理
结论
两家 CLI 都是「OAuth 订阅 + API Key 双轨」,但存储策略相反:Claude Code 在 macOS 上默认进 Keychain(service 名 Claude Code-credentials),明文 ~/.claude/.credentials.json 只是 fallback;Codex 则默认明文 ~/.codex/auth.json(0600),keyring 是可选模式。两者的 OAuth token 都自带刷新机制(CC:过期前 5 分钟缓冲;Codex:过期前 5 分钟或距上次刷新超 8 天),harness 不应代为刷新——只读解析文件/Keychain 即可拿到登录态、邮箱、套餐(CC 的 subscriptionType/rateLimitTier,Codex 的 id_token JWT claims),这正是 Yoda 的做法。
研究问题
- 凭证存哪里?文件、Keychain 还是环境变量?格式是什么?
- 订阅 OAuth 与 API Key 双轨并存时,哪个生效?优先级链是什么?
- token 何时刷新、由谁刷新?外部 harness 读取的安全边界在哪?
各 Agent 设计与实现
Claude Code
CC 源码为社区重建版(
src_2026-03-31,落后线上约 2 个月),以下仅作架构描述与短引用。
存储后端:getSecureStorage() 在 darwin 返回 createFallbackStorage(macOsKeychainStorage, plainTextStorage),其余平台直接明文(Linux libsecret 仍是 TODO)[一手源码 utils/secureStorage/index.ts:9-16]。
- Keychain 条目:generic password,service 名 =
Claude Code+ OAuth 环境后缀 +-credentials+ 非默认CLAUDE_CONFIG_DIR的 sha256 前 8 位 [一手源码utils/secureStorage/macOsKeychainHelpers.ts:27-41]。写入时把 JSON 转 hex 经security -istdin 传入,避免进程监控(CrowdStrike 等)看到明文 payload [一手源码utils/secureStorage/macOsKeychainStorage.ts:111-118]。 - 明文 fallback:
~/.claude/.credentials.json,写后chmodSync(storagePath, 0o600),并返回warning: 'Storing credentials in plaintext.'[一手源码utils/secureStorage/plainTextStorage.ts:57-65]。 - Keychain 读取有 TTL 缓存 + stale-while-error:刷新失败时继续供给旧值,避免一次
securityspawn 失败导致全局 "Not logged in" [一手源码macOsKeychainStorage.ts:50-63]。SSH 会话下 Keychain 锁定(exit code 36)有专门检测 [同文件:211-231]。
OAuth 结构:存储数据里的 claudeAiOauth 字段含 accessToken / refreshToken / expiresAt / scopes / subscriptionType 等 [一手源码 utils/auth.ts:1215-1220]。OAuth 端点:授权走 claude.com/cai/oauth/authorize,换 token 走 https://platform.claude.com/v1/oauth/token,client_id 固定 9d1c250a-... [一手源码 constants/oauth.ts:84-104]。订阅 scope 为 user:inference + user:profile 等;Console 流程额外有 org:create_api_key(用 OAuth 换一把长期 API Key)[一手源码 constants/oauth.ts:33-51]。
刷新:isOAuthTokenExpired() 用 5 分钟缓冲判断过期 [一手源码 services/oauth/client.ts:344-353];checkAndRefreshOAuthTokenIfNeededImpl 带跨进程磁盘变更检测(invalidateOAuthCacheIfDiskChanged)、最多 5 次重试、401 时 force 刷新 [一手源码 utils/auth.ts:1447-1460]。
双轨与优先级(官方文档与源码一致)[一手文档 claude-code-docs/docs/authentication.md:131-141,一手源码 utils/auth.ts:153-206]:
- 云厂商凭证(
CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY) ANTHROPIC_AUTH_TOKEN(Bearer 头,gateway 场景)ANTHROPIC_API_KEY(X-Api-Key头,交互模式需用户确认一次)apiKeyHelper脚本输出(默认 5 分钟或 401 时重新调用,TTL 可由CLAUDE_CODE_API_KEY_HELPER_TTL_MS调整)CLAUDE_CODE_OAUTH_TOKEN(claude setup-token生成的一年期 token,CI 用)/login写入的订阅 OAuth 凭证(Pro/Max/Team/Enterprise 默认)
关键开关:任一外部 key 源存在(Bedrock/Vertex/Foundry、ANTHROPIC_AUTH_TOKEN、外部 ANTHROPIC_API_KEY、apiKeyHelper)都会令 isAnthropicAuthEnabled() 返回 false,整体关闭一方 OAuth [一手源码 utils/auth.ts:100-149]。
账号画像:~/.claude.json 的 oauthAccount 存 emailAddress / displayName / organizationName / billingType / seatTier 等非敏感身份信息(本机实测字段名一致),订阅档位 subscriptionType 由 OAuth profile 接口按 organization_type(claude_max/claude_pro/claude_team/claude_enterprise)映射 [一手源码 services/oauth/client.ts:355-385]。
Codex CLI
存储:$CODEX_HOME/auth.json(默认 ~/.codex/auth.json),结构 AuthDotJson:auth_mode / OPENAI_API_KEY / tokens / last_refresh / agent_identity / personal_access_token [一手源码 codex-rs/login/src/auth/storage.rs:33-51]。Unix 写入强制 mode(0o600) [同文件 :148-151]。
tokens 为 TokenData:id_token(JWT,反序列化时就地解析 claims)、access_token(JWT)、refresh_token、account_id [一手源码 login/src/token_data.rs:11-25]。id_token claims 含 email、https://api.openai.com/auth 下的 chatgpt_plan_type(free/plus/pro/business/enterprise/edu)、chatgpt_account_id、chatgpt_account_is_fedramp [同文件 :29-42, 71-99]。
四种存储模式(AuthCredentialsStoreMode):File(默认)/ Keyring(service 名 Codex Auth,key 为 cli| + CODEX_HOME 路径 sha256 前 16 位)/ Auto(keyring 优先、文件兜底,keyring 写成功后删除文件)/ Ephemeral(纯内存)[一手源码 config/src/types.rs:89-99,login/src/auth/storage.rs:163-358]。
AuthMode 五态:ApiKey / Chatgpt / ChatgptAuthTokens(外部宿主注入、仅内存、宿主自己刷新)/ AgentIdentity / PersonalAccessToken [一手源码 app-server-protocol/src/protocol/common.rs:21-33]。
刷新:should_refresh_proactively 两条件任一触发——access_token 的 exp 在 5 分钟内到期(CHATGPT_ACCESS_TOKEN_REFRESH_WINDOW_MINUTES = 5),或 last_refresh 距今超 8 天(TOKEN_REFRESH_INTERVAL = 8)[一手源码 login/src/auth/manager.rs:93-94, 1912-1934]。刷新端点 https://auth.openai.com/oauth/token,grant_type: "refresh_token";失败按 body 分类为 refresh_token_expired / reused / invalidated,均为永久失败、要求重新登录 [同文件 :103, 894-947]。
登录流程:codex login 起本地 server 默认端口 1455,PKCE,issuer https://auth.openai.com,回调 http://localhost:1455/auth/callback [一手源码 login/src/server.rs:54-55, 156]。
环境变量旁路:CODEX_API_KEY(按调用方开关 enable_codex_api_key_env 门控,优先级最高)与 CODEX_ACCESS_TOKEN(直接注入 access token)[一手源码 manager.rs:516-518, 805-835]。
差异矩阵
| 维度 | Claude Code | Codex CLI |
|---|---|---|
| 默认存储 | macOS Keychain(明文 fallback);Linux/Windows 明文 | ~/.codex/auth.json 明文(0600) |
| 可选存储 | 无配置项(平台决定) | File / Keyring / Auto / Ephemeral 四模式 |
| 明文文件 | ~/.claude/.credentials.json(0600) | ~/.codex/auth.json(0600) |
| Keychain 条目 | service Claude Code-credentials | service Codex Auth,key cli|<hash16> |
| OAuth issuer | platform.claude.com / claude.com | auth.openai.com |
| 本地回调端口 | 动态(auth-code-listener) | 默认 1455 |
| 刷新条件 | 过期前 5 min 缓冲 | 过期前 5 min 或距上次刷新 > 8 天 |
| 套餐信息位置 | claudeAiOauth.subscriptionType + profile 接口 | id_token JWT 的 chatgpt_plan_type claim |
| API Key 旁路 | ANTHROPIC_API_KEY / apiKeyHelper / ANTHROPIC_AUTH_TOKEN | auth.json 的 OPENAI_API_KEY 字段 / CODEX_API_KEY env |
| 外部宿主注入 | CLAUDE_CODE_OAUTH_TOKEN(env / FD) | ChatgptAuthTokens 模式(内存、宿主刷新) |
| 账号画像文件 | ~/.claude.json oauthAccount | auth.json tokens.id_token(JWT 解码) |
最小复现
# Codex:查看 auth.json 结构(只看 key,不打印值)
jq 'keys' ~/.codex/auth.json
# => ["OPENAI_API_KEY","auth_mode","last_refresh","tokens"]
jq '.tokens | keys' ~/.codex/auth.json
# => ["access_token","account_id","id_token","refresh_token"]
ls -l ~/.codex/auth.json # => -rw-------(0600)
# CC(macOS):OAuth 在 Keychain,明文文件可能只剩 mcpOAuth
security find-generic-password -s "Claude Code-credentials" | grep svce
# => svce"<blob>="Claude Code-credentials"
jq 'keys' ~/.claude/.credentials.json
# 本机实测 => ["mcpOAuth"](主凭证已迁入 Keychain)
jq '.oauthAccount | keys' ~/.claude.json
# => ["emailAddress","displayName","organizationName","billingType","seatTier",...]Harness 接入建议(Yoda 实践)
Yoda 的 subscription-account-service 是「只读探测」范本 [一手源码 yoda/src/main/core/settings/subscription-account-service.ts]:
- CC:登录态与邮箱读
~/.claude.json的oauthAccount(:104-129);套餐先读~/.claude/.credentials.json,文件不存在再security find-generic-password -s "Claude Code-credentials" -w,取claudeAiOauth.subscriptionType+rateLimitTier正则提取倍率(如default_claude_max_5x→ "Max 5x")(:61-101)。 - Codex:读
~/.codex/auth.json,本地 base64url 解码tokens.id_token的 payload(不验签——只做展示,不做信任决策),取email和chatgpt_plan_type(:132-152)。 - Keychain 读取会弹授权框:Yoda 给 CC 套餐读取加了 10 分钟 TTL 缓存(
CLAUDE_PLAN_CACHE_TTL_MS),避免反复弹窗(:10-11)。 - 边界:harness 永远不要写这些文件、不要代为刷新 token(CC/Codex 都有跨进程失效检测,外部写入会触发竞态);不要把 token 值带出主进程或写日志。需要以特定身份起会话时,用各 CLI 的官方注入口(CC:
CLAUDE_CODE_OAUTH_TOKEN;Codex:CODEX_API_KEY/ app-server 的ChatgptAuthTokens),而不是伪造凭证文件。
失效条件
- CC 给 Linux 加上 libsecret 支持(源码中标注 TODO),
.credentials.json路径结论需复验 - Codex 把
Auto/Keyring设为默认存储模式——届时auth.json可能不存在,Yoda 的文件读取路径失效 - CC Keychain service 命名规则变化(当前含 OAuth 环境后缀与 CLAUDE_CONFIG_DIR hash)
- 两家刷新窗口常量(5 min / 8 天)或刷新端点变更
-
chatgpt_plan_type/subscriptionType取值集合扩充(Codex 源码注释明示 "values may vary by backend")
参考资料
- CC 官方认证文档:
claude-code-docs/docs/authentication.md(凭证存储、优先级链、setup-token) - CC 重建源码:
src_2026-03-31/utils/secureStorage/*、utils/auth.ts、constants/oauth.ts、services/oauth/client.ts - Codex 源码:
codex-rs/login/src/auth/{storage.rs,manager.rs}、login/src/token_data.rs、login/src/server.rs、config/src/types.rs - Codex 认证文档指针:
codex/docs/authentication.md→ https://developers.openai.com/codex/auth - Yoda:
src/main/core/settings/subscription-account-service.ts