git-worktrees:打造开发会话的无损隔离沙箱
对应官方文档:claude-code-docs/docs/common-workflows.md 里的 Run parallel Claude Code sessions with Git worktrees 和 Copy gitignored files to worktrees。
先搞清楚这是什么
--worktree(以及配套的 .worktreeinclude)最容易被误解为一种顺带手运行的 Git 小工具:帮你建个目录、切个分支,然后就和普通会话没区别了。
但在源码里,它的定位是会话级的“隔离容器”。 Claude Code 不是简单地创建一个 git worktree,而是把后续这一整场 session 的核心状态(CWD、Project Root、Settings 读取、退出清理决策以及 --resume 恢复逻辑)全部切到了那份副本之上。它解决的是在不污染主仓库、不丢失本地配置的前提下,进行并行、可丢弃、可恢复的开发实验。
运行时的真相
实现这套“容器化会话”的核心邏辑在于 src/utils/worktree.ts:
- 会话外壳初始化:启动时检测到
--worktree,src/setup.ts会先执行createWorktreeForSession(...)。这里会生成一个 slug 标识符,并进行严格校验,防止 worktree 路径利用..逃逸出.claude/worktrees/。 - 创建与恢复路径:
- Git 默认后端:源码并不总是执行
git worktree add。它会先通过readWorktreeHeadSha探测目标目录是否已有合法的 Git 指针。如果存在,则走“快回(Fast Resume)”路径,实现无损的会话恢复。 - 默认分支选择:基线分支的确定在
src/utils/git/gitFilesystem.ts。它优先读取本地refs/remotes/origin/HEAD的指向,找不到才回退到main/master探测。这解释了为什么它不需要联网就能分叉。
- Git 默认后端:源码并不总是执行
- 播种(Post-creation Setup):新副本创建后,
performPostCreationSetup(...)会同步几项关键物料,让它“立即具备生产力”:- Settings 同步:复制
settings.local.json。 - Hook 共享:通过
core.hooksPath让 worktree 共享主仓库的 Git 挂载逻辑。 .worktreeinclude白名单:这是一个双重过滤逻辑(模式匹配 + 事实被 Git 忽略)。源码会扫描.worktreeinclude语法,只把那些本应被 gitignore 但被你显式点名的本地物料(如.env)抄过去。它通过git ls-files --others --ignored进行确认,避免了盲目复制。
- Settings 同步:复制
- 清理与退出:
WorktreeExitDialog.tsx里的逻辑非常人性化。如果会话结束时 worktree 是干净的(无未提交改动/无新增 commit),它会静默自动清理;如果有改动,则弹窗让用户选择keep(保留副本及分支以供下次 resume)或cleanup(物理删除)。
使用时的关键约束
- 双重过滤约束:
.worktreeinclude只能抄那些被 gitignore 的文件。如果一个文件没进.gitignore也没被 track,写在 include 里也是白费,源码不会去碰。 - 后端可插拔性:虽然默认是 Git,但源码预留了
WorktreeCreate/WorktreeRemovehook。如果你配置了这些 hook,Claude Code 可以将“隔离副本”交给自定义的后端。 - 不改变持久设置:worktree 是会话级的。虽然你可以修改 worktree 内的
.claude/settings.json,但这不会反向影响你主仓库的设置。 - Mid-session 差异:工具
/enter_worktree与启动参数--worktree底层逻辑一致,但前者不会修改Project Root,更像是一次临时任务分支;而后者将副本视为整个 session 的根本。
推荐阅读路径
- 如果你关心本地配置如何在不同 worktree 间共享,下一篇该看 settings / hierarchy。
- 如果你关心 worktree 失败时如何自动回滚,去看 worktree.ts 里的
tearDown逻辑。 - 如果你想看并行 session 为什么能自动隔离,去看 bridgeMain.ts 里的
spawnMode === 'worktree'分流。
源码锚点
claude-code-opensource/src/utils/worktree.ts:整个 worktree 机制的核心:路径校验、创建/恢复、副本播种、清理。
📄 src/utils/worktree.ts — 整个 worktree 机制的核心:路径校验、创建/恢复、副本播种、清理。
typescript
const VALID_WORKTREE_SLUG_SEGMENT = /^[a-zA-Z0-9._-]+$/
const MAX_WORKTREE_SLUG_LENGTH = 64
/**
* Validates a worktree slug to prevent path traversal and directory escape.
*
* The slug is joined into `.claude/worktrees/<slug>` via path.join, which
* normalizes `..` segments — so `../../../target` would escape the worktrees
* directory. Similarly, an absolute path (leading `/` or `C:\`) would discard
* the prefix entirely.
*
* Forward slashes are allowed for nesting (e.g. `asm/feature-foo`); each
* segment is validated independently against the allowlist, so `.` / `..`
* segments and drive-spec characters are still rejected.
*
* Throws synchronously — callers rely on this running before any side effects
* (git commands, hook execution, chdir).
*/
export function validateWorktreeSlug(slug: string): void {
if (slug.length > MAX_WORKTREE_SLUG_LENGTH) {
throw new Error(
`Invalid worktree name: must be ${MAX_WORKTREE_SLUG_LENGTH} characters or fewer (got ${slug.length})`,
)
}
// Leading or trailing `/` would make path.join produce an absolute path
// or a dangling segment. Splitting and validating each segment rejects
// both (empty segments fail the regex) while allowing `user/feature`.
for (const segment of slug.split('/')) {
if (segment === '.' || segment === '..') {
throw new Error(claude-code-opensource/src/utils/git/gitFilesystem.ts:默认分支探测与基线选择逻辑。
📄 src/utils/git/gitFilesystem.ts — 默认分支探测与基线选择逻辑。
typescript
const resolveGitDirCache = new Map<string, string | null>()
/** Clear cached git dir resolutions. Exported for testing only. */
export function clearResolveGitDirCache(): void {
resolveGitDirCache.clear()
}claude-code-opensource/src/setup.ts:--worktree启动路径与 CWD/Project Root 切换。
📄 src/setup.ts — `--worktree` 启动路径与 CWD/Project Root 切换。
typescript
getProjectRoot,
getSessionId,
setOriginalCwd,
setProjectRoot,
switchSession,
} from './bootstrap/state.js'
import { getCommands } from './commands.js'claude-code-opensource/src/components/WorktreeExitDialog.tsx:决定是“静默清理”还是“弹窗询问”的裁决逻辑。
📄 src/components/WorktreeExitDialog.tsx — 决定是“静默清理”还是“弹窗询问”的裁决逻辑。
tsx
function recordWorktreeExit(): void {
/* eslint-disable @typescript-eslint/no-require-imports */
;
(require('../utils/sessionStorage.js') as typeof import('../utils/sessionStorage.js')).saveWorktreeState(null);
/* eslint-enable @typescript-eslint/no-require-imports */
}claude-code-opensource/src/utils/sessionRestore.ts:--resume如何拉起存量 worktree 状态。
📄 src/utils/sessionRestore.ts — `--resume` 如何拉起存量 worktree 状态。
typescript
type AgentDefinition,
type AgentDefinitionsResult,
getActiveAgentsFromList,
getAgentDefinitionsWithOverrides,
} from '../tools/AgentTool/loadAgentsDir.js'
import { TODO_WRITE_TOOL_NAME } from '../tools/TodoWriteTool/constants.js'