Git 工作树很好用,却止步于仓库边界
Git 工作树(worktree)确实好用,但它的作用范围只限于单个仓库,一跨出仓库边界就失效了。
那些讲解 worktree 的文章,讲对了什么
Git 工作树(git worktree add)已经成为现代软件工程和智能体(agent)工作流中不可或缺的基础操作。大多数社区文章在讲怎么用 git 工作树服务 AI 编码智能体时,对单仓库场景下的核心机制都讲得很到位:
- 无需 stash,也不用来回切换上下文:你可以在一个隔离的特性分支上启动智能体会话,不必 stash 未提交的本地改动,也不用改变当前的工作目录。
- 共享对象库:工作树与父仓库共用
.git对象库。创建一个工作树只要毫秒级,因为 Git 只是写了一个轻量的.git文本文件,指向主仓库里的.git/worktrees/<id>,省去了复制整份仓库带来的磁盘和网络开销。 - 各自独立的索引文件:每棵工作树都有自己的暂存区(index)、HEAD 引用指针和 reflog。这样多个智能体会话可以在不同特性分支上并行跑,不会遇到锁争用或文件冲突。
对单仓库工程来说,git 工作树确实提供了理想的本地执行环境。智能体在独立的文件树中操作,语言服务器能正确索引文件,构建工具原生运行,git status 也能干净地追踪改动。
但现代软件应用很少只待在一个仓库里。一旦某个功能跨多个仓库,单仓库的工作树模型就会撞上一道硬性的结构边界。
三仓库改动
设想多仓库(polyrepo)生态里一个很常见的架构任务:修改 API 契约。这个功能横跨三个独立的 git 仓库:
- api-service:用 Rust 实现更新后端点的服务。
- web-frontend:消费 API 响应的 TypeScript 应用。
- docs-site:存放公开 API 参考文档的文档仓库。
要用 AI 编码智能体完成这次改动,你需要一套复合工作区配置:
- 在 api-service 的
feature-auth分支上具备编辑权限。 - 在 web-frontend 的
feature-auth分支上具备编辑权限。 - 在 docs-site 的默认分支
main上具备只读参考权限。
手动敲原生 git worktree 命令,得在三个不同的仓库路径之间走完一长串繁琐步骤:
# Manual worktree creation across three repos
git -C ~/repos/api-service worktree add -b feature-auth ../worktrees/api-feature-auth
git -C ~/repos/web-frontend worktree add -b feature-auth ../worktrees/web-feature-auth
git -C ~/repos/docs-site worktree add ../worktrees/docs-main main
# Create a temporary workspace directory and symlink worktrees
mkdir -p ~/workspaces/feature-auth
ln -s ~/worktrees/api-feature-auth ~/workspaces/feature-auth/api
ln -s ~/worktrees/web-feature-auth ~/workspaces/feature-auth/web
ln -s ~/worktrees/docs-main ~/workspaces/feature-auth/docs
每做一个功能都要手动执行一遍,又慢又重复,还容易出错。于是开发者干脆写 shell 脚本,把多仓库的 worktree 组装过程自动化。
还是自己写脚本吧
为了给 AI 智能体会话自动搭建多仓库的 worktree,开发者往往会写一段大约 60 行的 bash 脚本,下面就是一个典型例子:
#!/usr/bin/env bash
set -euo pipefail
FEATURE_NAME="${1:-}"
if [[ -z "$FEATURE_NAME" ]]; then
echo "Error: Feature name required."
echo "Usage: $0 <feature-name>"
exit 1
fi
WORKSPACE_DIR="$HOME/.workspaces/$FEATURE_NAME"
mkdir -p "$WORKSPACE_DIR"
declare -A REPOS=(
["api"]="$HOME/repos/api-service"
["web"]="$HOME/repos/web-frontend"
["docs"]="$HOME/repos/docs-site"
)
echo "Initializing multi-repo worktrees for feature: $FEATURE_NAME"
for NAME in "${!REPOS[@]}"; do
REPO_PATH="${REPOS[$NAME]}"
TARGET_WORKTREE="$REPO_PATH/../worktrees/$NAME-$FEATURE_NAME"
if [[ "$NAME" == "docs" ]]; then
# Read-only reference repository stays checked out on main
echo "Creating reference worktree for $NAME on main..."
git -C "$REPO_PATH" worktree add "$TARGET_WORKTREE" main 2>/dev/null || true
else
# Feature repositories check out feature branch
echo "Creating feature worktree for $NAME on branch $FEATURE_NAME..."
git -C "$REPO_PATH" worktree add -b "$FEATURE_NAME" "$TARGET_WORKTREE" 2>/dev/null || true
fi
# Symlink worktree into unified workspace folder
ln -sfn "$TARGET_WORKTREE" "$WORKSPACE_DIR/$NAME"
done
# Create combined harness instructions
cat <<EOF > "$WORKSPACE_DIR/CLAUDE.md"
# Multi-Repo Workspace Context: $FEATURE_NAME
- api/: Rust backend service (editable feature worktree)
- web/: TypeScript web application (editable feature worktree)
- docs/: Documentation site (reference context only, DO NOT EDIT)
EOF
echo "Multi-repo workspace materialized at: $WORKSPACE_DIR"
cd "$WORKSPACE_DIR" && claude
这个脚本会自动创建 worktree,把各个目录软链接到一个统一的工作区文件夹里,生成一份基础的 CLAUDE.md,然后启动智能体的执行框架。
用来跑本地简单演示,这段脚本没问题。但要是拿到生产级的工程环境里用,就会暴露出四个根本性的短板。
脚本做不到的四件事
这种临时写的 shell 脚本,虽然能靠软链接把目录拼到一起,但要让多个仓库的智能体安全、稳定地运行,它缺少必要的技术底座。
保护对参考仓库的写入——脚本的做法却是:仅靠一条软性提示规则来约束。让分支与生命周期对齐——脚本的做法却是:留下脏工作树和孤儿树。把指令真正落地——脚本的做法却是:弄脏每个仓库里已被跟踪的文件。交付功能——脚本的做法却是:把每个分支各自推送。1. POSIX 文件系统写保护
shell 脚本用普通用户写权限(0755/0644)在磁盘上创建 worktree。要限制对 docs/ 的修改,脚本完全依赖 CLAUDE.md 里的一句文本指令:“docs/:仅供参考,切勿编辑”。在上下文负载较高或多步推理任务中,语言模型会不定时违反提示词指令。智能体在修构建或 lint 错误时,会直接改 docs/ 里的文件,从而改动参考仓库的 git 状态。脚本没法在操作系统层面强制实施写限制。
2. 会话生命周期与分支对齐
智能体会话结束或崩溃时,脚本会在多个仓库目录里留下活跃的 worktree 和悬空分支。如果会话中途失败:
- Worktree 锁的引用会残留在各个仓库的
.git/worktrees/里。 - 重新运行脚本会报错,因为分支或 worktree 路径已存在。
开发者必须逐个进入每个仓库目录,手动运行 git worktree prune,再删掉孤儿功能分支。另外,如果某个仓库创建分支失败(比如 main 上有未提交的冲突),脚本会让整个工作区处于部分初始化、跨仓库不一致的状态,而且没有原子回滚。
3. 干净的指令落地
脚本通过在工作区根目录写一个静态 CLAUDE.md 来注入指令。但真实仓库里本来就有各自厂商原生的指令文件(例如 api/CLAUDE.md 和 web/CLAUDE.md)。脚本没法跨仓库合并或综合这些指令。
如果脚本直接把指令文件写进各个代码仓库的 worktree(工作树),git status 里就会多出未跟踪的脏文件,智能体可能不小心把它们提交上去。
4. 多仓库功能交付
智能体在 api/ 和 web/ 里改完代码后,要交付这个功能,就得验证状态、往多个仓库推送功能分支,还要建立跨仓库的 Pull Request 引用。而 shell 脚本把交付全甩给了手动 git 操作。它没法验证挂载的多个仓库是否已原子化地准备好一个功能,没法追踪这次会话实际改了哪些仓库,也没法触发多仓库同步推送分支(deliver)。
注意,多仓库交付协调的是多个仓库之间的推送操作,而不是去搞复杂的依赖排序或自动合并顺序。
用 chmod 守护写入,而不是靠提示词
自定义多仓库 worktree 脚本最核心的漏洞,就是靠 LLM 遵守提示词来做写保护。软性提示词和软件钩子在上下文饱和、智能体复杂推理或直接执行 shell 命令时,迟早会失效。可靠的写保护需要在操作系统层面用 POSIX 文件权限掩码(chmod)强制文件不可修改。
if mode & 0o222 != 0 {
chmod(path, mode & !0o222)?;
}
在编排式视图目录(View Directory)架构中:
未提升的上下文 worktree:当某个参考仓库(比如 docs/)被挂载进会话工作区时,通过 clear_write_bits 递归地去掉它 worktree 中所有文件和目录的写位(0o222)。
内核确定性强制:如果智能体试图修改未提升仓库里的文件——不管是使用文件编辑工具、bash 输出重定向(>),还是自动代码修改器——操作系统内核都会立刻以 EACCES(权限拒绝)错误拒绝这次写入。