Subagent Worktree 锁定与生命周期(Git worktree 锁与自动清理机制)

Claude Code 的 subagent 在 worktree 里并发写文件时,工作区不会被自动清掉,靠的是两层 Git 原语:一是 git worktree lock 阻止 worktree 被剪除,二是基于 cleanupPeriodDays 的延迟清扫加 reflog 兜底。下面把锁的来源、清扫的触发条件、以及运行中为何能放心修改,逐条拆开。

一、subagent 拿到的是什么样的 worktree

父会话派发子任务时,如果子代理声明了 isolation: worktree,Claude Code 会在 .claude/worktrees/<name>/ 下创建一条新分支,工作目录完全独立于父会话,提交只会落在该分支上。多个子代理同时改 lib/utils.ts 不会互相覆盖,这是 worktree 隔离的初衷。

这一步里 Git 侧记录了哪些状态:.git/worktrees/<name>/ 目录会保存子 worktree 的 HEAD、commondir、gitdir 三项元数据;子目录里的 .git 实际上是一个指向 gitdir: 的文本文件,共享同一个对象库。子代理的所有写入都进同一个 objects,只是工作树与索引隔离。

二、运行期间不被清理的两道闸

子代理一启动,Claude Code 立即调用 git worktree lock 锁住这个工作树。锁文件位于 .git/worktrees/<name>/locked,内容是任意占位字符串(常规做法是写入当前工作目录的绝对路径,便于排查)。在锁存在期间,任何 git worktree remove、git worktree prune 都不会动它,这是 Git 自 2.10 起的内置行为。

自动清扫只对没有锁的 worktree 生效。子代理在 run 状态持有锁,即使配置 cleanupPeriodDays=0,扫到这条记录也会跳过。任务完成、清理钩子显式解锁后,worktree 才进入可回收队列。下面是子代理一次典型生命周期的执行步骤:

  1. 父会话创建 .claude/worktrees/<name>/ 并 git worktree add -b <branch> <path>;
  2. 子代理启动,Claude Code 立即执行 git worktree lock 写入 .git/worktrees/<name>/locked;
  3. 子代理在 worktree 内编辑、提交,所有操作仅作用于该 worktree 与对应分支;
  4. 子代理退出,Claude Code 解锁并按”无改动/有改动”分支决定保留或回收;
  5. 下次会话开始时,git worktree prune 清掉满足 cleanupPeriodDays 且无锁的孤立记录。
# 查看锁状态(子代理运行中通常会看到一行 locked)
git worktree list
# /repo/.claude/worktrees/feat-auth  abc1234 [feat-auth] locked: /repo/.claude/worktrees/feat-auth

锁带来的副作用也值得注意:同一 worktree 不允许两个分支同时 checkout,所以父会话在子代理还活着时不能 cd 进同一个目录并发执行;若必须提前取消,走 WorktreeRemove 钩子而不是裸 rm -rf,否则 .git/worktrees/<name>/ 的索引会残留,后续 git worktree prune 才会把它清掉。

三、结束后的延迟清扫策略

子代理一旦正常退出,锁被释放,worktree 进入候选清扫集。Claude Code 的判定条件是三件全满足才删除:无未提交改动、无未追踪文件、无未推送 commit;只要有一项不满足,目录与分支都保留,留给用户合并或丢弃。空 worktree 满足全部三条,会被立即回收,避免 .claude/worktrees/ 越攒越多。

cleanupPeriodDays 控制的是”沉睡多少天之后才视为孤立”。新结束的空 worktree 立刻清,跑了一周还没人合并的 worktree 只要没改动也清,--worktree 手动创建的常驻 worktree 不在这个范围内,这条规则与子代理的临时 worktree 形成清晰区分。

// settings.json
{
  "worktree": {
    "cleanupPeriodDays": 7
  }
}

四、底层 Git 机制串联

把上面的行为映射到 Git 原语,可以看到四个真正起作用的点。

行为 Git 原语 Claude Code 中的触发点
创建隔离工作树 git worktree add -b <branch> <path> --worktree 标志或 isolation: worktree 子代理声明
阻止运行中清理 git worktree lock 子代理启动时自动执行
索引一致性 .git/worktrees/<name>/{HEAD,commondir,gitdir} worktree 创建时由 Git 写入
清理孤立记录 git worktree prune cleanupPeriodDays 触发,通常在每次会话开始时跑一次

这套机制的关键在于锁是 Git 维护的,不是 Claude Code 自己在文件树里写一个 is_running 标记。即使 Claude Code 进程崩溃、Ctrl+C 中断、容器被 kill,只要 .git/worktrees/<name>/locked 还在,任何自动清扫都碰不到它,下次 git worktree list 还能看到它,以便人工收尾。

五、与 reflog 的兜底关系

子代理产生的提交落到分支上,分支 ref 与 objects 引用是绑定的。即使 worktree 目录被错误删除,只要 .git 还在,reflog 仍保留提交对象;git worktree prune 不会主动 gc 关联的 commit,所以”误删 worktree”不会直接”丢提交”。要彻底清理才需要 git reflog expire --expire=now --all && git gc --prune=now --aggressive,而这通常要用户显式触发。

因此子代理运行期间,真正的安全网是 git worktree lock,reflog 提供的是第二道恢复可能,延迟清扫则是把”没人要的空 worktree”在 N 天后回收,避免磁盘被临时目录塞满。三者各司其职,谁也不会在 agent 还活着时把它的目录抹掉。

六、落地时的两个易错点

第一,把 .claude/worktrees/ 写进 .gitignore 之后,父工作树就不会把这些目录当成未追踪文件,git status 保持干净;但不要把 .git/worktrees/ 一起忽略,后者是 Git 自身的索引区,写进去会让所有 worktree 命令失效。第二,需要”跨 worktree 共享”的环境变量(如 .env.local)用 .worktreeinclude 文件声明(语法同 .gitignore),WorktreeCreate 钩子会按需复制;没声明就裸跑,子代理启动后多半会卡在环境配置上,白白浪费一轮。

常见问题(FAQ)

Q1:子代理崩溃后,它的 worktree 会一直占着吗?

不会。锁由 Git 维护,即使 Claude 进程已退出,只要目录未删,锁就还在;但自动清扫的判定只看”是否空”,不再看锁,所以崩溃后留下的空 worktree 会在下次启动时由 git worktree prune 清掉。

Q2:能用 rm -rf 强行删 worktree 目录吗?

能,但会留下 .git/worktrees/<name>/ 索引残留,后续 git worktree list 还会显示一条无效记录;正确做法是 git worktree remove 加 --force,或先 git worktree unlock 再 git worktree prune。

Q3:父会话能否进入子代理的 worktree 看进度?

能读不能写。同一 worktree 已被子代理 checkout,父工作树再切进去会报”already checked out”,需用 git log 在父仓库追溯该分支的提交历史,或另开一个 worktree 指向同一分支只读浏览。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部