Glass Box Terrarium, explained from the inside

Last updated · Latest source on GitHub ↗

Git & GitHubLesson

Git worktrees

Several agents editing one checkout at once trip over each other: one switches the branch under another, or commits another's half-finished files. A git worktree gives each agent a working folder of its own, while they all share one repository.

Know git worktrees already? Skip to In Terrarium.

Outside the box · General

The concept

A worktree is a second working folder of the same repository, with its own files and its own checked-out branch, sharing one history. git worktree add <path> <branch> creates one, git worktree list shows them all, and git worktree remove <path> deletes one. Two worktrees can't check out the same branch at once.

A worktree is a fresh checkout, so it has none of the main folder's untracked files, such as installed dependencies. Uncommitted work in it shows only in that folder's git status, and deleting the folder deletes that work.

Claude Code can give each subagent a fresh worktree. It creates the worktree under .claude/worktrees/, from the default branch unless the settings say to branch from the current HEAD, and removes it when the subagent finishes without changes.

Full referenceGit docs: git-worktree

Looking inside

In Terrarium

The dispatch , .agents/skills/dispatch-subagents/SKILL.md, tells apart the ways a worktree appears here:

.agents/skills/dispatch-subagents/SKILL.md ↗18–25as of
18- **A dispatched subagent that touches git:** pass `isolation: 'worktree'` on the
19  Agent call. Without it, parallel agents share one checkout and race on
20  branches.
21- **`EnterWorktree`/`ExitWorktree`** moves your whole session. Use only when the
22  user says "worktree".
23- **Manual `git worktree add`** is for your own one-off inspection, never a
24  subagent's brief. Put it under `.claude/worktrees/NAME/`: path-scoped rules
25  don't load for files outside the project folder (issue #1944).

Each ends up under .claude/worktrees/, inside the project folder, which .gitignore leaves out of git. A subagent's brief has it run pnpm install in the new worktree and check that its HEAD is the commit the brief names.

Worktrees are swept before a session ends. pnpm check:worktrees and the session-log script's close step both run scripts/git/check-worktrees.ts, which lists every worktree with git worktree list and runs git status in each. The rule it applies:

scripts/git/check-worktrees.ts ↗159–161as of
159    const uncommitted = s.dirty
160    const unpushed = (s.unpushedCount ?? 0) > 0
161    const needsRescue = !s.isPrimary && (uncommitted || unpushed)

The main folder never fails. A linked worktree fails on any uncommitted change, untracked files included, or on a commit that no remote-tracking branch holds. Cases that hold nothing to rescue, such as a worktree whose HEAD is already in origin/main, are reported but excused; the script's EXCUSALS table lists them.

When a session closes, scripts/session/log-session.ts runs the same sweep. It removes each clean linked worktree whose HEAD is already in origin/main, except the one it runs in or one sitting at the origin/main tip, and it refuses a completed or partial log while another linked worktree holds work to rescue. For any other status it records a Friction instead.

The session log guards worktrees too: unless told otherwise, scripts/session/log-session.ts won't write a log from a linked worktree, which it tells by git's two directories:

scripts/session/log-session.ts ↗208–216as of
208export function isLinkedWorktree(cwd: string): boolean {
209  try {
210    const gitDir = git(['rev-parse', '--git-dir'], cwd).trim()
211    const commonDir = git(['rev-parse', '--git-common-dir'], cwd).trim()
212    return resolve(cwd, gitDir) !== resolve(cwd, commonDir)
213  } catch {
214    return false
215  }
216}

A subagent there shares its parent's session, and a log it wrote would replace the parent's (Subagents).

Why it's built this way

  • One checkout per agent. Parallel agents in one checkout raced on branches. A worktree per agent that touches git ends the race. The cost: each worktree needs its own install. .agents/skills/dispatch-subagents/SKILL.md.
  • A pinned start. A new worktree can start from an older main, so an agent that trusted it would build on stale code. Checking the commit before work starts closes that gap. The cost: one more step in every brief. .agents/skills/dispatch-subagents/SKILL.md.
  • Inside the project folder. Path-specific rules load only for files under the project folder, so an agent in a worktree beside the repo edited files with none of their rules loaded. Hand-made worktrees go under .claude/worktrees/, where Claude Code already puts a subagent's. The cost: the folder must stay git-ignored. .agents/skills/dispatch-subagents/SKILL.md.
  • A sweep for lost work. "Commit before you stop" as an instruction failed: one agent ended its turn waiting on a background check, then died silently once resumed. An instruction can't catch an agent that never returns, so a script reads git's own list of worktrees and fails on left-behind work. It shows the loss; it can't prevent it. scripts/git/check-worktrees.ts.

What to watch for

  • Make your own under .claude/worktrees/. A worktree outside the project folder misses the project's path-specific rules when an agent reads its files.
  • A fresh worktree has no node_modules. Run pnpm install there before pnpm gate:scoped or any test.
  • A squash-merged worktree with leftover changes still fails the sweep. Its HEAD never reaches main, so the leftovers aren't excused; remove the worktree once its PR merges.

Try it in your project

Add .claude/worktrees/ to your .gitignore. Run git worktree add .claude/worktrees/try-it -b try-it, change a file there, and run git status in both folders: each sees only its own change. Then run git worktree list, and remove the extra folder with git worktree remove .claude/worktrees/try-it.

Where next

Walkthroughs through this Lesson

Last updated · Latest source on GitHub ↗

Something went wrong

This content couldn’t be loaded. Reloading the page usually fixes it.

Technical details
route: /t/glassbox/guide/git-github/git-worktrees