把 Codex Home 从隐藏目录搬出来,用 Git 管理 AI 工作流资产

最近开始认真考虑一个问题:Codex 里沉淀下来的东西,应该怎么长期保存?

一开始,做法很朴素:把一些重要目录打成 zip,上传到云盘。这能兜住「电脑坏了怎么办」「换设备怎么办」这类问题,但不适合长期维护:

  • 每次备份都是一个新的压缩包,不容易看差异。
  • 只知道「备份了」,但不知道具体变了什么。
  • 想回到某个历史版本,需要先下载、解压、人工比较。
  • 想在多台设备之间同步,也不够自然。

压缩包更像一个快照,不像一个持续演进的工作区。

后来又回到了一个很老但很好用的答案:Git。

Codex 里到底有哪些值得保存的东西

Codex 的本地状态通常在 ~/.codex 下面。

这个目录有点像一个 AI 工具的「用户主目录」。里面既有真正值得保存的长期资产,也有大量只属于当前机器的运行状态。

值得保存的大概是这些:

AGENTS.md
rules/
skills/
memories/MEMORY.md
memories/memory_summary.md
memories/extensions/ad_hoc/notes/

它们分别对应几类东西。

AGENTS.md 是全局行为规则。比如希望 Codex 用什么语气、怎么协作、回答时注意什么,这些都可以放在这里。

rules/ 是更细的规则文件。它们通常用于补充一些本机长期有效的执行边界。

skills/ 是最重要的一类。Skill 会把一套可复用流程固化下来,让 Codex 下次遇到类似任务时不用从零开始理解。比如文档整理流程、某类调试流程、某个工具链的操作规范,都可以变成 Skill。

memories/MEMORY.mdmemories/memory_summary.md 更像长期记忆索引。它们不应该替代真实资料,但能帮助 Codex 在新会话里快速知道:哪些经验已经沉淀过,应该去哪里找。

memories/extensions/ad_hoc/notes/ 适合放那些主动补充的长期规则或偏好。

这些内容的共同点是:它们是可读的、可迁移的、可以被人审查的。Git 很适合管理它们。

不该被 Git 管的东西

~/.codex 里也有很多东西不适合提交。

比如:

auth.json
sessions/
archived_sessions/
browser-profiles/
cache/
.tmp/
tmp/
*.sqlite
*.sqlite-shm
*.sqlite-wal
logs/
worktrees/
generated_images/
installation_id

这些东西的问题不一样。

auth.jsonbrowser-profiles/ 可能包含登录态或凭证,不能进 Git。

sessions/archived_sessions/ 体积会越来越大,也可能包含大量上下文和内部信息。它们对于「换设备后继续使用 Codex」并没有那么关键。

*.sqlitecache/.tmp/logs/ 都是运行态。它们强依赖当前机器、当前版本、当前进程状态。同步到另一台设备上,价值不大,风险不少。

这里有一个很重要的边界:Git 应该保存的是「可迁移能力」,不是「运行现场」。

第一个想法:直接把 ~/.codex 变成仓库

最直接的想法是:

cd ~/.codex
git init

然后配一个白名单 .gitignore,默认忽略所有内容,只放行明确要同步的文件。

大概像这样:

*

!.gitignore
!AGENTS.md

!rules/
!rules/**

!skills/
!skills/**

!memories/
memories/*
!memories/MEMORY.md
!memories/memory_summary.md
!memories/extensions/
memories/extensions/*
!memories/extensions/ad_hoc/
memories/extensions/ad_hoc/*
!memories/extensions/ad_hoc/notes/
!memories/extensions/ad_hoc/notes/**

这个方案能工作。

另一台设备初始化时,也可以直接:

git clone <private-repo-url> ~/.codex
codex login

如果 ~/.codex 已经存在,就先备份:

mv ~/.codex ~/.codex.bak.$(date +%F-%H%M%S)
git clone <private-repo-url> ~/.codex
codex login

这个方案的体验问题是:~/.codex 是隐藏目录,平时不太顺手。

想经常用编辑器打开它,看看 Skill 写了什么,检查某条记忆有没有过期,或者把某个 Skill 复制出来给别人参考。隐藏目录可以打开,但不如放在一个明确的代码目录里自然。

更舒服的做法:把 Codex Home 放到代码目录

更好的办法是把 Codex Home 放到一个显式目录,比如:

~/code/codex-home

然后让 ~/.codex 指向它。

mv ~/.codex ~/.codex.bak.$(date +%F-%H%M%S)
git clone <private-repo-url> ~/code/codex-home
ln -s ~/code/codex-home ~/.codex
codex login

这样有几个好处。

第一,目录变得好找。它就在日常代码目录下,可以直接用编辑器打开。

第二,Git 管理更自然。仓库根目录就是 ~/code/codex-home,不用总是意识到自己在一个隐藏目录里操作。

第三,迁移体验也更清楚。新设备上先 clone 到 ~/code/codex-home,再建立 ~/.codex 软链接即可。

第四,Codex 仍然可以继续从 ~/.codex 读取数据。对 Codex 来说路径没有变;对人来说,真实文件已经搬到了更容易观察的位置。

这其实是一个很常见的做法:让程序继续使用它熟悉的默认路径,但把真实数据放到更适合人维护的位置。

当前机器迁移时,先别急着 Git 初始化

如果是新设备初始化,上面的 git clone -> ln -s -> codex login 很自然。

但如果是当前正在使用的机器,问题就不一样了。~/.codex 正在被 Codex 读取和写入,直接在它身上搬目录、建仓库、提交、推送,会把文件迁移、工具启动、Git 初始化和远端同步的风险压到同一步。更稳的做法是把风险拆小。

更稳的顺序是:

  1. 先迁移目录,只验证 Codex 能不能正常打开。
  2. 确认稳定后,再初始化 Git、写 .gitignore、提交和推送。

这两个动作不要绑在同一个脚本里。

目录迁移失败,回滚目标应该很单纯:把 ~/.codex 恢复成原来的普通目录。Git 初始化失败,则只是版本管理没做好,不应该影响 Codex 本身能不能启动。把两件事拆开,排障会清楚很多。

迁移脚本要放在 .codex 外面,并以当前目录作为目标

迁移这一步最好写成脚本,而且脚本不要放在 ~/.codex 里面。否则一旦迁移失败,脚本自己也可能被卷进问题目录。

更通用的做法是:脚本固定读取 ~/.codex,但不固定目标路径。读者先 cd 到自己希望放 codex-home 的父目录,再执行脚本。比如希望把仓库放到 ~/code/codex-home,就先进入 ~/code

可以先下载这两个脚本:

mkdir -p ~/code/codex-home-migration
cd ~/code
curl -fsSLo codex-home-migration/migrate-codex-home.sh https://shengsheng.fun/files/codex-home-git-sync/scripts/migrate-codex-home.sh
curl -fsSLo codex-home-migration/rollback-codex-home.sh https://shengsheng.fun/files/codex-home-git-sync/scripts/rollback-codex-home.sh
chmod +x codex-home-migration/migrate-codex-home.sh codex-home-migration/rollback-codex-home.sh

这里有一个容易误解的点:codex-home-migration 只是放脚本的目录,不是最终的 codex-home 目录。正常情况下,从它的上一级执行脚本:

cd ~/code
./codex-home-migration/migrate-codex-home.sh --dry-run

如果已经进入了 ~/code/codex-home-migration 再执行脚本,新版脚本会自动把上一级 ~/code 作为目标父目录,并在输出里明确展示最终目标。真正迁移前,应该先用 --dry-run 看一眼目标路径是否符合预期。

如果不想用 curl,也可以在浏览器里打开这两个地址,把脚本保存到 ~/code/codex-home-migration/

https://shengsheng.fun/files/codex-home-git-sync/scripts/migrate-codex-home.sh
https://shengsheng.fun/files/codex-home-git-sync/scripts/rollback-codex-home.sh

执行迁移前,先退出 Codex Desktop 和其他可能读写 ~/.codex 的进程,再回到目标父目录执行:

cd ~/code
./codex-home-migration/migrate-codex-home.sh

脚本会提示本次源目录、目标目录和备份目录,并要求输入 MIGRATE 确认。以上命令的结果是:

~/.codex -> ~/code/codex-home
~/code/codex-home-migration/last-migration.env

这里不做交互式路径输入,是有意为之。路径输入一旦打错,脚本很难判断用户真正想要什么;而 cd 到目标父目录再执行,结果更直观:codex-home 就会出现在当前目录下面。

迁移脚本只做文件系统操作,不做 Git 操作。它也不再完整复制 ~/.codex 里的所有运行态,而是复制可携带数据和必要的轻量本机配置,跳过 sessions、browser profile、cache、.tmp、sqlite、日志、临时 shell 状态、computer-use app bundle 等会膨胀体积或容易触发复制错误的内容。

这样做的原因是:这些运行态本来就不应该进 Git,很多也能由 Codex 重新生成。完整复制它们不仅让 codex-home 变得很大,还可能在 macOS 上碰到 app bundle 深路径、扩展属性或临时 Git pack 权限问题。

脚本的职责应该尽量窄:

  • 检查 ~/.codex 存在,并且当前还不是软链接。
  • 检查目标目录 当前目录/codex-home 不存在,避免覆盖已有数据。
  • 如果检测到 Codex 进程仍在运行,直接停止。
  • rsync 把可迁移数据复制到临时目录,例如 当前目录/codex-home.copying.<timestamp>/
  • 临时目录复制完成后,再改名成正式目录 当前目录/codex-home
  • 把原始 ~/.codex 改名成带时间戳的备份目录,例如 ~/.codex.backup.<timestamp>
  • 创建软链接:~/.codex -> 当前目录/codex-home
  • 写一个状态文件,记录本次迁移的目标目录和备份目录,例如 当前目录/codex-home-migration/last-migration.env
  • 最后校验 ~/.codex 确实是软链接,并且指向预期目录。

这个流程里,原始目录不会被删除,只会被改名成备份。即使迁移后 Codex 打不开,最重要的数据也还在。

迁移成功后,重新打开 Codex,先确认几件事:

  • Codex 能正常启动。
  • 常用 Skill 能被读取。
  • 全局规则仍然生效。
  • 需要的记忆和知识库入口还在。

确认这些都没问题,再考虑 Git。

回滚脚本也要提前准备

迁移脚本之外,还应该提前准备回滚脚本。

回滚脚本的职责也要窄:删除 ~/.codex 软链接,把上一步留下的 ~/.codex.backup.<timestamp> 恢复成 ~/.codex。它不应该删除 ~/code/codex-home,因为迁移后的副本里可能有排查线索,也可能有迁移后新产生的数据。

如果迁移后 Codex 打不开,就继续保持 Codex 关闭,在终端执行:

cd ~/code
./codex-home-migration/rollback-codex-home.sh

回滚脚本会读取 ~/code/codex-home-migration/last-migration.env,确认上一次迁移时的备份目录,并要求输入 ROLLBACK 确认。

回滚完成后,目录会回到迁移前的形态:

~/.codex/                  # 恢复成普通目录
~/code/codex-home/          # 保留,供手动检查或手动删除

这个回滚路径存在之后,迁移就不再是一次不可逆操作。它更像一次有备份、有切换、有验证、有回退方案的上线。

CODEX_HOME 也能改,但桌面应用更适合软链接

Codex CLI 支持 CODEX_HOME

也就是说,纯 CLI 场景里可以这样启动:

CODEX_HOME=~/code/codex-home codex

这样 Codex 会从指定目录读取配置、Skill 和其他 home 数据。

不过如果平时主要使用桌面应用,情况会复杂一点。图形界面启动的应用不一定继承你在 shell 里设置的环境变量。你当然可以想办法给桌面应用注入环境变量,但这会让维护复杂度上升。

因此对桌面应用来说,软链接更稳:

~/.codex -> ~/code/codex-home

Codex 仍然读默认位置。Git 仓库仍然在好找的位置。两边都舒服。

私有仓库也要白名单

即使仓库是私有的,也不建议放开所有文件。

私有不等于可以把登录态、缓存和本机数据库全部推上去。原因有两个。

第一,风险不必要。凭证、浏览器 profile、session 这类东西没有必要离开当前设备。

第二,噪声会淹没真正有价值的变更。你真正想看的是「新增了哪个 Skill」「改了哪条规则」「记忆摘要有什么变化」,而不是一堆 sqlite wal 文件和缓存文件在变。

所以 .gitignore 应该从第一天就用白名单模式。

如果是当前机器刚迁移完,可以等 Codex 验证稳定后,再进入新目录做 Git 初始化:

cd ~/code/codex-home
git init
git branch -M main

然后先写 .gitignore 白名单。不要在没有 .gitignore 的情况下直接 git add .

写完 .gitignore 后,先看工作区里有哪些候选文件:

git status --short

确认候选文件合理后,再加入暂存区,并检查 Git 实际跟踪了什么:

git add .
git status --short
git ls-files | rg 'auth|session|sqlite|browser-profiles|cache|tmp|logs|worktrees'

git status 用来看本次提交会包含哪些文件,git ls-files 用来检查敏感或运行态文件有没有被纳入 Git。如果第二条命令有输出,就先停下来检查。

只有确认输出符合预期,才提交和推送:

git commit -m "Initial portable Codex home"
git remote add origin <private-repo-url>
git push -u origin main

换设备时会发生什么

用这个方案换设备,大致是:

git clone <private-repo-url> ~/code/codex-home
ln -s ~/code/codex-home ~/.codex
codex login

然后 Codex 会重新生成那些不进 Git 的运行态:

auth.json
sessions/
browser-profiles/
cache/
*.sqlite
logs/
tmp/

这很正常。

换设备真正要迁移的是你的工作方式和能力,不是旧机器的所有运行痕迹。

新的设备应该继承:

  • 你写过的 Skill。
  • 你长期使用的规则。
  • 你希望 Codex 记住的偏好。
  • 可读的记忆索引。

新的设备应该重新生成:

  • 登录态。
  • 浏览器授权。
  • 临时会话。
  • 缓存和数据库。

这个边界一旦想清楚,迁移就会简单很多。

这个方案解决什么

这个方案不是为了「备份一个目录」。

它解决的是:把 AI 工具的个人工作流资产,从一个隐藏的运行状态目录里拿出来,变成一个可读、可审查、可版本管理、可迁移的仓库。

压缩包适合兜底,Git 适合长期演进,软链接适合兼顾工具默认路径和人的维护体验。

最后形成的结构大概是:

~/code/codex-home/        # Git 仓库,方便人维护
~/.codex -> ~/code/codex-home

仓库里只跟踪可迁移资产:

AGENTS.md
rules/
skills/
memories/MEMORY.md
memories/memory_summary.md
memories/extensions/ad_hoc/notes/
.gitignore

其它运行态留在工作区,但不进 Git。

这样一来,Codex 还是那个 Codex;只是它的长期能力终于有了一个清楚、可见、能版本管理的家。