ONE OBJECT STORE · FOUR WORKING TREES · 27 COMMITS · ONE MORNING

Git worktrees:
how three AI agents share one repo

react/
main
.git/ — a directory
agent-a/
worktree-agent-a
.git → worktrees/agent-a
agent-b/
worktree-agent-b
.git → worktrees/agent-b
agent-c/
worktree-agent-c
.git → worktrees/agent-c
.git/objects — every version of every file, stored once
object stores: 1working trees reading it: 4history duplicated: 0 bytes

Tuesday, 09:00. Three unrelated tasks, three parallel agent sessions, and one clone they are about to fight over. This is the whole morning end to end: the collision, the 206 bytes of pointers git writes to make a second checkout, 27 commits into one shared store, the agent whose work gets thrown away, and two pull requests merged by 11:47. are tappable. You are the one who has to review all of it.

01

The collision

At 09:00 there is one clone of react — 457,488 objects, a gigabyte of history, 21,607 commits, one working tree — and three agent sessions about to write into it. The first six minutes of this morning go on finding out which parts of a repository are shared and which are not. The answer turns out to be short enough to fix.

STEP 01 / 05
THREE TASKS, ONE DIRECTORY
09:00 — three unrelated tasks, three sessions, one checkout
agent A
lint rule
agent B
flaky test
agent C
dev warning
react/ — one working tree
what they actually collide on:
package.json yarn.lock build/ port 3000

The three tasks are unrelated and live in different packages: a new lint rule in eslint-plugin-react-hooks, a flaky test in react-dom, a reworded development warning in react. Three sessions start in the same directory. Within two minutes each is editing files the other two did not expect to move. The real collisions are not in those three source files, though — they are in package.json, the lockfile, the build/ directory, and port 3000. Shared surfaces that were never assigned to anybody.

three agents editing three different packages still collide — on the lockfile, the build directory, and port 3000
STEP 02 / 05
A BRANCH IS 41 BYTES
a branch is a file, and it is very small
$wc -c .git/refs/heads/main41bytes — 40 hex + newline
$wc -c .git/HEAD21bytes of text
$cat .git/HEADref: refs/heads/mainnames the current branch
git switch — rewrites 41 bytesthenrewrites 7,276 tracked files
including the ones the other two sessions are editing

A branch is a file. .git/refs/heads/main holds 40 hexadecimal characters and a newline — 41 bytes, in this repository and in yours. is 21 more bytes of text, reading ref: refs/heads/main, and it names which of those files is current. Recording a branch switch is therefore one very small write. Applying one is not: git then rewrites the to match, all 7,276 tracked files of it, including the ones the other two sessions are part-way through editing.

git switch rewrites one 41-byte file — and then every file in the working tree, under whoever is typing
STEP 03 / 05
THE FILE THAT REMEMBERS EVERY FILE
two sessions, one .git/index — the staging area is a single file
A: git add
B: git add
.git/index
index.lock — held by A
B: fatal: Unable to create ‘.git/index.lock’: File exists.
no interleaving, no queue — the second one just stops

The is one binary file. It opens with the four bytes DIRC, then a version and an entry count, then one entry per tracked path carrying that path, its content hash, and full filesystem stat data. In this clone it is 1.18 MB describing 7,276 files, and it is what makes git status fast. There is exactly one of it, and two git add processes do not interleave. The second one stops with a lock error, having staged nothing.

git’s staging area is one binary file — two agents running git add at the same time is a lock-file race, and the loser just fails
STEP 04 / 05
WHAT A CHECKOUT ACTUALLY IS
what a second checkout actually needs its own copy of
PER CHECKOUT — 3 things
HEADwhich branch you are on
indexwhat you have staged
the fileswhat you are editing
SHARED — all of the rest
objects/every version ever committed
refs/every branch and tag
configremotes and settings
hooks/the scripts that run
three agents need three of the left column, and none of the right

Here is the observation the rest of this tutorial spends. Only three things belong to where you are right now: HEAD, the index, and the files on disk. Everything else under .git — the , the , the config, the hooks, the remotes — is history and setup, and none of it has any opinion about how many checkouts exist. Three agents do not need three histories. They need three copies of a very short list.

only three things in git belong to your current checkout — HEAD, the index, and the files; all the rest is shared history
STEP 05 / 05
THE OBVIOUS FIX, AND WHAT IT COSTS
the obvious fix: give each agent its own clone
clone A
clone B
clone C
downloaded1.0 GiB each
clone time1 min 58 s each
stored on disk1.0 GiB each
branches the others can seenone
three identical copies of a history that never differs

So give each agent its own clone. Priced honestly, on the repository this morning actually runs on: cloning react over the network took 1 minute 58 seconds and produced a 1.0 GiB .git directory. Three agents means three of those — three downloads, three gigabytes, and three sets of remote-tracking refs that cannot see each other’s branches. The history being copied is identical every time. That is the part worth refusing to pay for.

cloning react takes 1 min 58 s and 1.0 GiB — parallelism by cloning means paying that once per agent
deep dive: the index, up close

Why a lock file and not a database. Git coordinates writes by creating index.lock and renaming it into place, which is atomic on every filesystem git supports. The failure mode you meet in practice is a stale one: kill a git process mid-write and the lock outlives it, so the next command refuses until you delete the file yourself.

The index is a cache, not just a list. Each entry stores the file’s size, inode, and modification time, so git can decide a file is unchanged without reading it. Touch a file without editing it and git has to hash it again to find out nothing happened — which is why git status after a big rebuild is slower than you expect.

Making status fast on large repositories. Index format v4 compresses repeated path prefixes; the untracked cache remembers which directories had nothing new; and core.fsmonitor lets a background watcher tell git which paths changed, so it can skip most of the tree entirely.

Sparse checkouts live here too. The rules that decide which files land in your working tree are stored per checkout, alongside the index — which is why two checkouts of one repository can legitimately hold different subsets of the same commit.

02

What git actually stores

Open .git/objects and nothing is recognisable: no filenames, no diffs, no branches — just directories named 00 through ff. Everything chapter 01 called shared history is in there, held by four kinds of object and one addressing rule. Nothing is written in this chapter; it opens the store that the rest of the morning writes into.

STEP 01 / 04
HAND IT BYTES, GET BACK AN ADDRESS
hand git bytes, get back the address those bytes will always live at
you typewhat is up, doc?
git prepends a headerblob 16\0what is up, doc?
SHA-1 of all of thatbd9dbf5aae1a3862dd1526723246b20206e5fc37
written to.git/objects/bd/9dbf5aae1a38…
first two hex digits name the directory, the other 38 name the file

Git is a store: give it bytes, and it returns the address those bytes will always live at. echo -n "what is up, doc?" | git hash-object --stdin answers bd9dbf5a…, on your machine and on mine. The hash covers a header as well as the content — the string actually fed to SHA-1 is blob 16\0what is up, doc? — which is why running sha1sum on the same file gives you a different number and a confusing afternoon.

git hashes a header plus your content, never the content alone — sha1sum on the same file disagrees, and git is not the one that is wrong
STEP 02 / 04
FOUR KINDS OF OBJECT
a commit object, in full — 184 bytes of plain text
tree7c19d0da3a5d5b…
parent3eaf97fd66c8fa…
authoragent <a@b> 1785332402
committeragent <a@b> 1785332402
lint commit 9
the tree
100644blob.editorconfig
040000treepackages
…one row per entry
the snapshot everyone talks about is the single line reading “tree”

There are four: the holds the bytes of one file version and carries no name; the is a directory listing that supplies the names; the names one tree, its parents, its author, and its message; the tag names a commit and says something about it. A real commit from this morning is 184 bytes of plain text you can read with cat. The snapshot everyone talks about is the single line beginning tree.

a commit object is 184 bytes of plain text — the snapshot is one line naming a tree
STEP 03 / 04
THE SAME FILE, STORED ONCE
three checkouts, one identical package.json, one blob
react/package.json
agent-a/package.json
agent-b/package.json
the hash is the storage address, so a duplicate has nowhere else to go

Because the address is computed from the content, identical content has exactly one address, and a duplicate has nowhere else to go. Two branches holding the same package.json share one blob. So do two working trees, and three, and four. This is the arithmetic the next chapter spends: four checkouts of react do not need four copies of react’s history, because there is no mechanism by which a second copy could exist.

two checkouts holding an identical file store it once — content addressing gives a duplicate nowhere to live
STEP 04 / 04
LOOSE, THEN PACKED
the same objects, stored two ways — average size per object
loose, before gc254 objects · 1,016 KiB
4.0 KB each
packed, after gc457,742 objects · 1.00 GiB
2.3 KB each
repacking absorbed all 254 and left the pack 8 MB smaller than before

New objects land : one zlib-compressed file each. Then rewrites them into a where similar objects are stored as deltas against one another. Measured on this clone: the morning left 254 loose objects taking 1,016 KiB, about 4.0 KB apiece, while the packed store averages 2.3 KB across 457,742 objects. Repacking absorbed all 254 and the pack came out 8 MB smaller than it had been.

gc absorbed 1,016 KiB of loose objects and the packfile came out 8 MB smaller than before it started
deep dive: inside the store

How a packfile stays small. Objects that resemble each other are stored as deltas along a chain, with only the chain’s base held in full. An .idx file maps each object name to its offset so lookup stays fast, which is why reading a ten-year-old commit costs about what reading yesterday’s does.

Refs get packed too. This clone has 1,102 refs and almost none of them are files: they live in packed-refs, one line each. The newer reftable backend replaces that arrangement entirely, for repositories where a directory of hundreds of thousands of tiny files had become the bottleneck.

The hash is changing. SHAttered demonstrated a real SHA-1 collision in 2017. Git responded first by hardening its own SHA-1 to detect collision attempts, then by supporting SHA-256 repositories from 2.29 in 2020. SHA-256 becomes the default in Git 3.0; hosting support is the reason almost nobody has moved yet.

Why object names can never change. A commit names its tree by hash and its parents by hash, so altering any byte anywhere changes every name above it. That is what makes history tamper-evident, and also why --amend cannot edit a commit and instead writes a new one.

03

One store, many trees

git worktree add ../agent-a -b agent-a finishes in 1.76 seconds and leaves behind a complete second checkout. Chapter 01 named the three pieces of state a checkout owns; this chapter watches git write exactly those three and nothing else. The store stays at 457,488 objects. The count of working trees reading it goes from one to four.

STEP 01 / 05
GIT STOPS BEING A DIRECTORY
the same name, two different kinds of thing
react/.git
a directory
objects/refs/confighooks/indexHEADworktrees/
agent-a/.git
a file, one line long
gitdir: /…/react/.git/worktrees/agent-a
that pointer is the entire trick
every tool that shells out to git keeps working, without knowing worktrees exist

In a , .git is a file. One line long: gitdir: /…/react/.git/worktrees/agent-a. That pointer is the entire trick, and it is also why the rest of your toolchain does not need to learn anything. Git’s repository-discovery rule already handles a .git that turns out to be a pointer, so every script and editor that shells out to git keeps working inside a worktree without knowing worktrees exist.

in a linked worktree .git is a one-line text file — which is why tools that never heard of worktrees still work inside one
STEP 02 / 05
THE FOUR POINTERS AND THE ONE BIG FILE
.git/worktrees/agent-a/ — everything the second checkout owns
HEAD24 Bref: refs/heads/agent-a
ORIG_HEAD41 Bwhere it was before
gitdir135 Babsolute path to the .git file
commondir6 B../..
four pointers206 Btotal
index1.18 MBits own staging area
the same three things chapter 01 named, plus two paths to find the rest
history contributed to this directory: 0 bytes

Inside .git/worktrees/agent-a/ are four pointer files totalling 206 bytes: HEAD at 24, ORIG_HEAD at 41, gitdir at 135 — an absolute path back to the .git file — and at 6, holding the string ../... Alongside them sits this checkout’s own index, which on react is 1.18 MB. Compare that against chapter 01’s list: HEAD, the index, the files. The same three things, plus two paths for finding everything else.

the pointers that make a second checkout come to 206 bytes — the index beside them is 1.18 MB, and neither is history
STEP 03 / 05
HOW GIT FINDS THE REPOSITORY
how git finds the repository from inside a worktree
.gita file, not a directory
gitdir:sets $GIT_DIR
commondirsets $GIT_COMMON_DIR
$GIT_DIR — yoursHEADindexORIG_HEAD
$GIT_COMMON_DIR — everyone’sobjects/refs/heads/confighooks/
two roots, and the split is the whole feature

The walk, in order. Find .git; it is a file, so read the gitdir: line and set $GIT_DIR to it; look for commondir in that directory and set $GIT_COMMON_DIR from it. Every internal path now resolves against one of those two roots — HEAD and the index against yours, objects and refs and config and hooks against everybody’s. You can watch it happen: git rev-parse --git-dir and --git-common-dir print the two answers.

git resolves every internal path against one of two roots — and a worktree is nothing more than a second value for the first one
STEP 04 / 05
WHICH REFS ARE YOURS, AND WHICH ARE EVERYONE’S
which refs each worktree owns, and which one copy everybody shares
ONE PER WORKTREEHEADORIG_HEADrefs/bisect/*refs/worktree/*refs/rewritten/*
ONE, SHARED BY ALLrefs/heads/*refs/tags/*refs/remotes/*
fatal: ‘main’ is already used by worktree at ‘/…/react’
a branch is one shared file, so exactly one worktree may hold it

One per worktree: HEAD, ORIG_HEAD, refs/bisect/*, refs/worktree/*, refs/rewritten/*. Shared: branches, tags, and remote-tracking refs. A constraint falls straight out of it — git worktree add ../dup main answers fatal: ‘main’ is already used by worktree at …, because a branch is one shared file and two checkouts moving it would be writing over each other. Use when two trees genuinely want the same commit.

three agents can each run their own bisect at once, because refs/bisect is per worktree — but no two may hold the same branch
STEP 05 / 05
THE BILL
the same second checkout, bought two ways
git cloneworktree add
wall clock1 min 58 s1.76 s
downloaded1.0 GiBnothing
history written1.0 GiB0 bytes
working tree written60 MB60 MB
branches it can seeits ownall 1,102
a checkout, not a download — the store it attaches to is already on your disk

Measured on the repository this morning runs on. A network clone of react: 1 minute 58 seconds, 1.0 GiB downloaded, 1.0 GiB stored. git worktree add: 1.76 seconds, nothing downloaded, 60 MB of working tree, 206 bytes of pointers, and zero bytes of history. Both give you a second checkout. Only one of them pays for the history twice. The commands that manage them — list, remove, prune, lock, move, repair — exist because at that price you make them constantly.

worktree add: 1.76 s against a clone’s 1 min 58 s, and it downloads nothing at all
deep dive: worktrees before and beyond

There was an earlier attempt. contrib/workdir/git-new-workdir did roughly this with symlinks into .git. It never became a real command: symlinks are not portable to Windows, and a stray rm -rf in the wrong directory could follow one back into the repository. The built-in landed in Git 2.5, in July 2015.

Some settings must never be shared. core.worktree, core.bare, and core.sparseCheckout describe one checkout, not the repository. Turning on extensions.worktreeConfig gives each worktree its own config.worktree so they can differ — at the cost of making the repository unreadable to older git.

The gitdir path is absolute. Move a worktree directory with mv and that 135-byte file now points at nothing. git worktree repair rewrites both ends of the link; git worktree move avoids the problem by updating them as it goes.

Submodules are the rough edge. A submodule’s own checkout state is not part of the per-worktree split, so two worktrees of a superproject can disagree about which submodule commit is checked out in ways that surprise you. Teams that live in worktrees usually keep submodules out of the arrangement.

04

One agent per tree

09:06. The three sessions restart, each in its own directory, each on its own branch, all reading the same 457,488 objects. What follows is the part the git manual does not cover: which of an agent’s problems this arrangement solves, and which four it leaves exactly where they were.

STEP 01 / 05
THREE SESSIONS, THREE DIRECTORIES
09:06 — the same command, three terminals
$claude --worktree agent-a.claude/worktrees/agent-a
$claude --worktree agent-b.claude/worktrees/agent-b
$claude --worktree agent-c.claude/worktrees/agent-c
where each branch starts:
baseRef “fresh” → origin’s default branch“head” → your HEAD
the default starts an agent from what is shipped, not from your unfinished work

claude --worktree agent-a creates .claude/worktrees/agent-a/ on a new branch and starts the session inside it. The is worth knowing about: by default the branch starts from the remote’s default branch, not from whatever you have half-finished locally. Set worktree.baseRef to “head” when you want the opposite. Run the command twice more in two more terminals and the morning has its three sessions.

an agent’s worktree branches from origin’s default branch by default — it starts from what shipped, not from your unfinished work
STEP 02 / 05
A FRESH CHECKOUT HAS NO .ENV
a worktree is a fresh checkout — tracked files, and nothing else
tracked files60 MB, checked out
.envgitignored — absent
node_modules/gitignored — absent
build cachesgitignored — absent
.worktreeincludecopies gitignored files back in
this site’s repo: 4.5 MB of git history, 527 MB of node_modules

A worktree contains tracked files and nothing else, so everything gitignored is missing: .env, node_modules, build caches. A .worktreeinclude file, written in gitignore syntax, copies chosen gitignored files into each new worktree — and only files that are both matched and gitignored, so tracked files are never duplicated. Dependencies you install per tree, which is where the disk actually goes. This site’s own repository carries 4.5 MB of git history and 527 MB of node_modules.

this site’s repo: 4.5 MB of history against 527 MB of node_modules — git was never the expensive part of a parallel setup
STEP 03 / 05
THE THINGS THAT STILL COLLIDE
what a worktree separates, and what it leaves in one piece
ISOLATEDthe filesthe indexHEADits branch
STILL SHAREDport 3000the local databaseMCP server staterefs/heads/*permission approvals
isolation is a filesystem property, not an agreement between agents

A worktree separates the filesystem. It does not separate port 3000, the local database, an MCP server’s state, or the branch namespace, and three agents will find all four. One shared thing is deliberate: choosing “don’t ask again” for a command inside a worktree saves the rule to the main checkout, so it applies in every worktree and outlives the one you granted it in. Isolation is a property of directories. It is not an agreement about architecture.

worktrees isolate files and only files — two agents will still fight over port 3000 and one local database
STEP 04 / 05
AGENTS INSIDE AGENTS
a running subagent’s worktree, and what the cleanup sweep does about it
.git/worktrees/sub-1/HEADindexgitdircommondirlocked
agent still runninglocked — skipped
uncommitted changesskipped
unpushed commitsskipped
clean and past its ageremoved
one file named “locked” is the whole mechanism

A subagent can have a worktree of its own, permanently, by putting isolation: worktree in its frontmatter. While it runs, its worktree is locked — a file called locked appears in the administrative directory from chapter 03, and the periodic cleanup sweep skips anything holding it. The sweep also skips any worktree with uncommitted changes, untracked files, or unpushed commits, which means the failure mode is an accumulation of stale directories rather than lost work.

while a subagent runs, a file named “locked” in .git/worktrees/<id>/ is the entire thing stopping cleanup from deleting its work
STEP 05 / 05
THE ONE-BRANCH RULE, IN PRACTICE
one branch per checkout — so ownership is never a question
react/main
agent-a/worktree-agent-a
agent-b/worktree-agent-b
agent-c/worktree-agent-c
claude --worktree "#1234"
fetches pull/1234/head → .claude/worktrees/pr-1234
somebody else’s pull request, in its own directory, touching nothing of yours

Chapter 03’s constraint turns out to be the useful kind. Two agents cannot be pointed at one branch, so “who owns this branch” never becomes a question you have to answer at 11:00 with three sessions running. The same machinery handles review: claude --worktree "#1234" fetches pull/1234/head and checks it out in its own directory — somebody else’s pull request, running locally, touching nothing you have open.

--worktree "#1234" fetches pull/1234/head into its own directory — you can run a colleague’s PR without disturbing your tree
deep dive: wiring the setup

Replacing the git logic entirely. A WorktreeCreate hook takes over creation, including putting worktrees somewhere other than .claude/worktrees/, and is how the same parallel workflow runs on Perforce or SVN. Because the hook replaces the default path, .worktreeinclude stops being processed — copy what you need inside the hook instead.

Put the directory in .gitignore. Worktrees created under .claude/worktrees/ sit inside the repository, so without an ignore rule every parallel session shows up as untracked files in the main checkout — and in the diff you are about to review.

Writes to the shared .git are allowed. Sandboxing normally confines a session to its own directory, which would break git commit from inside a worktree, since the objects belong to the main repository. The sandbox carves out that path deliberately.

What is shared on purpose. Project-scope plugins load in every worktree of a repository rather than needing reinstallation, and permission approvals are saved to the main checkout. Both follow the same principle as the object store: configuration belongs to the repository, not to the checkout.

05

Commits are the agent’s save points

Between 09:20 and 11:05 the three sessions write 27 commits into the one store, adding 243 objects to the 457,488 that were already there. Each commit costs a handful of small objects and buys back an exact tree — which changes what the careful move is when a model is about to rewrite forty files.

STEP 01 / 05
THE UNDO YOU GET FOR FREE
one commit touching six files — everything it writes
6blobone per changed file
4treeone per directory on the path
1commitnames the tree and the parent
objects added to the store: 11
a few kilobytes, and an exact tree you can return to

A commit is a : a tree you can return to exactly, addressed by its hash. Here is what one costs, measured. A commit touching six files in react wrote 11 objects — six blobs, four trees for the directories along the path, and the commit itself. A few kilobytes. For a session about to attempt a wide refactor, committing first is the cheapest insurance available, and the only kind that still works after the process is gone.

a six-file commit wrote 11 objects and a few kilobytes — committing before every risky step costs almost nothing
STEP 02 / 05
THE DIFF IS THE REVIEW UNIT
one real commit, two ways to find out what it did
the whole file, ReactFiberHooks.js
5,253 lines
the commit’s diff
200 lines
git show --stat → 2 files changed, 148 insertions(+), 10 deletions(−)
the same saving for your attention and for the model’s context

You do not review what an agent wrote. You review what changed, which is a different and much smaller thing. A real commit to react’s ReactFiberHooks.js shows as 148 insertions and 10 deletions across two files — 200 lines of diff against a 5,253-line file. The same arithmetic runs in the other direction: a diff is far cheaper context for the model than re-reading the file, and it is the form in which the change can actually be judged.

200 lines of diff instead of a 5,253-line file — the same 26× saving for your attention and for the model’s context
STEP 03 / 05
SMALL COMMITS, HONEST MESSAGES
the same work, committed two ways — and what each shape allows
one commit, 40 filesbisect: nothing to searchrevert: all of it or none
nine commits, 4 files eachbisect: 4 tests to the culpritrevert: any one of them
bisect, revert, and review all work one commit at a time

Bisect, revert, and review all operate one commit at a time, so a forty-file “implement the feature” commit cannot be bisected, cannot be partly reverted, and will not be read. git add -p splits a working tree into hunks you accept one at a time, which is the tool for turning one agent’s sprawling change into a reviewable sequence. The message matters for the same reason: it is what tomorrow’s session reads when it needs to know why this code looks like this.

nine small commits cost four tests to bisect; one big commit costs a search that cannot start
STEP 04 / 05
THE REPOSITORY IS ALSO THE CONTEXT
asking history a question instead of reading the files
git log -S"useLayoutEffect"110 commits
who added or removed this string?
git log -- <path>its history only
what has happened to this file?
git blame <file>one commit per line
which commit wrote this line?
git log --oneline -2020 subject lines
what has been going on lately?
a session that remembers nothing about yesterday can still read why

History answers “why is this here” more cheaply than any file dump. git log -S — the — finds the commits that added or removed an exact string: in react’s reconciler, 110 of them touched useLayoutEffect. git log -- <path> narrows to one file, git blame to one line. These are questions with short answers, where reading the directory is an answer with no question.

git log -S found the 110 commits that touched useLayoutEffect in react’s reconciler — a question about history, not a scan of the files
STEP 05 / 05
WHAT NOT TO COMMIT
one package.json, three independent resolutions
agent-ayarn.lock @ 3f2a1cmerges cleanly
agent-byarn.lock @ 9d40beconflict
agent-cyarn.lock @ c17e05conflict
.gitattributes: yarn.lock merge=ours
every one of the three diffs looked correct on its own

The usual list — build output, generated files, secrets — plus one that only shows up with parallel writers. Three branches each resolve the same package.json independently and produce three different lockfiles. Each diff is correct on its own, each passes review, and the second merge conflicts. A .gitattributes merge driver settles it by declaring the lockfile regenerable rather than mergeable, which is what it always was.

three agents produce three different lockfiles from one package.json — every individual diff looks correct, and the second merge does not
deep dive: commit hygiene, mechanically

What conventional commits actually buy. A machine-readable subject line helps changelog generation and release tooling far more than it helps a reader. The part a reader needs is the body explaining why, which no convention can supply for you.

How git add -p decides. It splits the diff into hunks and offers each one; s splits a hunk further where the change has a gap in it, and e drops you into the hunk to edit which lines are staged. The index, from chapter 01, is where the accepted ones accumulate.

--amend does not edit anything. Object names are derived from content, so changing a commit means writing a new one and moving the branch to it. The original is still in the store, unreferenced, and the reflog in chapter 06 is what remembers where it went.

Hooks are per repository, not per worktree. .git/hooks lives in the common directory, so a pre-commit hook installed once runs identically in all four checkouts — which is either exactly what you want or a surprise, depending on whether the hook assumes it is running in the main one.

06

When an agent is wrong

10:35. Agent C’s approach is wrong — not broken, wrong, which is worse, because it passes its tests. Nine commits have to go. This chapter prices that, then goes looking for the one operation in git that actually destroys work, since almost nothing else does.

STEP 01 / 05
THE DISCARD COSTS ONE FILE
10:35 — throwing away nine commits of an agent’s work
git worktree remove --forceone directory, 60 MB
git branch -D agent-cone 41-byte file
its 90 objects are still there, now unreachable — nothing was rewritten

git worktree remove --force ../agent-c deletes a directory. git branch -D agent-c deletes a 41-byte file and prints the hash it had been pointing at. The nine commits and their 90 objects are still in the store, now unreachable: nothing was rewritten, nothing was compacted, and the object count did not move. Discarding an agent’s entire branch is two deletions and no bookkeeping.

discarding nine commits deletes a directory and a 41-byte file — the object count in the store does not change at all
STEP 02 / 05
THE THREE WAYS BACK
three ways back to work you just deleted
the hash git printedgit branch -D said “was 744800f854”
git fsck --lost-founddangling commit 744800f854…
git reflogevery position HEAD has held
reflog entries, reachable: 90 daysreflog entries, unreachable: 30 daysunreachable objects: 2 weeks
deleting the branch removed its reflog — the commits themselves outlive it

Worth being precise here, because the usual advice is slightly wrong. git branch -D removes the branch’s own along with the ref, so git reflog agent-c now fails. What survives is the hash the delete printed, and the commits themselves, which git fsck --lost-found reports as dangling. Three windows govern how long you have: reflog entries last 90 days, unreachable reflog entries 30, and unreachable objects two weeks.

deleting a branch deletes its reflog too — the recovery route is the hash git printed, or git fsck finding the dangling commit
STEP 03 / 05
WHICH COMMIT BROKE IT
git bisect over 1,000 commits — the range git still has to search
commits left to test: 1,000test 1
Bisecting: 499 revisions left to test after this (roughly 9 steps)
each test halves what is left, so the cost grows as log₂ of the range

The test passing at 09:20 fails at 11:05. halves the range on every test, and it will tell you the cost up front: over a thousand commits it announces “roughly 9 steps”, over the nine commits of one agent’s branch, roughly 2. git bisect run npm test does the whole search unattended. Because bisect state is per worktree, one checkout can run it while the other two carry on working.

git predicts its own cost: “roughly 9 steps” to bisect a thousand commits, because the search is log₂ of the range
STEP 04 / 05
TWO AGENTS, ONE FILE
a merge has three inputs, and the third one is what makes it decidable
basethe common ancestor
oursagent A’s version
theirsagent B’s version
.git/rr-cache/<hash>/ — your resolution, filed
replayed when the identical conflict reappears
rerere repeats a decision you already made — it does not make one

Agent A and agent B both edited the same file, so the second merge conflicts. A uses three inputs, not two: your side, their side, and the common ancestor. The ancestor is what turns “these differ” into “this side changed that line and the other did not”. records how you settled a conflict and replays that resolution when the identical conflict reappears, which is worth turning on for a branch that gets rebased repeatedly.

the common ancestor is what makes a merge decidable — without it git could only report that two versions differ
STEP 05 / 05
THE ONE STATE GIT CANNOT RECOVER
what git can get back, by whether an object was ever written
state of the workobject?recovered by
committedwrittenreflog, fsck, the hash
staged with git addwrittenthe object exists — fsck finds it
modified, never addednevernothing
untracked, then git cleannevernothing
the only destructive state in git is the one before git add

Everything above works because an object was written. git restore over uncommitted changes and git clean -fd over untracked files destroy work permanently, because nothing was ever addressed and there is nothing to look up. The reflog cannot help; fsck cannot help. This is the actual argument for making an agent commit early and often — not tidiness, but the fact that everything before git add is the only part of this system with no undo.

fsck and the reflog can only find objects that were written — everything before git add is the one truly destructive state in git
deep dive: undo, precisely

reset has three targets. --soft moves the branch and stops; --mixed also rewrites the index; --hard also rewrites the working tree, which is the only one of the three that can lose anything.

Why a branch beats a stash for agent work. A stash is a commit with two parents living on a stack nobody looks at, identified by position rather than by name. When three sessions are running, a named branch costs the same and can be told apart from the other two.

revert against reset. Reset moves a branch backwards, which is a rewrite; anyone who pulled that branch now disagrees with you about history. Revert writes a new commit undoing an old one, which is why it is the correct tool on anything already pushed.

The operation that loses other people’s work. A force-push replaces a remote branch with yours, discarding commits pushed in between. --force-with-lease refuses unless the remote is still where you last saw it, which turns a silent overwrite into an error message.

07

Landing it

11:05. Two branches are good, main has moved twice since 09:00, and four directories are still on disk. The last 42 minutes are merge order and cleanup — both of which behave differently when every checkout is reading and writing the same store.

STEP 01 / 04
FETCH ONCE, EVERYONE SEES IT
git rev-parse --git-path, run inside agent-a’s worktree
refs/remotes/…/react/.git/refs/remotesshared
refs/heads/…/react/.git/refs/headsshared
HEAD…/.git/worktrees/agent-a/HEADyours
one git fetch → all four checkouts are up to date
there is one remote configured, not four

Remote-tracking refs live in the shared half of chapter 03’s split, and you can check it: git rev-parse --git-path refs/remotes run inside a worktree answers with a path in the main .git, while the same question about HEAD answers with the worktree’s own directory. So one git fetch, run anywhere, brings all four checkouts up to date at once. There is one remote configured, not four, and rebasing two branches onto a moved main needs no fetching of its own.

one git fetch in any worktree updates the remote-tracking refs all four read — there is one remote configured, not four
STEP 02 / 04
THE PR IS THE UNIT
11:05 — two branches, reviewed as diffs, merged in order
agent-a → main9 commits · 36 filesmerged 11:31
agent-b → main9 commits · 27 filesmerged 11:47
CORTEX 2026 BENCHMARK, YEAR OVER YEAR
pull requests per developer+20%
incidents per pull request+23.5%
more changes landing, and more of them going wrong per change

Push each branch, open a pull request each, read the diffs, merge in dependency order. The case for keeping them small is stronger when a model wrote them, and the industry numbers point the same way: Cortex’s 2026 benchmark reports pull requests per developer up 20% year over year while incidents per pull request rose 23.5%. More changes landing, and a higher share of them going wrong. The review of the diff is the step where that gets settled.

pull requests per developer up 20% year over year, incidents per pull request up 23.5% — output rose faster than the checking did
STEP 03 / 04
TEARING DOWN
two ways to remove a worktree, and what git knows afterwards
commandon diskinside .git
git worktree removedirectory goneadmin directory gone
rm -rfdirectory goneadmin directory stays
git worktree list → agent-b [prunable]
git worktree prune -v → Removing worktrees/agent-b: gitdir file points to non-existent location
automatic gc waits 3 months before pruning it for you

git worktree remove takes the directory and its administrative entry together. Delete the directory with rm -rf instead and git does not notice: git worktree list marks the entry prunable and it sits there until git worktree prune runs, which automatic maintenance only does after three months. Locked worktrees are never pruned, and git worktree repair fixes the absolute paths after you move a directory.

rm -rf on a worktree leaves its admin entry marked prunable for three months — git only finds out when prune runs
STEP 04 / 04
WHAT GC KEEPS
11:47 — what gc does with the morning’s leftovers
leftoverwhat happens to itkept for
254 loose objectspacked into the packfilenow
agent C’s 90 objectsunreachable, still stored2 weeks
reflog entriesexpired past their window90 / 30 days
stale worktree entriespruned3 months
the discarded branch stays recoverable for another fortnight

11:47. packs the morning’s 254 loose objects, expires reflog entries past their windows, and prunes worktree entries whose directories are gone. Agent C’s 90 objects are unreachable but not yet gone: unreachable objects are kept two weeks, so the decision to throw that branch away stays reversible for a fortnight after the morning it was made. The store ends the day one compressed morning larger than it started.

the discarded branch’s objects survive two more weeks — throwing an agent’s work away is a decision you can take back until a fortnight later
deep dive: keeping a shared store healthy

Maintenance without waiting for gc. git maintenance start registers background jobs that repack incrementally and refresh the commit graph, which keeps a shared store fast without the occasional long pause that gc --auto produces at the worst moment.

When the store is too big to want all of. A partial clone with --filter=blob:none fetches file contents on demand instead of up front, and sparse-checkout limits which paths reach the working tree. Sparse rules are per worktree once extensions.worktreeConfig is on, so two agents can hold different slices of one repository.

origin/HEAD goes stale. The default branch a fresh worktree starts from is read from origin/HEAD, which is set once at clone time and never updated. If the project renamed its default branch, git remote set-head origin -a is what corrects it.

The guardrail that is not git. Branch protection, required reviews, and required checks live on the host, not in the repository, and they are what actually stops an unreviewed branch from reaching main. Everything in this chapter is a convention until something enforces it.

Σ

The morning’s ledger

Every figure below was measured on a clone of react rather than estimated, and the right margin says which chapter earned it. Three agents worked in parallel for three hours in four checkouts of one repository, and the history — the expensive part, the part that took two minutes to download — was never copied at all.

ONE TUESDAY · 09:00 → 11:47 · REACT AT 9a81195, MEASURED ON GIT 2.55
THE MORNING
09:00 · tasks handed out·······································3, in 3 packages01
09:04 · what they collided on·······································the lockfile, build/, port 300001
09:06 · git worktree add, each·······································1.76 s03
09:20 · dependencies, per tree·······································installed separately04
11:05 · commits written·······································2705
10:35 · agent C discarded·······································9 commits, 90 objects06
11:47 · pull requests merged·······································207
THE STORE
objects after the clone·······································457,48801
objects added by the morning·······································24305
objects in the store at 11:47·······································457,73107
object stores·······································103
working trees, at peak·······································403
history duplicated·······································0 bytes03
THE THREE EXTRA CHECKOUTS, BOUGHT TWO WAYS
worktree addgit clone
wall clock5.3 s5 min 56 s
downloadednothing3.0 GiB
history written0 bytes3.0 GiB
working trees written180 MB180 MB
ledger closed
The agents themselves are the subject of a different tutorial: how one support ticket gets closed — the loop, the tools, the guardrails, and the nine seconds of human that made it safe.
$ git worktree list/Users/you/code/react 9a81195bed [main]