ONE OBJECT STORE · FOUR WORKING TREES · 27 COMMITS · ONE MORNING
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.