那天我发现 Codex CLI 只能通过 OAuth 浏览器流程登录,而且一台机器只能有一个活跃凭据。Work 和 Personal 两个 OpenAI 账号,切来切去,还得保持跨 Mac 和 MBP 的同步。完全不优雅。
决定写个工具搞定这事。
问题背景
Codex CLI 认证的硬约束:
- 仅支持 OAuth 浏览器登录(
codex login) - 凭据全局唯一:
~/.codex/auth.json - 没有内置的多账号管理
- 没有自动 token 刷新机制(token 有 7-14 天生命周期)
需求:
- 快速切换多个 OpenAI 账号(rent / work 等)
- 自动刷新 token,无感知
- 跨机器同步(本地 Mac + 远程 MBP)
- 快照备份,30 天自动清理
解决方案架构
核心设计
~/.codex/auth.json (Codex 读取的活跃凭据) ↑↓~/.codex/auth_<name>.json (命名账号快照,source of truth) ↑↓~/.codex/.active_account (当前激活的别名) ↓~/.codex/journal/ (每次修改自动快照,30 天清理) ↓mbp:~/.codex/ (通过 scp 同步的远程副本)关键点:
auth.json和auth_<name>.json始终保持一致(switch / refresh 时同步).active_account记录当前别名,支持 refresh 时自动找到对应账号- journal 作为审计日志,可以回溯之前的任何 token 状态
Token 刷新机制
OpenAI OAuth 实现了 refresh token rotation:每次刷新后,旧 token 作废,新 token 写入。
脚本调用 https://auth.openai.com/oauth/token 端点:
curl -X POST https://auth.openai.com/oauth/token \ -H "Content-Type: application/json" \ -d "{ \"grant_type\":\"refresh_token\", \"client_id\":\"app_EMoamEEZ73f0CkXaXp7hrann\", \"refresh_token\":\"rt_bIdf6WifVig4...\" }"返回新 access_token、id_token、可能的新 refresh_token。
并发安全:因为 refresh token 单次使用,多个进程同时刷新会互相冲突。解决办法是在主机级别加锁,或者干脆不允许并发——脚本采用后者(实际场景下用户不会同时在两台机器上跑 codex)。
核心命令
codex-switch
# 列出所有账号codex-switch# 输出:# 账号列表:# rent account_id=a3194... expires 2026-04-12 (+6d) last_refresh=2026-04-02 19:24 ◀ 当前# work account_id=b5271... expires 2026-04-10 (+4d) last_refresh=2026-03-30 10:15## 活跃 auth.json:# rent account_id=a3194... expires 2026-04-12 (+6d) last_refresh=2026-04-02 19:24## Journal 快照 (最近10条):# auth_rent.20260406T174402Z.json# auth_work.20260406T143015Z.json# ... (自动清理 30 天前的)
# 创建新命名账号(从当前 auth.json)codex-switch create work -f # -f 强制覆盖
# 立即切换codex-switch work# → 自动刷新 token# → 问:同步到 mbp? [y/N]
# 新账号登录(清空凭据 → codex login → 保存 → 恢复原账号)codex-switch login personal# → 暂存当前凭据# → 触发 `codex login`(浏览器弹出)# → 登录新账号后,保存到 auth_personal.json# → 恢复原活跃凭据# → 问:同步到 mbp?# → 问:立即切换到 personal?codex-refresh
# 刷新当前账号(读 .active_account)codex-refresh# → 调 OpenAI OAuth 刷新# → auth.json + auth_<name>.json 双写# → 问:同步到 mbp?
# 指定账号刷新codex-refresh --account work
# 跳过询问,直接同步codex-refresh --sync实现细节
参数流转和同步逻辑
switch 的关键步骤(以 codex-switch work 为例):
# 1. Journal 快照当前 auth.jsonjournal_snapshot "~/.codex/auth.json"journal_gc # 清理 30+ 天的
# 2. 复制新账号到活跃位置cp ~/.codex/auth_work.json ~/.codex/auth.json
# 3. 记录当前别名echo "work" > ~/.codex/.active_account
# 4. 自动刷新(不再单独问 sync,交给 refresh 统一处理)codex-refresh --account work [--sync|--no-sync]refresh 的 token 同步:
如果指定了 account,刷新后会同时更新两个文件:
# 写入新 tokenauth['tokens']['access_token'] = response['access_token']auth['tokens']['refresh_token'] = response['refresh_token']json.dump(auth, open(auth_file, 'w'))
# 同步命名文件(保持一致)if account: cp ~/.codex/auth.json ~/.codex/auth_account.json跨机器同步:
if do_sync: for host in mbp: scp ~/.codex/auth.json ${host}:.codex/auth.json if account: scp ~/.codex/auth_account.json ${host}:.codex/auth_account.json关键:两个文件都推过去,mbp 上的 auth.json 和 auth_account.json 状态与本地一致。
为什么需要 codex-journal
Token 有生命周期,refresh_token 有 rotation。万一出问题想回滚?或者排查”昨天用的是哪个 token”?
快照方案:
# 每次修改前自动快照journal_snapshot "~/.codex/auth.json"# → ~/.codex/journal/auth.20260406T174402Z.json
# 30+ 天自动清理find ~/.codex/journal -name "*.json" -mtime +30 -delete这样既有审计日志,又不会无限膨胀磁盘(假设每个快照 ~4KB,每天最多 10 份,30 天 =1.2MB)。
问题排除与边界情况
并发 refresh
场景:在 MacBook 上切换了 work 账号,同时 MBP 也在 refresh。
现象:OpenAI API 返回 "Your access token could not be refreshed because your refresh token was already used"。
原因:refresh_token 单次使用,A 机器用掉后,B 机器同时也要用,就冲突了。
解决:
- 脚本本身是单进程的,不会自己并发
- 用户在两台机器上手动操作不会同时发生(物理上只有一个人)
- 万一真的冲突了,新的 token 会失败,但旧的 journal 快照还在,可以恢复
跨机器时差
如果本地 refresh 后还没 scp 到 mbp,而用户立即切到 MBP 运行 codex,会用旧 token。
现象:MBP 上的请求被拒(旧 token 过期)。
解决:
- 脚本会自动提示”同步到 mbp?”,用户一般会按 y
- 如果没同步,下一次
codex-refresh时会获取新 token 并推过去 - 用户可以明确跑
codex-refresh --sync强制同步
login 时凭据丢失
场景:codex-switch login newaccount 途中,codex login 卡住或失败。
流程:
mv ~/.codex/auth.json ~/.codex/auth.json.stash.$$ # 暂存codex login # 失败了# → 检查 auth.json 是否存在if [[ ! -f ~/.codex/auth.json ]]; then # 恢复原凭据 mv ~/.codex/auth.json.stash.$$ ~/.codex/auth.jsonfi原凭据永远不会丢失,最多浪费一个 stash 临时文件。
使用流程示例
首次设置
# 当前已经 `codex login` 过一次,有 auth.json(假设是 rent 账号)
# 1. 命名当前账号codex-switch create rent -f
# 2. 添加另一个账号codex-switch login work# → 浏览器弹出登录# → 登录 work 账号# → 保存为 auth_work.json,恢复 auth.json(现在还是 rent)# → "立即切换到 work?" → y# → 自动 refresh work 的 token# → "同步到 mbp?" → y
# 3. 查看现状codex-switch# → 列出 rent、work 两个账号,work 是当前激活的
# 4. 随时切换codex-switch rent# → refresh rent 的 token,自动同步到 mbp日常使用
# 需要用 work 账号跑 codexcodex-switch work # 一条命令,搞定
# 需要手动刷新(离线后重连)codex-refresh --sync
# Token 快过期了(剩 1 天),提前刷新codex-refresh相关工具生态
社区里确实有类似的方案。WebSearch 发现了几个:
现有工具:
- CCS (Claude Code Switch) — 最成熟的方案,v3.0,支持 Claude API、OAuth、其他模型(Gemini、Copilot)
- cc-switch / claude-swap / claude-code-switch — 各种社区实现
- 简单方案:用环境变量 + shell alias(
CLAUDE_CONFIG_DIR=~/.claude-work隔离凭据)
现有方案的局限:
- 都是本地多账号管理
- 凭据存 macOS Keychain(不可 SSH 传输)
- 没有跨机器同步能力
- 没有 token 快照审计
- 没有自动刷新机制(Codex 的特有需求)
所以 codex-switch 的独特之处就在这里:为了跨机器 SSH 同步而重新设计凭据存储和刷新流程。
关键洞察
为什么不用现有工具?
CCS 和其他工具都很成熟,但它们解决的问题是”本地机器上快速切换”。一旦加上”SSH 同步”这个约束,就完全变了:
- Keychain 不可 SSH 传输 — 现有工具都依赖 macOS Keychain,无法跨机器同步
- 凭据必须是文本文件 — 才能 scp,所以必须重新设计存储结构
- Token 刷新需要自动化 — OpenAI OAuth 的 token 有生命周期,需要定期刷新并自动推到远程
与其改现有工具(破坏它的设计),不如从头写一个为 SSH 同步优化的版本。
为什么 auth.json 和 auth_.json 要双写?
auth.json是 Codex 实际读的文件auth_<name>.json是你的”源”,防止失误覆盖- 两个同步意味着:无论从哪个文件操作,另一个都是最新的
用 Codex 时读 auth.json,但如果要查历史、对比账号状态,看 auth_* 们。
Token 刷新前为什么要快照?
因为 refresh_token 是旋转的,旧的作废。如果刷新失败了,旧 token 在快照里,可以手动恢复。Plus 审计日志。
交付物
三个脚本 + README,已上传 GitHub Gist:
https://gist.github.com/StevenLi-phoenix/015548b4e92630139992136459df5e4d
安装:
curl -fsSL https://gist.github.com/StevenLi-phoenix/015548b4e92630139992136459df5e4d/raw/codex-switch -o ~/.local/bin/codex-switchcurl -fsSL https://gist.github.com/StevenLi-phoenix/015548b4e92630139992136459df5e4d/raw/codex-refresh -o ~/.local/bin/codex-refreshcurl -fsSL https://gist.github.com/StevenLi-phoenix/015548b4e92630139992136459df5e4d/raw/codex-journal -o ~/.local/bin/codex-journalchmod +x ~/.local/bin/{codex-switch,codex-refresh,codex-journal}修改 SYNC_HOSTS 数组里的 mbp 为你的 SSH 别名即可。
收获
最大的收获是理解了 OAuth token rotation 的真实成本。单次使用 = 并发不友好。但这种设计确实更安全:一旦 token 泄露,黑客只能用一次,下一次刷新就无法再用了。
另一个是无感跨机器同步有多重要。写这工具之前每次切账号都得 scp 文件。现在一条命令搞定,包括 sync。人机交互就是这样,少一步询问能显著提升体验。
最后是 Bash 脚本依然强力。1000 行代码搞定的事,你用 Python 可能得 2000 行。Shell 就是好。