Git Worktrees and Parallel Agents

One repository can have multiple working trees checked out at once. That is the whole feature. This page covers the mental model, the commands, the rules git enforces, and why worktrees are the standard isolation mechanism when several coding agents work on the same repo in parallel.

Sanitized — no real hosts, paths, or project names. This is the capstone of the Git series (reading order in _Software-Development-MOC) and reuses its sensor-logger example; it leans especially on the database model from Git-from-First-Principles.


The mental model: one database, many mount points

A git repo is two separable things that git clone happens to give you together:

  1. The object database — everything under .git/: commits, blobs, branches, stashes, config. This is the repo.
  2. A working tree — the checked-out files you edit, plus a private HEAD and index (staging area). This is just a view of one commit, materialized as files.

git worktree add creates a second working tree attached to the same object database. The analogy that holds up: the object database is a filesystem, and each worktree is a mount point — same data underneath, multiple independent views, each positioned somewhere different (a different branch). Or in process terms: worktrees are like threads sharing one address space (the object DB), where each thread gets its own private stack (HEAD + index + working files).

$ cd ~/src/sensor-logger              # main worktree, on branch main
$ git worktree add ../sensor-logger-fix42 -b fix-42

$ git worktree list
/home/dev/src/sensor-logger        a1b2c3d [main]
/home/dev/src/sensor-logger-fix42  a1b2c3d [fix-42]

Now ../sensor-logger-fix42/ is a full checkout on branch fix-42. You can build there, run tests there, and commit there — while ~/src/sensor-logger sits untouched on main.

The second worktree is nearly free. Its .git is not a directory but a one-line file pointing back at the main repo:

$ cat ../sensor-logger-fix42/.git
gitdir: /home/dev/src/sensor-logger/.git/worktrees/sensor-logger-fix42

No objects are copied. A commit made in any worktree lands in the shared database and is instantly visible from all the others — no push, no pull, because there is only one repo.

What each worktree owns

The essentials from Git-from-First-Principles, applied here: a branch is a moving pointer in the shared database; a worktree is a checkout standing on one. Each worktree gets a private copy of exactly the two per-checkout things — HEAD (which branch you’re on) and the index (what you’ve staged) — and for a linked worktree those live under .git/worktrees/<name>/ in the main repo.

That’s the mechanical reason worktrees are independent: agent A staging files in worktree A touches its own index, so agent B’s git status in worktree B never sees it. Shared history, private “where am I” and private “what’s staged.”

The lifetime asymmetry is what makes the agent pattern work: git worktree remove ../sensor-logger-fix42 deletes the directory, but the branch and its commits survive in the database. The worktree is scaffolding; the branch is the deliverable.

Why not just a second clone?

A second git clone also gives you an independent directory, but:

  • Clones don’t share anything. Moving a commit between two clones means push/pull through a remote (or git fetch from a local path). Worktrees share commits, branches, and stashes the instant they’re created.
  • Clones duplicate the object database — real disk and a second fetch history to maintain.
  • Clones drift. Two clones each have their own view of the remote; worktrees can’t disagree about what commits exist.

Use a clone when you want true isolation (different remotes, different config). Use a worktree when it’s the same project and you just need another checkout.

The commands

git worktree add ../proj-fix42 -b fix-42     # new branch, new worktree
git worktree add ../proj-17 origin/issue-17  # detached checkout of an existing ref
git worktree list                            # every worktree + its branch
git worktree remove ../proj-fix42            # delete the checkout (branch survives)
git worktree prune                           # clean metadata after a manual rm -rf

remove deletes the working directory only. The branch and its commits stay in the shared database — merge or delete them like any other branch.

The rules git enforces

  • One branch, one worktree. Git refuses to check out a branch that another worktree already has checked out. Two views writing the same branch’s HEAD would corrupt each other — same reason two processes don’t share a write file descriptor without locking.
  • Each worktree has its own HEAD, index, and untracked files. git status in one worktree says nothing about another.
  • Untracked files don’t teleport. Only committed objects are shared. Build artifacts, node_modules/, .venv/ — each worktree needs its own (rerun npm install / uv sync per worktree).
  • Don’t rm -rf a worktree directory by hand. Git keeps per-worktree metadata under .git/worktrees/; a manual delete strands it. git worktree remove does both, git worktree prune cleans up after the fact.

How parallel agents use worktrees

The problem: two agents editing files in the same working tree trample each other — agent A’s test run picks up agent B’s half-written file, git status mixes both change sets, and neither can commit cleanly. It’s two processes writing one directory with no locking.

The fix is one worktree per agent:

git worktree add ../worktrees/issue-17 -b issue-17
git worktree add ../worktrees/issue-23 -b issue-23
git worktree add ../worktrees/issue-31 -b issue-31

Each agent gets a private directory and a private branch. They run tests without interference, commit independently, and every commit lands in the shared object database — so a supervising agent (or you) can git diff main...issue-17 each branch from the main worktree, review, and merge in risk order. This is exactly the shape of a parallel issue sweep: dispatch one agent per ready-for-agent issue, review diffs at the end instead of driving each fix.

Claude Code has this built in two ways — the worktree mechanism is identical in both (one flag, one git worktree add per agent, auto-removed if the agent changed nothing); what differs is who coordinates. A worked example makes the contrast concrete.

The scenario: three small, independent fixes in sensor-logger — issue #17 (off-by-one in a rounding helper), #23 (missing aria-label on a UI control), #31 (stale copy on a settings page). All three mutate files, so they can’t share one working tree.

Way 1 — the Agent tool (manual dispatch)

The main conversation launches three subagents as three tool calls in one message, each with a hand-written prompt:

Agent {
  subagent_type: "general-purpose",
  isolation: "worktree",
  description: "Fix issue #17",
  prompt: "Fix the rounding off-by-one described in issue #17.
           Write a failing test first, then fix. Commit on a branch named issue-17.
           Report: branch name, files touched, test results."
}
Agent { isolation: "worktree", description: "Fix issue #23", prompt: "..." }
Agent { isolation: "worktree", description: "Fix issue #31", prompt: "..." }

Each agent wakes up in its own fresh worktree. The three reports come back separately, and the main conversation is the integrator: it reads the reports, diffs the branches, decides merge order. The bash equivalent: you opened three terminals, ran git worktree add in each, and started three workers by hand — coordination lives in your head.

Way 2 — the Workflow tool (scripted dispatch)

Same job, but the fan-out is a script instead of hand-written tool calls:

export const meta = {
  name: 'issue-sweep',
  description: 'Fix ready-for-agent issues in parallel, verify each',
  phases: [{ title: 'Fix' }, { title: 'Verify' }],
}

const issues = args   // e.g. [17, 23, 31] passed in at invocation

const results = await pipeline(
  issues,
  n => agent(
    `Fix issue #${n}. Failing test first, then implement. Commit on branch issue-${n}.
     Return: branch, files touched, test counts.`,
    { isolation: 'worktree', phase: 'Fix', label: `fix:#${n}` }
  ),
  (fix, n) => agent(
    `Check out branch issue-${n} and adversarially verify the fix for issue #${n}:
     run the full test suite, try to find a case the fix misses. Return a verdict.`,
    { phase: 'Verify', label: `verify:#${n}` }
  )
)
return results

Same isolation: 'worktree' per agent — but now a program owns the structure: every fix automatically gets a verify step chained behind it, #17’s verifier starts the moment #17’s fixer finishes even if #23 is still running, and handing it 12 issues instead of 3 changes nothing about the script. The bash analogy for the difference:

for n in 17 23 31; do
  ( fix_issue "$n" && verify_issue "$n" ) &
done
wait

— except the “commands” are agents, and the script runs deterministically in the background and returns one structured result.

Which to use

  • Agent tool = manual dispatch. Right for 2–3 one-off parallel tasks where the coordination fits in the conversation.
  • Workflow tool = scripted dispatch. Right when the structure repeats per item (fix → verify per issue), the item count is variable, or the pattern is worth keeping as a named, reusable workflow.

Two agent-specific gotchas:

  • Worktree isolation costs setup time and disk per agent — use it when agents mutate files in parallel, not for read-only exploration (readers can all share one tree safely).
  • Merging is still your problem. Worktrees prevent file-level trampling during work; they don’t prevent two branches from making logically conflicting changes. The review/merge step at the end is where that gets caught.

Cheat sheet

TaskCommand
New branch in a new worktreegit worktree add ../dir -b branch
See all worktreesgit worktree list
Remove a worktree (keep branch)git worktree remove ../dir
Clean up after manual deletegit worktree prune
Compare an agent branchgit diff main...branch

One thing this pattern leans on heavily: every worktree carries its own untracked build artifacts, so the rules from Ignoring-Files-in-Git are what keep each agent’s git status readable instead of full of another directory’s noise. Ignore hygiene stops being cosmetic the moment several agents work at once.

That closes the series. You have the database model (Git-from-First-Principles), remotes as caches of someone else’s pointers (Remotes-and-Tracking-Branches), control over what git watches at all (Ignoring-Files-in-Git), the collaboration loop (Pull-Commit-Push-on-a-Shared-Repo, Using-git-stash-Safely), combining independent work (Merging-Rebasing-and-Reading-History), the safety net that makes all of it recoverable (Undoing-Things-in-Git), and now several checkouts of one repository at once.