← 返回博客2026-06-20

为什么每个 AI 编码 agent 都该有自己的 git worktree

把两个 agent 指向同一个 checkout,它们会互相破坏。这是解法——以及怎么让它自动发生。

问题

我用 AI 编码 agent 做开发——常常同时开着四个 Claude Code 或 Codex 会话,每个还会派生 sub-agent。有很长一段时间它们全在同一个 checkout 里干活。这就是那个陷阱。

一个 git 工作目录只有一个 HEAD:一个分支、一个 index、磁盘上一套文件。在里面跑多个 agent,它们不是在协作——而是在没有任何锁的情况下互相覆盖同一批物理文件。三起真实事故:

  • 一个 agent 在构建中途 git switch另一个 agent 的 cargo 正在编译,读到了撕裂的源码——一半旧分支、一半新分支——带着指向谁都没碰过的代码的幽灵报错失败。
  • 两个 agent 改同一批文件。一个正改到一半,另一个也动了同样的文件,差点把对方未提交的工作 stash 掉。
  • 一个会话一直住在共享的 main 树里,手里攥着未提交的改动。别人合并后 main 在它脚下前进;git switch main 被挡住(同一个分支只能在一处被 check out);一个无法合并的 .pbxproj 炸成了冲突。

不同分支也救不了你——这正是大多数人忽略的地方。分支只是共享历史里的一个标签;一个工作目录在同一时刻仍然只能 check out 一个分支。所以共用一个目录的两个 agent 只能不停地把它 git switch 来回切,而这一切换正是制造损坏的那一下。(未提交的改动根本不在任何分支上——它就是共享目录里的文件。)解药必须落在磁盘上:第二套文件。

这就是 worktreegit worktree add 把一个分支 check out 到它自己的目录,配自己的 HEAD 和 index,共享同一个仓库。每个 agent 拿到私有的文件,在物理上无法碰撞。「一个分支在它自己的目录里」本身就是 worktree——救你的是这个,而不是分支。

✗ 陷阱 一个 checkout,多个 agent agentagentagent 一个 checkout(main) 一个 HEAD · 一套文件 git switch 切到一半 → 另一个 agent 的构建读到撕裂 源码;未提交的工作还被冲掉。 ✓ 解法 每个 agent 一个 worktree agentagentagent worktreeworktreeworktree 一个共享仓库(.git) 共享历史 · 私有文件

同样的 agent,两种布局:共用一个 checkout(互相碰撞) vs 各自一个 worktree(互不干扰)。

怎么配置

你可以完全不碰 git,甚至不用开终端。你在编辑器或 App 里用自然语言描述需求——「加一个加注滑块;在单独的 worktree 里做,做绿了再合并回来」——agent 替你跑 git(你只需批准那条命令)。Claude Code 和 Codex 都是会执行 shell 的 agent,所以「建 worktree」是它们做的事,不是你做的。配上下面的 hook,你连「在 worktree 里」都不用说。

Claude Code —— 一等公民支持

Claude Code 内置 worktree:你开口要一个(或者在 CLI 上用 --worktree flag 启动),它就跑 git worktree add、在你默认分支上拉出新树、在里面干活;做绿之后由你叫它合并回来——内置功能不会自己合并。给 sub-agent 加 isolation: worktree,它会拿到一个一次性 worktree,没有改动地收尾时自动移除。你全程都在说人话。

Codex —— 思路一样,注意 sandbox

Codex 没有内置 worktree 功能,但它也是会跑 shell 的 agent:你同样开口,它会自己跑 git worktree add。唯一一个真实的坑是 sandbox。Codex 的 workspace-write 模式只能写它的主工作区——也就是你启动它的那个目录(或用 -C 指定的目录)——外加任何额外的可写根目录。所以如果你在主仓库里启动 Codex,像 ~/.codex/worktrees/<repo>/<name> 这样位于树外的 worktree(本仓库的工具正是放在这里)默认是不可写的。三种解法:直接在 worktree 里启动 Codex(codex -C ~/.codex/worktrees/<repo>/<name>),让它成为工作区;或保持主仓库为工作区、用 --add-dir <那个目录> 把 worktree 加进来;或者,对于反复用到的布局,在 ~/.codex/config.toml[sandbox_workspace_write].writable_roots 里列上父目录。把 worktree 放在主工作区之内,则这些都不需要。

新问题:怎么让每个新 session 和子 agent 都遵守这条规则

到这里,worktree 还是在你或 agent「记得开口」时才被建出来。可一旦有几十个 session 和 sub-agent,「记得」必然失守。解法是用 hook 把它变成结构性的——让 worktree 成为默认路径,而不是要记住的事:

  • 一个 SessionStart hook:在共享 main 树里打开的会话,会收到「先建 worktree 再编辑」的提醒。
  • 一个 PreToolUse hook:当编辑目标落在 main 树里时,硬阻断编辑工具(Edit/Write/MultiEdit)——于是 agent 对 main 的编辑被拒绝,被逼上 worktree。(覆盖:ALLOW_MAIN_EDIT=1。)
  • 提交/合并闸门(一个 pre-commit hook 加一个 Bash PreToolUse hook),让工作只能通过受认可的合并进入 main

hook 把常规的编辑/提交/合并路径卡住;两个封装脚本把剩下的也变成一步——一个建 worktree(链接 .env、分配固定错开的备用端口、避开主栈的 :3001/:5173),一个做 --no-ff 合并并移除它、树脏就拒绝。

底层怎么运作

主仓库的 .git 下只有一个对象数据库、一套 ref。每个链接的 worktree 在 .git/worktrees/<name>/ 有一个小管理目录,存自己的 HEAD 和 index;它的工作目录里放的是一个 .git 文件(一行指针指回去),而不是目录。

状态逐 worktree共享
HEAD、index、工作文件、进行中的 merge/rebase
对象、ref(分支/tag)、config、hooks、stash

这种拆分就是机制所在——也是为什么分支和 worktree 不可互换:worktree 是磁盘上的一个私有槽位,把某一个标签落地成真实文件。因为每个 worktree 拥有自己的 HEAD 和文件,两个可以同时停在不同分支;因为它们共享对象和 ref,一个里的 commit 在另一个里立刻可见。两个坑:stash 是仓库全局的(在一个里 push 的 stash 在所有里都看得到),以及同一个分支默认不能被 check out 两次——git 会拒绝,也会拒绝任何会去更新「别处已 check out 的分支」的操作。磁盘开销是工作集,而非历史:每个 worktree 重复 check out 的文件和构建产物(target/node_modules/),所以 N 个 worktree ≈ N 倍工作集。

诚实的边界

  • worktree 不消除合并冲突。隔离不等于协调:两个 worktree 改同几行,合并时照样冲突——worktree 只是把碰撞从「无声的线上损坏」挪成「一次你有意去解决的显式合并」。让分支小、短命,勤集成。
  • 磁盘和重新构建的时间。每个 worktree 一次冷启动的 cargo build / npm install 是实打实的若干 GB 和几分钟。
  • 本地配置逐 worktree。新 worktree 没有你任何被 gitignore 的配置;把它链接进来。(server 二进制从 shell 读环境变量,而非 .envcargo test 需要 DATABASE_URLbuild/check 不需要。)
  • 清理纪律。收尾 = 合并,然后 git worktree remove——别 rm -rf(会留下陈旧条目)——再 git worktree prune

实用建议

  • 让主树空闲停在干净的 main 上,作为合并目标。
  • 给每个 worktree 自己的备用端口 dev 栈和自己的 node_modules——别 symlink 到主仓库(Vite 的 fs.allow 会 403 掉 worktree 之外的字体)。
  • 用 hook 强制,而不是靠自觉。塑造我们做法的那条洞察:一个后台 sub-agent 曾在明确的「不要合并」下合并到了 main,而没有任何环境变量能区分 sub-agent 和主线程——所以闸门必须默认拒绝,而不是一次信任检查。

总结

  1. 问题:一个 checkout + 多个 agent = 互相破坏(撕裂的构建、被冲掉的未提交工作)。不同分支救不了——分支只是共享历史里的一个名字,bug 在磁盘上。
  2. 解法:worktree = 把一个分支 check out 到它自己的目录(自己的 HEAD 和文件),架在共享仓库上——物理隔离,互不碰撞。
  3. 怎么用:别敲 git——用自然语言描述任务,agent 自己建 worktree。真正关键的 hook 不是「保护 main」(那是标配)——而是让每个新 session 和子 agent 都只剩 worktree 这一条能走通的路:一进 main 树就被提醒、想改 main 就被拒——worktree 仍由 agent 自己建,但绕不过去。
  4. 边界:worktree 消除的是工作树竞态,不是合并冲突——合并冲突靠「分支切小、勤合并」来压住。