你可能遇到过这种场面:给 Codex 派了一个重构任务,它跑到你的主工作区里又是改文件又是跑测试,而你刚好想在同一时间、同一个仓库里改另一个 bug。结果 AI 的未提交改动和你的手写改动混在一起,连git status都分不清哪一行是谁改的。我最早用 Codex 处理中型项目时,就被这种“共享一个工作目录”的模式坑过很多次。
后来 Codex 引入了 Git Worktree 的集成,问题才算解决。但很多朋友对“Codex 工作树”和“Git 分支”的关系理解是含糊的:有人以为开 Worktree 就是新建分支,有人以为 Worktree 是个存储文件的东西。这篇文章我把这套机制的底层逻辑、配置方式,以及我真的踩过的坑,一次讲清楚。
这篇文章适合正在用 Codex(或任何 Agent 类 AI 编程工具)做实际项目的人,也适合 Git 有基础但一直没搞明白git worktree这个命令到底在干嘛的同学。
1. 为什么 Codex 这种 AI 编程工具必须引入 Worktree
1.1 一个 AI 会话锁住整个目录的尴尬
我第一次在真实项目里大规模用 Codex,是把一个老旧的 API client 模块改成统一的 Zod 校验。任务派下去之后,Codex 按照它的标准流程:读目录、读文件、改代码、跑测试。本来都挺顺利,但我同时想在同一仓库里修一个前端组件样式问题,于是自己也在 IDE 里开了个分支改代码。
结果就乱了。
Codex 在不知道我手上有未提交改动的情况下,直接运行了 eslint --fix 和格式化命令,把整个 src 目录都扫了一遍。我改了一半的组件样式文件,被它的格式化操作“顺手”规范化了,我自己的改动和 AI 的改动混在同一个 diff 里。最尴尬的是,Codex 跑完一轮指令之后,还提交了一个 commit,把我未完成的前端修改也卷了进去。
这不是 Codex 的“智力问题”,这是文件系统隔离问题。当多个实体(你、Codex 会话 A、Codex 会话 B)共享同一个工作目录时,彼此的中间状态必然互相污染,谁都没法保证“当前状态”是稳定的。
1.2 Codex 花在“确认现状”上的时间远比你想象的多
用过 Agent 类工具的人都知道,这类 AI 工具不是只调一次接口就完事。它的执行模式更像是:观察现状 -> 决定动作 -> 执行 -> 观察结果 -> 再决定下一个动作。
每一步决策都依赖“当前目录此刻是什么状态”。如果工作区里同时有另一个会话在改文件,Codex 就会反复陷入一个循环:读到一份被改了一部分的代码,执行测试报错,重新读文件,发现内容又变了,再执行一次。我在一次日志里看到,有的步骤被重复执行了三遍,原因就是工作区状态不稳定。
在单工作目录模式下,这种干扰是无解的。你不能让 Codex“等一下再动”,因为它没有全局锁的概念。它默认自己工作区里的所有文件变化都是自己造成的,可实际上还有另一个 AI 会话、或者你自己的手写改动。
1.3 Git Worktree 是把“物理隔离”交给版本控制的答案
Codex 的解决方式非常直接:不再让所有会话挤在同一个目录里,而是引入 Git 原生的 worktree 机制。每个 Codex 会话都基于当前仓库创建一个独立的工作目录,目录里是一套完整的可编辑文件,但共享同一个.git对象库。
这样每个会话都有自己独立的文件系统上下文,改了文件、跑了测试、提交了 commit,都不会影响主工作区,也不会影响另一个 Codex 会话。等任务结束后,Codex 把那个 worktree 目录清理掉,整个过程对主仓库就像什么都没发生过。
听起来很优雅,但前提是你得先搞清楚 Worktree 到底是怎么运作的,否则一旦出现“分支删不掉”“worktree 目录还在但不知道对应哪个分支”这种问题,会非常头疼。
2. 拆开 Git 的内部:Worktree 和分支到底谁是谁
2.1 分支是贴纸,Worktree 是工作台
先说结论:分支和 Worktree 根本不是同一个层次的东西,它们之间是“绑定关系”,不是“等价关系”。
Git 里的分支,本质上只是.git/refs/heads/目录下一个文本文件,里面存着一个 commit hash。它唯一的作用是“贴”在某一个 commit 上,作为可移动的引用。你执行git branch时,它不会创建任何文件副本,只是往这个贴纸上写了一个新的 commit 位置。
Worktree 则是实际存在的、你能看到的那一套文件目录。它包含你正在编辑的源码、配置、构建产物,以及一份独立的.git工作区元数据(HEAD、index 等)。
我用一个经常给同事讲的类比:分支是贴纸,Worktree 是办公桌。贴纸贴在哪一叠资料上,表示你当前在跟踪哪一份工作;办公桌则是你实际铺开资料、动手修改的地方。同一个档案柜(.git对象库)可以连接多张办公桌,但你不会把同一张贴纸同时贴到两张办公桌上。
2.2 底层存储:.git/worktrees 里到底放的是什么
当你执行git worktree add之后,Git 会在主仓库的.git/worktrees/<worktree-name>/目录下创建一个子目录。里面主要包含:
HEAD:这个 worktree 当前检出的分支或 commitindex:这个 worktree 自己的暂存区,独立于其他 worktreecommondir:指向主仓库.git目录的路径,告诉 Git 公共对象库在哪gitdir:指向这个 worktree 的元数据目录
换句话说,每个 worktree 都有一份独立的 HEAD 和 index,但 commit 对象、分支引用等公共数据是共享的。
这意味着两件事:第一,不同 worktree 可以处于完全不同的分支,互不干扰;第二,worktree 不会复制仓库历史,几百 MB 的提交记录不会因为多建几个 worktree 就翻倍。
2.3 为什么一个分支不能同时被两个 Worktree 检出
这是理解 Worktree 和分支关系最关键的约束。
同一个分支同时只能在一个 worktree 中被“检出”。如果你尝试在第二个 worktree 里git checkout一个已经被另一个 worktree 使用的分支,Git 会直接拒绝,并报错:
fatal: 'feature/xxx' is already checked out at '/path/to/another/worktree'原因很好理解:一个分支对应一个 HEAD 和一个 index。如果两个 worktree 同时检出同一个分支,两边的工作区都认为自己才是“当前状态”,Git 无从判断哪一份文件改动是权威的。这也保证了后续所有操作(提交、合并、diff)都有明确的基准。
所以记住这句话:分支标记“在哪儿”,Worktree 提供“干活的地儿”。两者配合,才能实现多任务并行。
3. Codex 工作树实操:从安装配置到第一次并行会话
3.1 安装时的那次提问,到底要不要选“是”
新版 Codex 在安装或首次运行时,会询问是否允许它使用 Git Worktree。我当时毫不犹豫选了“允许”,因为我已经在手动项目里用 worktree 很久了,知道这个机制靠谱。如果你用的是老版本,也可以在.codex/config.toml中手动开启 worktree 相关选项(具体字段名以当前版本文档为准,Codex 迭代很快,字段名变过几次)。
我的建议是:只要你不是在一次性小脚本里随便跑个任务,就选“是”。这个选择只改变 Codex 管理工作区的方式,不会影响你的仓库历史,也不会动已有分支。
3.2 启用后 Codex 的目录约定与会话行为
启用之后,我再给 Codex 派任务时,它的行为完全变了。
比如我在main分支上,让它做一个“把项目里所有 API 错误处理统一成 Result 类型”的重构。它会基于当前分支新建一个独立的 worktree,分支名通常带有codex标识和会话关键词,目录则放在仓库外的某个临时位置,或者按它的内置规则生成路径。
重要的是,我可以同时开多个会话。比如同时让会话 A 做上面那个 Result 类型重构,让会话 B 写一个 markdown 转 PDF 的命令行工具。两个会话分别在各自的 worktree 里跑,改文件、执行测试、提交 commit,互相看不见。我在主目录里还可以继续做自己的事,完全不受影响。
这种并行能力,在单工作目录模式下是没法想象的。之前两个会话同时跑,光是package.json被不同会话分别修改导致的冲突,就让我在合并时头大了好几次。
3.3 手动管 Worktree 的高频操作一览
虽然 Codex 会自动创建和清理 worktree,但我还是建议你掌握手动操作命令,因为你总会遇到 Codex 清理不及时、或者你想自己临时开一个 worktree 的场景。
下面这几个命令是我用得最勤的:
| 操作意图 | 命令 | 说明 |
|---|---|---|
| 基于新分支创建 worktree | git worktree add -b <new-branch> <path> | 创建一个新分支并立即检出到新目录 |
| 基于已有分支创建 worktree | git worktree add <path> <branch> | 在新目录中检出已有分支 |
| 查看所有 worktree | git worktree list | 列出全部工作树路径、分支和当前状态 |
| 移除 worktree | git worktree remove <path> | 删除工作目录及对应元数据 |
| 清理失效记录 | git worktree prune | 删除已被手动删除目录的残留记录 |
一个完整的操作流长这样:
# 创建一个基于新分支 feature/codex-login 的 worktree git worktree add -b feature/codex-login ../repo-codex-login # 进入该目录干活 cd ../repo-codex-login # 回到主仓库看看当前所有工作树 cd ../repo git worktree list # 功能完成后,先移除 worktree,再删分支 git worktree remove ../repo-codex-login git branch -D feature/codex-login如果你只是临时想看一个分支的代码,而不想动当前工作区,git worktree add <path> <branch>而不加-b是更好的选择:它不会创建新分支,只是把那个分支的内容在另一个目录里铺开。
3.4 一次双任务并行的实测记录
为了验证 Codex 的 worktree 机制是不是真的稳定,我做过一次比较极端的测试:在同一个仓库里同时派两个任务,两个任务都会改动项目里的公共工具函数目录。
- 会话 A:给所有工具函数加上 JSDoc 注释,并统一错误抛出方式
- 会话 B:把日志模块从 console.log 替换为统一的 logger 封装
这两个任务如果放在同一个工作区里做,几乎是必冲突的。因为它们都要扫公共目录,并且都会运行 lint。但在两个独立 worktree 下,整个过程异常顺利:会话 A 在自己的分支上提交了所有改动,会话 B 也提交了,最后我手动把两个分支合并到 dev,Git 自动解决的冲突量比预想中小很多。
这个测试给我的结论是:Worktree 的价值不在于“让 Git 自动解决冲突”,而在于让大多数冲突在文件系统阶段就根本不会产生。
4. 用起来之后,我从这些坑里捡回了一堆 commit
4.1 删不掉的“被占用”分支
我第一次用 Worktree 跑完一个功能分支后,想在主仓库里直接删掉那个分支,结果 Git 毫不留情地拒绝:
error: Cannot delete branch 'feature/xxx' checked out at '/path/to/worktree'当时我还懵了一下,因为分支明明已经合完了。后来反应过来,分支仍然被那个 worktree 的 HEAD 引用着。Git 不会允许你删掉一个正在被某个工作树检出的分支,否则那个 worktree 就变成了悬挂状态。
解决办法很简单:
git worktree remove /path/to/worktree git branch -D feature/xxx4.2 --force 的代价与残留目录修复
git worktree remove有保护机制:如果 worktree 里还有未提交的改动,它会拒绝删除。这个设计是好的,但有时候 Codex 的会话中途被打断,worktree 里残留了几个临时文件,此时你不确定那些改动是否重要。
如果确定不要了,可以用--force强删:
git worktree remove --force /path/to/worktree注意:这个操作会直接丢弃未提交改动,不会进回收站,也没有任何 undo 手段。我就有过一次手滑,把一上午写的实验性改动连同 worktree 一起强删了,追悔莫及。
还有一种常见情况:worktree 对应的目录已经被你手动rm -rf删掉了,但git worktree list里还留着记录。这时候执行一次git worktree prune,Git 会清理掉这些已经失效的元数据。
4.3 多工作区下的状态误判
多个 worktree 同时存在时,最容易踩的认知坑就是“我在主仓库看到干净状态,就以为所有分支都干净”。
有一次我在主分支上跑git status,输出显示 clean,就放心地把另一个分支合并进主分支了。合并完成后才发现,那个分支在它的 worktree 里还躺着几个未提交的改动文件,根本没在这次合并范围内。好在改动不大,后来又手工补齐了,但过程很尴尬。
我现在的习惯是,多 worktree 场景下用下面这段命令把所有工作区状态一次打出来,避免漏看:
for wt in $(git worktree list --porcelain | grep '^worktree ' | awk '{print $2}'); do echo "=== $wt ===" git -C "$wt" status --short | head -20 done把这段存成 alias 或脚本,每次开会话前先跑一遍,能省掉不少不必要的困惑。
4.4 磁盘与构建成本的现实账本
Worktree 不会复制.git历史,但会复制工作目录里的所有文件。如果你的项目里有node_modules、dist、.next、vendor这类大目录,而且没有在.gitignore里忽略它们,那每新建一个 worktree,就等于把这些大目录重新复制一份。
我实际遇到过一个项目,仓库本身只有 500MB,但node_modules加构建产物接近 2GB。同时开 4 个 worktree,磁盘空间直接吃掉了 8GB。这还只是磁盘层面,更麻烦的是每建一个 worktree,首次构建都要从头再来一次,CI 缓存在本地也起不到作用。
所以如果你要在多 worktree 下工作,最好先确认:
.gitignore是否覆盖了所有构建产物目录- 工作目录路径所在的磁盘分区空间足够
- 大仓库可以考虑用
git sparse-checkout或部分克隆减少不必要的文件复制
5. 什么项目适合 Codex + Worktree,什么场景真的别硬上
5.1 五个值得引入的信号清单
我自己总结了几条比较实用的判断信号,如果你的情况符合其中任意一条,建议尽快把 Worktree 用起来:
- 你经常同时给 Codex 派两个以上的独立任务。并行任务在共享工作区下几乎必然互相干扰,worktree 是最省心的隔离方式。
- 你希望 AI 改代码的同时,自己还能在主分支继续开发。这是 Worktree 最典型的使用场景,相当于给 AI 开了个独立工位。
- 你需要 review AI 的代码,又不想弄脏自己的开发环境。在独立 worktree 里看代码、跑测试,review 完直接移除,主仓库始终保持干净。
- 你想安全试验破坏性重构。比如大规模重命名、删除模块、调整目录结构,这些操作一旦失败会很麻烦,在独立 worktree 里做就无所谓,大不了删掉重来。
- 你的 Codex 会话经常因为工作区状态不稳定而重复执行。说明已经出现状态污染,Worktree 是根治手段。
5.2 三类硬上会很难受的项目
Worktree 不是万能药,我在下面三类项目里尝试过,体验都不太好:
第一类:巨型单仓库,且构建产物无法优雅忽略。如果项目里的构建工具会把大量中间产物写进工作目录,而.gitignore又没配好,每开一个 worktree 就是一份完整的磁盘镜像,成本太高。
第二类:工具链强依赖固定目录名或绝对路径。有些脚本会在代码里硬编码/home/user/project/xxx或者pwd的结果,worktree 一旦放在别的路径下,这些脚本就直接失效。
第三类:团队成员普遍不熟悉 Git worktree 操作。协作时如果有人对着一个 worktree 的目录执行了git checkout或git reset,很可能会把整个分支处于特殊状态,其他人在别的 worktree 里还看不到。这类团队更适合先用单工作区 + 分支切换的保守方案。
如果你已经踩到上面这些坑,但又需要隔离环境,替代方案可以考虑用 CI 跑 Agent 任务,或者临时用git stash切换分支的笨办法,虽然不如 Worktree 优雅,但至少不会把磁盘撑爆。
6. 我的使用清单和最后几条手记
6.1 我的日常 Worktree 命名规范
Worktree 开多了之后,路径命名如果不规范,找起来会非常痛苦。我现在的约定是:所有手动创建的 worktree 统一放在主仓库的兄弟目录下,命名格式为<仓库名>-<分支名或功能名>。
比如主仓库目录叫repo-api,我要开始一个登录重构,就会建../repo-api-login-refactor。这样一眼就能看出这个目录是哪个仓库的、干什么用的。
6.2 一个值得养成的 Codex 会话习惯
我现在每次给 Codex 派完任务,不会让它立刻清理 worktree,而是先保留这个独立分支的目录,等我自己 review 完代码、确认合并之后,再手动移除。原因很简单:Codex 认为自己完成了,但你自己还没验证。如果 worktree 被清得太早,临时想回去看某段代码或补一个小改动,就得重新走一遍分支重建流程,很烦。
6.3 最后再提醒一句
Worktree 和分支是配合关系,不是替代关系。分支负责标记每一次变更的来龙去脉,Worktree 负责给你一个干净的物理工作区。理解了这层关系之后,再回头看 Codex 为什么要在每个会话里默认开 Worktree,就很自然了——它不是在发明什么新机制,只是把这套 Git 老牌能力用在了 AI 编程场景里。