RTK 的 /clean-worktrees 命令:自动批量清理已合并 Git Worktree 的完整实现剖析
本文围绕 rtk 仓库中 .claude/commands/clean-worktrees.md 命令定义展开:它是一条面向 Claude Code 的自动化清理命令,用于一键删除所有已合并进 master 分支的 git worktree,全程无需交互确认。读完本文,你能完整理解这条命令的三步执行流程(prune 失效引用、识别已合并 worktree、安全删除)、--dry-run 预览机制、五重安全护栏的实现细节,以及它与 rtk worktree 工作流中 /worktree、/worktree-status、/clean-worktree 等命令如何衔接成一套完整的隔离开发环境生命周期管理方案。
背景:rtk 的 worktree 工作流为什么需要自动清理
rtk 是一个用 Rust 编写的 CLI 代理工具(详见 CLAUDE.md 的项目概述),其贡献工作流重度依赖 git worktree 做隔离开发:/worktree 命令会按照约定把分支 feature/new-filter 创建到 .worktrees/feature-new-filter 目录(分支名中的 / 替换为 - 作为目录名),并在后台异步运行 cargo check;/worktree-status 命令负责查询后台检查结果。
当 PR 陆续合并回 master 后,这些 .worktrees/ 目录会不断堆积,占用磁盘空间并让 git worktree list 的输出日益冗长。人工逐个执行 git worktree remove 既低效又容易误删。.claude/commands/clean-worktrees.md 正是为此设计的"无人值守"清理命令——它的 frontmatter 明确声明:
---
model: haiku
description: Clean all merged worktrees automatically (no interaction)
---
model: haiku 表明该命令交给轻量模型执行(脚本本身就是确定性的 bash,不需要强推理),这也是 Claude Code custom command 的常见写法:文档即 prompt,内嵌脚本即实现。
命令对比:/clean-worktrees 与 /clean-worktree
仓库中同时存在两条清理命令,定位不同,文档开头的对比表是理解边界的关键:
| 命令 | 定义文件 | 交互方式 | 适用场景 |
|---|---|---|---|
/clean-worktree(单数) |
.claude/commands/clean-worktree.md |
交互式,删除前询问 [y/N] 确认 |
想逐项审查后再清理 |
/clean-worktrees(复数) |
.claude/commands/clean-worktrees.md |
全自动,一次删除所有已合并分支的 worktree | 合并 PR 后批量清理、定时维护 |
此外还有 /tech:remove-worktree 用于删除指定单个 worktree(支持未合并分支的强删确认),三者共同构成"创建 → 检查 → 单个删除 → 批量清理"的闭环。
使用方法
原文档定义的用法非常简洁:
/clean-worktrees # Remove all merged worktrees
/clean-worktrees --dry-run # Preview what would be deleted
- 不带参数:实际执行删除;
- 带
--dry-run:只预览"将要删除什么",不做任何修改,适合在创建新 worktree 前评估磁盘状况。
完整实现脚本剖析
命令文档的核心是"Execute this script"——把以下 bash 脚本作为可执行规范交给 Claude Code 运行。下面完整继承原文档脚本,并按其内部逻辑分三段讲解:
#!/bin/bash
set -euo pipefail
DRY_RUN=false
if [[ "${ARGUMENTS:-}" == *"--dry-run"* ]]; then
DRY_RUN=true
fi
echo "Cleaning Worktrees"
echo "=================="
echo ""
# Step 1: Prune stale git references
echo "1. Pruning stale git references..."
PRUNED=$(git worktree prune -v 2>&1)
if [ -n "$PRUNED" ]; then
echo "$PRUNED"
echo "Stale references pruned"
else
echo "No stale references found"
fi
echo ""
# Step 2: Find merged worktrees
echo "2. Finding merged worktrees..."
MERGED_COUNT=0
MERGED_BRANCHES=()
CURRENT_DIR="$(pwd)"
while IFS= read -r line; do
path=$(echo "$line" | awk '{print $1}')
branch=$(echo "$line" | grep -oE '\[.*\]' | tr -d '[]' || true)
[ -z "$branch" ] && continue
[ "$branch" = "master" ] && continue
[ "$branch" = "main" ] && continue
[ "$path" = "$CURRENT_DIR" ] && continue
if git branch --merged master | grep -q "^[* ] ${branch}$" 2>/dev/null; then
MERGED_COUNT=$((MERGED_COUNT + 1))
MERGED_BRANCHES+=("$branch|$path")
echo " - $branch (merged)"
fi
done < <(git worktree list)
if [ $MERGED_COUNT -eq 0 ]; then
echo "No merged worktrees found"
echo ""
echo "Current worktrees:"
git worktree list
exit 0
fi
echo ""
echo "Found $MERGED_COUNT merged worktree(s)"
echo ""
if [ "$DRY_RUN" = true ]; then
echo "DRY RUN - No changes will be made"
echo ""
echo "Would delete:"
for item in "${MERGED_BRANCHES[@]}"; do
branch=$(echo "$item" | cut -d'|' -f1)
path=$(echo "$item" | cut -d'|' -f2)
echo " - $branch"
echo " Path: $path"
done
echo ""
echo "Run without --dry-run to actually delete"
exit 0
fi
# Step 3: Remove merged worktrees
echo "3. Removing merged worktrees..."
REMOVED_COUNT=0
for item in "${MERGED_BRANCHES[@]}"; do
branch=$(echo "$item" | cut -d'|' -f1)
path=$(echo "$item" | cut -d'|' -f2)
echo ""
echo "Removing: $branch"
if git worktree remove "$path" 2>/dev/null; then
echo " Worktree removed"
else
echo " Git remove failed, forcing..."
rm -rf "$path" 2>/dev/null || true
git worktree prune 2>/dev/null || true
echo " Worktree forcefully removed"
fi
if git branch -d "$branch" 2>/dev/null; then
echo " Local branch deleted"
else
echo " Local branch already deleted"
fi
if git ls-remote --heads origin "$branch" 2>/dev/null | grep -q "$branch"; then
echo " Remote branch exists: origin/$branch (not auto-deleted)"
fi
REMOVED_COUNT=$((REMOVED_COUNT + 1))
done
echo ""
echo "Cleanup complete"
echo ""
echo "Summary:"
echo " Removed: $REMOVED_COUNT worktree(s)"
echo ""
echo "Remaining worktrees:"
git worktree list
echo ""
WORKTREES_SIZE=$(du -sh .worktrees/ 2>/dev/null | awk '{print $1}' || echo "N/A")
echo "Worktrees disk usage: $WORKTREES_SIZE"
参数解析:从 $ARGUMENTS 到 DRY_RUN 开关
Claude Code 会把命令后跟的参数注入 ${ARGUMENTS} 变量。脚本开头用 [[ "${ARGUMENTS:-}" == *"--dry-run"* ]] 做子串匹配——注意 :- 默认值写法保证了不带参数时不会触发 set -u 的 unbound variable 错误。这是 bash 严格模式(set -euo pipefail)下处理可选参数的典型防御写法。
Step 1:git worktree prune -v 清理失效引用
worktree 的元数据记录在 .git/worktrees/ 下,但如果 worktree 目录被外部手段(如直接 rm -rf)删除过,git 仍会保留一条"僵尸"记录。git worktree prune -v 会在正式扫描前把这些陈旧引用清掉,且用 -v 把被清除的条目打印出来——如果输出非空([ -n "$PRUNED" ]),脚本会明确提示 "Stale references pruned",否则报告 "No stale references found"。这一步确保 Step 2 遍历的 git worktree list 结果是干净可信的。
Step 2:解析 worktree 列表,筛出已合并分支
这是整个脚本的识别核心,逐行拆解其解析策略:
- 数据来源:
git worktree list的人类可读输出。每行形如/path/to/worktree [branch-name](主仓库行末标注(main repo),无分支方括号)。 - 路径提取:
awk '{print $1}'取第一个空白分隔字段作为 worktree 物理路径。 - 分支提取:
grep -oE '\[.*\]' | tr -d '[]'从方括号中提取分支名。末尾的|| true很关键——在set -o pipefail下,主仓库那一行没有方括号时grep会返回非零退出码,若不加|| true整个脚本会直接终止;随后[ -z "$branch" ] && continue把无分支的行(即主仓库)跳过。 - 三重跳过保护:空分支、
master/main(受保护主干)、$CURRENT_DIR(当前工作目录,防止把用户正站在其中的仓库删掉)。 - 合并判定:
git branch --merged master列出所有已合入master的本地分支,再用grep -q "^[* ] ${branch}$"做行锚定精确匹配(*表示当前分支前缀,表示普通分支,$防止前缀误匹配,如fix/a误中fix/a-b)。
命中的条目以 分支|路径 的形式存入 MERGED_BRANCHES 数组,同时实时打印 - $branch (merged) 清单。若一个都没找到,脚本打印当前 git worktree list 后以 exit 0 正常结束——这是"幂等"设计:重复执行无副作用。
--dry-run 分支:只报告,不执行
识别完成后,若 DRY_RUN 为 true,脚本进入纯预览分支:逐项输出 Would delete: 清单(分支名 + 物理路径),提示 "Run without --dry-run to actually delete",然后 exit 0。此时不会触碰任何 worktree、分支或磁盘数据,可以安全地用于"先看后删"的评估场景。
Step 3:删除 worktree + 本地分支,保留远端
对数组中的每个条目依次执行三级操作,每一级都有降级路径:
git worktree remove "$path":正规删除(会校验目录状态)。失败时降级为强制清理:rm -rf "$path"后紧跟git worktree prune消除残留引用,并打印 "Worktree forcefully removed"。这正是 git 官方对"目录脏了 remove 失败"场景推荐的手工兜底组合。git branch -d "$branch":只用安全删除(-d,要求已合并,本脚本中该条件必然成立)。失败时不降级为-D,而是如实报告 "Local branch already deleted"——与/tech:remove-worktree中对未合并分支提供git branch -D强删形成刻意的安全梯度差异。- 远端分支探测:
git ls-remote --heads origin "$branch"查询origin上是否还存在同名分支;存在时只报告 "Remote branch exists: origin/$branch (not auto-deleted)",绝不自动删除。远端删除属于不可逆的共享状态变更,被刻意排除在自动流程之外。
收尾输出:可审计的执行摘要
脚本结尾打印三段信息,让每次执行都留下可核查的痕迹:
Summary: Removed: N worktree(s)——本次删除计数;git worktree list——清理后的完整存量清单;du -sh .worktrees/的磁盘占用(取du输出的第一列人类可读大小,失败时回退为N/A)——直接呼应 rtk worktree 约定统一存放于.worktrees/目录(见.claude/commands/worktree.md中WORKTREE_DIR="$REPO_ROOT/.worktrees/$WORKTREE_NAME"的路径构造逻辑)。
安全特性(Safety Features)汇总
原文档列出的五重安全护栏,与脚本代码一一对应:
- 只删已合并分支——
git branch --merged master是唯一的入选条件,未合并分支永远不会进入删除队列; master与main受保护——Step 2 中显式continue跳过,双主干命名都覆盖;- 绝不删除当前工作目录——
[ "$path" = "$CURRENT_DIR" ] && continue防止脚本运行时把脚下仓库误删; --dry-run预览模式——任何删除前都可以零风险验证将要发生的事;- 远端分支只报告不删除——
git ls-remote探测后仅打印提示,远端状态由人决定。
另外从脚本结构看还有两处文档未逐条列出但同样重要的保护:set -euo pipefail 严格模式下任何意外失败都会立即中止而非带着中间错误继续执行;grep 分支匹配使用 ^[* ] ...$ 锚定,避免分支名前缀误伤。
何时使用
原文档给出的三个典型时机:
- 合并 PR 之后:
/clean-worktrees立即回收已交付的开发环境; - 每周例行维护:作为固定节奏的磁盘与元数据清理;
- 创建新 worktree 之前:先跑
/clean-worktrees --dry-run预览现状,避免.worktrees/下同名目录冲突(.claude/commands/worktree.md的 Troubleshooting 一节明确列出了 "worktree already exists" 这一常见故障,其根治手段正是先做清理)。
手动删除未合并分支(命令的边界外场景)
/clean-worktrees 有意不碰未合并分支。若确实需要手工清理某个未合并的 worktree,原文档给出了对应的逃生通道:
git worktree remove --force .worktrees/feature-name
git branch -D feature/name
git worktree prune
这三条命令分别强制移除 worktree 目录、强删本地分支(-D 忽略合并状态)、清除 git 残留引用。需要注意的是 --force + -D 组合会丢弃未合并的工作,仓库中交互式的 /tech:remove-worktree 命令正是把这三步封装成了带"未合并工作将被删除 [y/N]"确认提示的安全流程,可优先于裸命令使用。
小结:一条命令背后的工程约定
/clean-worktrees 的价值不止于一段 bash 脚本:它是 rtk 仓库整套 worktree 约定(.worktrees/ 统一目录、category/description 分支命名、.gitignore 中的 .worktrees/ 忽略规则、后台 cargo check)的收尾环节。通过"先 prune 清脏、再精确识别已合并项、最后分级删除且保留远端"的三步设计,它在无人值守执行的前提下,把误删风险压缩到接近零——这正是把 git 的散装命令(worktree list/prune/remove、branch --merged、ls-remote)编排成可复用的团队级运维原语的思路,对任何重度使用 git worktree 的仓库都有直接的借鉴价值。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00