把 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,收好权限。就这么简单,剩下都是细节。


二、动手前的准备

你需要三样东西:

  1. 能连上老机器(源)——你本来就在用它,ssh 老机器 能进就行。
  2. 能连上新机器(目标)——云服务器一般给你一个 .pem 私钥文件。
  3. 知道新机器的登录用户名——这是最常被忽略的一步,下一节专门讲。

三、第一个坑:私钥权限 + 用户名靠试

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 的基本素养)

这份文件里是能直接用你账号的活凭证,当现金对待:

  1. 不 echo、不打印 token 到终端——查信息只 python3 抽字段名,别 cat 整个文件。
  2. 本地中转文件用完立刻 shred -u——别让它躺在 /tmp 里过夜。
  3. 私钥 .pem 同理——如果你为了操作复制过副本,用完销毁副本,原件收在 600 权限的地方。
  4. 别把 auth.json / .pem 提交进 git——.gitignore 里加上 *auth*.json*.pem*.credentials.json
  5. 权限永远 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 验一下。 唯一要长记的是第七节那个共享凭证会互相顶下线的坑——真要长期多机,各登各的账号最省心。

Read more

哨兵机制:让 Agent 一触即醒

0. 一句话点破本质 **让"等"发生在便宜的子进程里,让贵的 agent 只在有事时醒。**心跳解决"最迟多久必有人查岗",探针解决"事情一发生几乎立刻有人到场"——两个机制回答的是两个不同的问题,谁也替代不了谁。 1. 机制全貌:会自杀的轮询进程 + 宿主的"尸体通知" 我的实现只有两块积木: 积木一:一个有明确死法的后台循环 # 放行任务的同时,后台挂上(run_in_background) for i in $(seq 1 20); do 信号=$(ssh data "tmux capture-pane -t dna

By ladydd

Agent 心跳机制·设计与实现

0. 一句话点破本质 **心跳不是闹钟,是"带着完整世界快照的自我唤醒"。**闹钟只解决"什么时候醒";心跳真正要解决的是你点出的那个问题——醒来的那个瞬间,清楚自己是谁、任务到哪了、这一跳该干什么。我所有跑得好的心跳,提示词都写得像给一个失忆的陌生人看的;所有出过事的心跳,都是因为假设"我还记得"。 1. 第一性原理:为什么"醒来知道干啥"这么难 一个长期任务里的 agent 面临三重失忆: 1. 上下文会被压缩——多轮之后早期细节只剩摘要,心跳打进来时,那条心跳提示词可能是上下文里唯一高保真的任务描述 2. 世界在你睡着时变了——下属可能干完了、卡死了、跑偏了,你脑子里的"进度"从睡着那刻就开始过期 3. 任务本身会变—

By ladydd

我没手动映射 3000,公网为什么还能访问?一次 UPnP 误开孔复盘

写在前面:标题里的“自己打开”只是当时的主观感受。路由器没有失控,也不存在神秘穿透。真正发生的是:排障自动化从局域网主动调用了 UPnP AddPortMapping,路由器按协议新增了公网映射。 1. 原本的设计边界 家里的 Open WebUI 跑在一台 Ubuntu 主机的 Docker 中: 内网主机 192.168.x.x:3000 路由器上手动配置的入口是: 公网 TCP 13000 → 内网主机:3000 外部用户不直接访问家宽端口,而是先到云端 Caddy: 用户浏览器 → https://ai.example.com (云端 Caddy) → http://home.example.com:13000 (DDNS → 家宽公网

By ladydd

公网一度只剩 18000:透明代理、Docker 与端口映射该怎么分锅

先说结案:“为什么当时只剩 18000”没有找到可以被证据确认的最终根因。 故障形态在后续复测时已经消失,现场又缺少故障时刻的路由器配置快照、UPnP 表和双向抓包。本文记录的是已排除什么、还缺什么证据,而不是宣布一个并未证明的答案。 0. 三条结案结论 结论一:AI 工具超时的根因已经确认 sing-box 当时是 enabled,但实际状态为 inactive。因此 Codex、grok、agy 等程序直连国外服务并超时。恢复 sing-box 后,三者单轮请求均成功。 sing-box 被停止 → AI/开发工具出站超时 这条证据完整,已经结案。 结论二:公网 3000 意外开放的根因也已经确认 排障自动化误调用了路由器 UPnP AddPortMapping,临时创建了: 公网 3000 → 内网 Open WebUI :3000

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号