Yoda
参考Agent 设计指南模型与账户

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 -i stdin 传入,避免进程监控(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:刷新失败时继续供给旧值,避免一次 security spawn 失败导致全局 "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]:

  1. 云厂商凭证(CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY
  2. ANTHROPIC_AUTH_TOKEN(Bearer 头,gateway 场景)
  3. ANTHROPIC_API_KEYX-Api-Key 头,交互模式需用户确认一次)
  4. apiKeyHelper 脚本输出(默认 5 分钟或 401 时重新调用,TTL 可由 CLAUDE_CODE_API_KEY_HELPER_TTL_MS 调整)
  5. CLAUDE_CODE_OAUTH_TOKENclaude setup-token 生成的一年期 token,CI 用)
  6. /login 写入的订阅 OAuth 凭证(Pro/Max/Team/Enterprise 默认)

关键开关:任一外部 key 源存在(Bedrock/Vertex/Foundry、ANTHROPIC_AUTH_TOKEN、外部 ANTHROPIC_API_KEYapiKeyHelper)都会令 isAnthropicAuthEnabled() 返回 false,整体关闭一方 OAuth [一手源码 utils/auth.ts:100-149]。

账号画像~/.claude.jsonoauthAccountemailAddress / displayName / organizationName / billingType / seatTier 等非敏感身份信息(本机实测字段名一致),订阅档位 subscriptionType 由 OAuth profile 接口按 organization_typeclaude_max/claude_pro/claude_team/claude_enterprise)映射 [一手源码 services/oauth/client.ts:355-385]。

Codex CLI

存储$CODEX_HOME/auth.json(默认 ~/.codex/auth.json),结构 AuthDotJsonauth_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]。

tokensTokenDataid_token(JWT,反序列化时就地解析 claims)、access_token(JWT)、refresh_tokenaccount_id [一手源码 login/src/token_data.rs:11-25]。id_token claims 含 emailhttps://api.openai.com/auth 下的 chatgpt_plan_type(free/plus/pro/business/enterprise/edu)、chatgpt_account_idchatgpt_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-99login/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_tokenexp 在 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/tokengrant_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 CodeCodex 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-credentialsservice Codex Auth,key cli|<hash16>
OAuth issuerplatform.claude.com / claude.comauth.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_TOKENauth.jsonOPENAI_API_KEY 字段 / CODEX_API_KEY env
外部宿主注入CLAUDE_CODE_OAUTH_TOKEN(env / FD)ChatgptAuthTokens 模式(内存、宿主刷新)
账号画像文件~/.claude.json oauthAccountauth.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.jsonoauthAccount: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(不验签——只做展示,不做信任决策),取 emailchatgpt_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.tsconstants/oauth.tsservices/oauth/client.ts
  • Codex 源码:codex-rs/login/src/auth/{storage.rs,manager.rs}login/src/token_data.rslogin/src/server.rsconfig/src/types.rs
  • Codex 认证文档指针:codex/docs/authentication.mdhttps://developers.openai.com/codex/auth
  • Yoda:src/main/core/settings/subscription-account-service.ts

On this page