关于 worktree-hygiene
用于安全管理 Agent、脚本和人工会话共用的 Git worktree 与分支的技能。它规定了删除、所有权交接、并行文件修改和清理已落地分支的检查方式。
主要功能
- 01安全删除 worktree 的检查
- 02明确的所有权交接
- 03多参与者间的文件归属
- 04已落地分支清理脚本
适用场景
- 01安全删除已完成的 worktree
- 02将 worktree 交给其他 Agent
- 03避免共享目录中的修改冲突
- 04清理已落地的分支
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vivswan/ skills --skill worktree-hygiene安装后在 Claude Code 中输入/worktree-hygiene确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.mdREADME.md.codex-plugin/plugin.jsonagents/openai.yamlreferences/retire-branch.mdscripts/retire-branch.mtsscripts/retire/judge.mtsread.mts详细介绍
工作树卫生
工作树是共享的可变状态。删除具有破坏性,移交意味着所有权转移,而且每个工作树都共享主仓库的
.git。以下每条规则都用于防范生产环境中出现过的破坏性故障。
下文中的“参与者”是指任何会向工作树写入内容的对象:Agent、子 Agent、人工会话,或其中任何一方启动的进程。
适用场景
- 删除 Git 工作树,或在分支合并后将其退役(包括其工作树、本地引用,以及可选的远程引用)
- 将工作树或分支移交给另一参与者,或恢复拥有它的参与者
- 多个参与者(Agent、会话、脚本)共享同一个仓库或工作树
- 在存在其他工作树时切换分支或写入
git config
删除工作树
执行 git worktree remove 前,按以下顺序重新核实这三项:
-
通过新鲜的状态码确认干净。 运行一次新的
git -C <tree> status --porcelain -uall,并检查状态码。切勿相信先前报告中的脏文件数量:数量无法区分新文件与删除过程中产生的删除项,而状态码可以。-uall会覆盖status.showUntrackedFiles=no配置,避免未跟踪文件被悄悄隐藏。除非能明确确认某个条目的确切路径可以丢弃,否则每个条目都会阻止删除。删除过程中产生的条目会将其自身的删除显示为
D,协调中的参与者所用的保活标记(见下文)则会单独显示为??。未按此方式确认的??、M或D条目都代表实际工作。- 删除干净的工作树不会丢失任何已跟踪内容。其提交仍保留在对应的引用上。
- 被忽略的文件不会显示在普通状态输出中,但会随工作树一同删除。如果工作树中可能包含有价值的被忽略产物(如本地数据库、采集的数据),请先使用
git -C <tree> status --porcelain -uall --ignored列出它们。这里也要保留-uall,因为单独使用--ignored不会覆盖status.showUntrackedFiles=no配置。
-
确保提交位于持久引用上。 分离头指针工作树中的独有提交可能只能通过其私有 HEAD 访问,而删除工作树会连同 reflog 一起删除它。
git -C <tree> symbolic-ref -q HEAD以非零状态退出表示处于分离头指针状态。删除前,请在git -C <tree> rev-parse HEAD指向的提交上创建分支或标签,或证明该提交可通过持久引用访问。 -
确认没有正在运行的写入进程。 检查工作目录位于该工作树内的进程(使用
lsof -a -p <pid> -d cwd或lsof +D <tree>)。未出现在运行列表中的参与者本身不会写入,但它启动的进程(测试链、安装进程)仍可能在运行。请先终止这些进程,或等待它们结束。如果工作树处于锁定状态(
git worktree list --porcelain会显示locked及其原因),或其他参与者可能正在使用它,切勿在不了解其所有者及用途的情况下删除。
删除后:
- 使用
git worktree remove删除,切勿直接使用rm -rf。 手动删除会在共享的 git 目录中留下工作树的管理条目。Git 会因此继续将其分支视为已检出,并阻止删除该分支,直到该条目被清理。git worktree prune --expire now会立即清除过期条目。直接运行prune会遵循gc.worktreePruneExpire(默认三个月),因此刚删除的工作树条目可能仍会保留,其分支也仍会被锁定。Git 最终也会自行清除条目,但切勿依赖其清理时间。- 在清理前先锚定提交。
prune会删除每个被清理条目的私有 HEAD 和 reflog;它会一次清理所有缺失且未锁定的工作树,而不仅仅是你的工作树。git worktree list --porcelain会标记每个可清理条目,并显示其记录的HEADSHA,因此请先为任何分离头指针工作树创建分支或标签(规则 2)。
- 删除工作树不会终止仍在运行的进程。 已结束参与者遗留的卡住的测试链,可能在其目录删除后继续运行数小时。请重新检查并终止任何工作目录指向已删除路径的进程。
同时删除分支
删除分支上的工作树不会删除该分支。只有在确认其工作已保留在主线上后,才删除分支。scripts/retire-branch.mts(下文的“退役已合并分支”)会先验证,再在一次受门控的运行中完成删除。无论如何,以下两条规则都必须遵守:
-
绝不要信任
git branch -d。 它只检查是否合并到了分支配置的上游(如果未设置,则检查是否合并到了 HEAD),而通常的上游是分支自己的origin/<topic>,而不是你的主线。它可能在工作尚未合入时成功,也可能在工作已合入时拒绝,因此两种结果都不能证明任何事情。 -
只有在租约锁定到你已验证的 sha 时,才能删除服务端 ref,这样,如果有推送在你验证与删除之间落地,操作会被拒绝,而不是将其销毁:
git push <remote> --force-with-lease=refs/heads/<topic>:<verified-sha> :refs/heads/<topic>
自动清理可能会销毁仍在使用的工作区
会自动清理隔离工作树的运行框架,会在其执行者完成且工作树干净时将其移除。如果执行者在尚未写入任何内容时就停下来“等待”,而其他人仍在它的工作树中工作,就会导致他们在已删除的目录中工作。
-
在开始时立即将工作树标记为有改动,前提是执行者要在自己的隔离工作树中协调其他人。在工作树根目录下放置一个未跟踪的标记文件就足够了(例如
.orchestrator-keepalive)。确认该标记在
git status --porcelain -uall中确实显示为??。这里也要保留-uall,否则status.showUntrackedFiles=no配置会隐藏此检查所需的关键证据。忽略规则也可能将其隐藏;被隐藏的标记毫无保护作用,因此若发生这种情况,请换个名称。该标记绝不会被暂存或提交(删除规则 1 将其指定为唯一可丢弃的
??),并且会在发出最终信号或交接前删除。 -
在恢复任何曾在隔离工作树中干净停下的执行者之前,确认工作树仍然存在。如果不存在,请重新启动一个新的执行者。
退役已合并分支
scripts/retire-branch.mts 会在一个程序中完成退役,因此如果门控检查失败,就无法执行破坏性步骤。它会将主线获取到本次运行专属的 ref,证明分支已合入,在分支的工作树上执行上述删除规则,然后移除工作树,并删除固定到已验证 sha 的本地 ref。
首先确认 CI 结果:由 /watch-github 技能提供。然后进行演练。不带 --execute 时会运行所有门控检查,但不会销毁任何内容:
bun "<skill-dir>/scripts/retire-branch.mts" --branch feature/thing
[fetch] origin/main -> 4f2a9c... (the remote-tracking ref was already current)
[branch] refs/heads/feature/thing is at 91bd0e...
[landed] by three-way merge: feature/thing merges into 4f2a9c without changing its tree
[worktree] /repo/.worktrees/thing is clean and idle
[worktree] would remove /repo/.worktrees/thing (add --execute)
[delete] would delete refs/heads/feature/thing at 91bd0e... (add --execute)
REHEARSED: every gate passed for feature/thing; nothing was destroyed
然后正式执行:
bun "<skill-dir>/scripts/retire-branch.mts" --branch feature/thing --execute
| 退出码 | 含义 | 应对措施 |
|---|---|---|
| 0 | 所有门控检查均已通过(且在使用 --execute 时,所有步骤均已执行) | 完成 |
| 1 | 门控检查根据证据拒绝了操作:尚未合入、工作树中有未完成工作、ref 已变更 | 阅读拒绝原因;其中会说明阻碍是什么 |
| 2 | 用法错误 | 修正调用方式 |
| 3 | 测量出错:ref 无法解析、git 执行失败、探测无法运行、克隆为浅克隆 | 问题从未得到答案;绝不要移除门控检查后重试 |
bun "<skill-dir>/scripts/retire-branch.mts" --help:
usage: retire-branch.mts --branch <name> [options]
--branch <name> the branch to retire (required)
--repo-root <path> repository to operate in (default: cwd)
--remote <name> remote to fetch the mainline from (default: origin)
--mainline <name> branch the work must have landed on (default: main)
--original-tip <sha> pre-rebase tip to also prove landed
--no-writer-check skip the lsof live-writer probe (hosts without a usable lsof)
--execute actually remove the worktree and delete the branch
prints: one [gate] line per step; without --execute the destructive steps read 'would ... (add --execute)' and
the run ends 'REHEARSED: every gate passed for <branch>; nothing was destroyed'
exits: 0 every requested step passed; 1 a gate refused on the evidence;
2 usage error; 3 broken measurement (the question was never answered)
--original-tip 用于分支在合入前经过变基的情况:旧 tip 也必须证明已合入。--no-writer-check 用于 lsof 无法给出答案的主机,运行会明确说明这一点。
压缩合并或变基合并会使分支的 sha 不在主线上,因此祖先关系检查会拒绝。脚本会将分支合并到 tip(git merge-tree --write-tree,git 2.38+):如果 tip 的树中包含这些内容,就表示分支没有携带主线所缺少的任何内容,包括主线后来对同一文件所做的编辑。脚本会打印通过了哪个门控检查:[landed] by ancestry 或 [landed] by three-way merge。各项门控检查的存在理由见:references/retire-branch.md。
移交工作树
所有权必须明确转移,绝不能靠推断。在任何时刻,一个工作树都恰好只有一个所有者:单个执行者,或已在其中授予互不重叠的逐文件负责区域的协调执行者(见下文“文件所有权”)。交接是一个有明确名称的事件(停止并授予所有权),不是根据沉默来猜测。
- 先停止前任。 向已完成或空闲的 Agent 发送消息会使其恢复运行。交接后发送确认或感谢消息会唤醒前任,令其恢复运行并写入其继任者当前拥有的工作树(导致活跃写入者覆盖内容)。顺序始终是:先停止执行者(在 Claude Code 中使用 TaskStop),之后无需再道别。
- 继任者在编辑前检查是否存在活跃写入者。 先检查 cwd 位于该树内的进程(与移除规则 3 中的 lsof 检查相同),然后对一个频繁变动的文件计算哈希,等待,再计算一次哈希(
shasum <file>; sleep 5; shasum <file>)。这些检查只能收集证据,不能证明不存在写入者:写入者可能在两次编辑之间处于空闲状态,也可能正在写入其他文件。所有权不变量仍然是上文所述的明确停止并授予所有权。 - 已移除工作树的分支归收集者所有。 停止执行者并移除其工作树,会将该分支转交给收集者,且只转交给收集者。该分支后续的修复应由一个全新的执行者在一个全新的工作树中完成。
- 绝不让已释放的执行者重新运行。 向已停止的执行者发送消息,会使其在一个已不存在的目录中恢复运行。一旦其工作树被移除,就绝不再向该执行者发送消息。
并行执行者之间的文件所有权
当多个执行者在同一个工作树中写入,或多轮执行者处理相同文件集时:
- 所有轮次中的每个文件都必须恰好只有一个所有者。 为每个执行者明确指定文件白名单。
- 后续轮次的文件清单应通过减去此前任何轮次涉及的所有文件来计算,依据文件清单,而不是“看起来已完成”。
- 只要前一轮的执行者可能仍在写入,就不能开始新一轮。
将文件重新分配给仍在负责它们的前一轮执行者,会导致写入竞争冲突和重复工作,即使精确匹配的编辑语义可以避免直接损坏也不例外。
一个分支,一个工作树
默认情况下,Git 拒绝检出已在任何工作树中检出的分支(“已被工作树使用”)。因此,一个分支至多由一棵工作树持有,其他人只有在持有者释放该分支后才能接手:
git -C <holding-tree> checkout --detach # or check out another branch there,
# or remove the worktree (rules above)
git checkout <branch> # only now succeeds elsewhere
此拒绝机制是防护措施,不是锁:git worktree add --force 和 git checkout --ignore-other-worktrees 可以绕过它。绝不要用它们接手仍由活跃工作树持有的分支。同一个分支被检出两次,意味着两个写入者在写入同一个引用。
应围绕这一防护机制规划,而不是试图对抗它。需要检出分支的操作(rebase、merge、堆栈工具)只能在持有该分支的工作树释放或移除后运行,绝不能在其所有者仍处于活跃状态时运行。
工作树共享主仓库的 .git
每个关联工作树都会保留一个小型私有 git 目录(HEAD、index、进行中的 rebase 或 bisect 状态、refs/worktree/*)。其他所有内容都通过主仓库的 git 目录($GIT_COMMON_DIR)共享:一份配置、一个远程列表、一套钩子,以及一个存放分支和标签的仓库。多个参与者并发操作时会产生以下影响:
-
git config写入会影响整个仓库。 某个参与者启用rerere、设置remote.pushDefault,或重写branch.<name>.*配置段,都会立即改变所有同级工作树的行为。只有启用了extensions.worktreeConfig,且写入目标是config.worktree时,才存在每个工作树独立的配置。否则,所有写入都是共享的。 -
身份信息是共享的。 在一个工作树中写入
user.email或user.name,会影响所有同级工作树的下一次提交。应通过命令级别设置身份(git -c user.email=...),或使用作用域限定在相应参与者的环境变量;其他参与者运行期间,切勿写入共享配置。 -
分支和标签是共享的。 在一个工作树中更新或删除分支,或移动标签,都会立即在所有工作树中生效。同级工作树若正准备对你刚删除的引用执行 rebase 或 merge,就会报错,而且无法诊断原因。因此,应协调对引用的修改,或安排在没有其他工作树运行时进行。
每个工作树独有的引用不受此影响:HEAD、
FETCH_HEAD和其他伪引用、refs/worktree/*、refs/bisect/*、refs/rewritten/*。 -
钩子默认是共享的。 在一个工作树中安装或编辑钩子,会改变所有同级工作树下一次提交执行的钩子。每个工作树独立的
core.hooksPath,或解析后因工作树而异的相对钩子路径,是例外。除此之外,都应视为共享。