把 Codex CLI 的登录态"搬"到一台新服务器
场景:你在一台老机器上早就登录好了 Codex CLI,现在开了台新服务器、装好了 codex,但它没登录。你不想在新机上重新走一遍 OAuth 网页授权(有时候服务器上根本打不开浏览器),只想把老机器上那份"已经登录好的身份"复制过去。
这篇讲的就是这个搬运动作的完整方法论——为什么能搬、怎么搬、有哪些坑。命令里所有隐私都用占位符,照着换成你自己的即可。
一、先理解一件事:Codex 的登录就是一个文件
这是整个操作的地基。Codex CLI(ChatGPT OAuth 登录模式下)的登录状态,不在什么系统钥匙串里,也不在环境变量里,就是家目录下一个单独的 JSON 文件:
~/.codex/auth.json
它长这样(字段名是真的,值我打码了):
{
"auth_mode": "chatgpt",
"OPENAI_API_KEY": null,
"tokens": {
"access_token": "<很长的 access token>",
"id_token": "<id token>",
"refresh_token": "<refresh token>",
"account_id": "<你的账号 UUID>"
},
"last_refresh": "2026-01-01T00:00:00Z"
}
关键认知:
- 登录 = 这个文件。 把它原样复制到另一台机器的同一路径,那台机器就"变成了已登录状态",不需要再授权。
access_token是短命的(通常一小时左右),但没关系——只要refresh_token有效,codex 一运行就会自动拿它换新的 access_token,并把新值写回这个文件。所以哪怕你搬过去的是一份"放了十天的旧文件",只要 refresh_token 没失效,照样能用。account_id是你账号的稳定标识,搬运前后必须一致——它是你核对"搬对了没"的锚点。OPENAI_API_KEY在 ChatGPT 登录模式下是null,用不到,别去动它。
一句话:搬登录 = 把这一个文件从 A 机复制到 B 机的 ~/.codex/auth.json,收好权限。就这么简单,剩下都是细节。
二、动手前的准备
你需要三样东西:
- 能连上老机器(源)——你本来就在用它,
ssh 老机器能进就行。 - 能连上新机器(目标)——云服务器一般给你一个
.pem私钥文件。 - 知道新机器的登录用户名——这是最常被忽略的一步,下一节专门讲。
三、第一个坑:私钥权限 + 用户名靠试
3.1 私钥权限必须收紧
云平台给你下载的 .pem,权限往往是 644(别人可读)。SSH 会直接拒绝使用权限过松的私钥,报 UNPROTECTED PRIVATE KEY FILE。先收紧:
chmod 600 你的私钥.pem
3.2 用户名不知道?挨个试
云服务器的默认用户名五花八门,取决于镜像:
| 镜像 | 常见用户名 |
|---|---|
| Ubuntu 官方 | ubuntu |
| 腾讯云轻量 | lighthouse / root |
| Amazon Linux | ec2-user |
| CentOS | centos / root |
| 很多国内云默认 | root |
不确定就写个循环挨个试,谁能进就是谁(BatchMode=yes 让它连不上就立刻失败,不卡在密码提示上):
KEY=你的私钥.pem
IP=新服务器IP
for u in root ubuntu lighthouse ec2-user centos; do
echo "=== 试 $u ==="
timeout 15 ssh -i "$KEY" -o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=10 -o BatchMode=yes ${u}@${IP} 'whoami && echo OK' 2>&1 | tail -2
done
哪一行打出了用户名和 OK,那就是你的登录用户。
四、搬运前:两边都体检一遍
别急着复制,先确认源是好的、目标是干净的。
看源(老机器)的登录——只看关键字段,不把 token 打到屏幕上(养成习惯,token 别出现在终端历史里):
ssh 老机器 'python3 -c "
import json
d = json.load(open(\"$HOME/.codex/auth.json\"))
print(\"account_id:\", d[\"tokens\"][\"account_id\"])
print(\"last_refresh:\", d[\"last_refresh\"])
print(\"auth_mode:\", d[\"auth_mode\"])
"'
看目标(新机器)——codex 装了吗?~/.codex/ 目录有吗?已经有 auth.json 了吗(有的话待会要备份)?
ssh -i "$KEY" root@${IP} '
which codex; codex --version
ls -la ~/.codex/ 2>/dev/null || echo "无 ~/.codex 目录"
ls -la ~/.codex/auth.json 2>/dev/null || echo "无 auth.json(干净)"
'
注意:
~/.codex/目录通常在你第一次运行codex时就自动建好了(里面会有一些 sqlite 状态文件),但auth.json只有登录后才出现。如果连目录都没有,先在新机上随便跑一下codex让它把目录建出来,或者手动mkdir -p ~/.codex。
五、核心动作:三步搬运
原则:本地机器只做中转,凭证落地即清,全程不把 token 打印出来。
KEY=你的私钥.pem
IP=新服务器IP
TMP=$(mktemp) # 本地临时中转文件
# 1) 从老机器把 auth.json 拉到本地临时文件
ssh 老机器 'cat ~/.codex/auth.json' > "$TMP"
python3 -c "import json; json.load(open('$TMP')); print('JSON 合法')" # 传输完整性自检
# 2) 目标若已有旧 auth,先备份(可回滚);干净则跳过
ssh -i "$KEY" root@${IP} '
[ -f ~/.codex/auth.json ] && cp -p ~/.codex/auth.json ~/.codex/auth.json.bak.$(date +%s) \
&& echo "已备份旧 auth" || echo "无旧 auth,无需备份"
'
# 3) 写入新机器。umask 077 保证落地就是 600 权限,不给别人留一秒的可读窗口
ssh -i "$KEY" root@${IP} 'umask 077; cat > ~/.codex/auth.json' < "$TMP"
写完立刻收权限 + 校验搬对了没(比对 account_id 和源一致):
ssh -i "$KEY" root@${IP} '
chmod 600 ~/.codex/auth.json
ls -la ~/.codex/auth.json
python3 -c "import json; d=json.load(open(\"$HOME/.codex/auth.json\")); print(\"account_id:\", d[\"tokens\"][\"account_id\"])"
'
看到 account_id 和第四节源那边打出来的一模一样,就是搬对了。
最后销毁本地临时文件(它含活 token,不能留):
shred -u "$TMP" 2>/dev/null || rm -f "$TMP"
六、验证:用只读命令,别乱触发刷新
Codex 有个专门查登录状态的子命令,它只读本地文件、不发起网络刷新,是最安全的验证方式:
ssh -i "$KEY" root@${IP} 'codex login status'
打出 Logged in using ChatGPT 就成了。
小技巧:验证前后各看一次
last_refresh字段,如果值没变,说明这条命令确实没触发 token 刷新——纯读,不影响任何东西。这在下一节的"共享凭证"场景里很重要。
七、最大的坑:两台机器共用一份凭证,会互相"顶下线"
这是你必须知道的一件事,否则某天会莫名其妙被登出。
OAuth 的 refresh_token 在很多实现里是**"一次性轮换"**的:A 机用它换了新 token,服务端就可能把这个旧 refresh_token 作废,发一个新的给 A。这时候还揣着旧 refresh_token 的 B 机,下次想刷新就会失败——被顶下线了。
所以"把一份登录复制到两台机器同时用",本质是让两台机器抢同一个 refresh_token,谁最后刷新谁活着,另一台迟早掉线。
应对:
- 能接受偶尔重登:直接共用,掉线了就把上面的搬运流程再跑一遍(把当前活着那台的 auth.json 重新搬给掉线那台),一分钟的事。
- 想彻底避免:给新机器单独登一个账号(在新机上跑一次完整的
codex login走授权),两台各揣各的凭证,互不干扰。这是长期多机的正解。
判断你掉没掉线:codex login status 报未登录,或运行时报 refresh token was revoked——就是被顶了,重搬即可。
八、安全纪律(handle 活 token 的基本素养)
这份文件里是能直接用你账号的活凭证,当现金对待:
- 不 echo、不打印 token 到终端——查信息只
python3抽字段名,别cat整个文件。 - 本地中转文件用完立刻
shred -u——别让它躺在/tmp里过夜。 - 私钥
.pem同理——如果你为了操作复制过副本,用完销毁副本,原件收在 600 权限的地方。 - 别把 auth.json / .pem 提交进 git——
.gitignore里加上*auth*.json、*.pem、*.credentials.json。 - 权限永远 600——只有属主可读写。
九、出问题了查这张表
| 症状 | 原因 | 解法 |
|---|---|---|
UNPROTECTED PRIVATE KEY FILE |
.pem 权限太松 | chmod 600 你的.pem |
所有用户名都 Permission denied |
用户名没试对 / key 不匹配这台机 | 换用户名循环再试;确认 pem 是这台机的 |
codex login status 说未登录 |
auth.json 没到位 / 路径不对 / 权限不对 | 确认写的是 ~/.codex/auth.json、chmod 600、JSON 合法 |
| 能查状态但一运行就报 refresh 失败 | refresh_token 已失效(过期或被别的机器顶了) | 拿当前活着那台的 auth.json 重新搬;或新机单独重登 |
新机没有 ~/.codex/ 目录 |
codex 从没在这台机跑过 | 先跑一次 codex 让它建目录,或 mkdir -p ~/.codex |
| 搬完 account_id 和源不一样 | 搬串了文件 / 源本身不是你以为的账号 | 重新从正确的源拉,核对 account_id |
十、一句话总结
Codex 的登录就是 ~/.codex/auth.json 一个文件。搬登录 = 把它从老机器原样复制到新机器同一路径、权限收成 600、核对 account_id 一致、codex login status 验一下。 唯一要长记的是第七节那个共享凭证会互相顶下线的坑——真要长期多机,各登各的账号最省心。
陕公网安备61011302002223号