This is a follow-up to my post on Claude Code’s worktree isolation. That post was about the experience. This one is about the mechanics. If you’ve ever wondered what a worktree actually is — not the pitch, but the internals — read on.
I ran every command in this post for real. The output is actual output.
The problem worktrees solve
You’re deep in a feature. Files modified, half-committed, brain in the zone. Then someone drops a message: “urgent bug in production, can you look?”
Here’s what you do if you don’t know about worktrees:
git stash
git checkout main
git checkout -b hotfix/urgent-bug
# fix the bug
git commit -m "fix: urgent bug"
git checkout main
git merge hotfix/urgent-bug
git stash pop
That last step is where it falls apart. The stash pop can conflict with the merge result. I ran exactly this scenario to show you what happens:
$ git stash
Saved working directory and index state WIP on main: 9ad49c8 add lib.rb
$ git merge hotfix/urgent-bug
Fast-forward
app.rb | 1 +
1 file changed, 1 insertion(+)
$ git stash pop
Auto-merging app.rb
CONFLICT (content): Merge conflict in app.rb
Even on a trivial file with no real conflict, git can’t reconcile the stash over the merge. Now you’re debugging a conflict that didn’t exist before you tried to be careful. This is the stash trap — it feels safe going in, and it bites you coming out.
What a worktree actually is
A git repo has one .git/ directory containing everything: object store, refs, HEAD, config.
Normally you get one working tree — the files on disk that correspond to your current branch.
A worktree gives you a second (or third, or fourth) working tree pointing at the same .git.
Each worktree has its own:
HEAD— which branch it’s onindex— its own staging area- working directory — its own files on disk
But they all share the same object store. No copying. No re-downloading. Just new pointers.
Creating one
The command takes a path and, optionally, -b to create a new branch at that worktree:
$ git worktree add .worktrees/hotfix -b hotfix/urgent-bug
Preparing worktree (new branch 'hotfix/urgent-bug')
HEAD is now at 9ad49c8 add lib.rb
$ git worktree list
/private/tmp/wt-demo 9ad49c8 [main]
/private/tmp/wt-demo/.worktrees/hotfix 9ad49c8 [hotfix/urgent-bug]
Without -b, git checks out an existing branch into the new worktree. With -b, it creates a
fresh branch first — so the worktree starts clean from your current commit, on a branch that’s
yours to mess with.
Think about it as a shortcut directory to another branch you need to work in parallel.
Two working directories. Two branches. One .git. And critically — my uncommitted changes in
main are completely untouched:
$ cat app.rb
app.rb content v1
uncommitted work in progress ← still here
$ cat .worktrees/hotfix/app.rb
app.rb content v1 ← clean, from last commit
The worktree starts from the last commit, not from your dirty working directory. This is the key insight: you never have to stash anything. Your work-in-progress just stays where it is.
How git stores the worktree
When you add a worktree, git does two things:
1. Creates a .git file (not directory) in the worktree:
$ file .worktrees/hotfix/.git
.worktrees/hotfix/.git: ASCII text
$ cat .worktrees/hotfix/.git
gitdir: /private/tmp/wt-demo/.git/worktrees/hotfix
It’s a pointer. Not a full git repo — just a path back to the main .git. This is why the
worktree directory is tiny:
212K .git/ ← main repo
16K .worktrees/hotfix ← worktree (just files + a pointer)
108K ../wt-clone/.git ← a full clone for comparison
A clone duplicates the entire object store. A worktree costs almost nothing.
2. Creates metadata under .git/worktrees/:
$ ls .git/worktrees/hotfix/
COMMIT_EDITMSG HEAD ORIG_HEAD commondir gitdir index logs/ refs/
$ cat .git/worktrees/hotfix/HEAD
ref: refs/heads/hotfix/urgent-bug
This is where git tracks the worktree-specific state: which branch it’s on, its own index,
its own reflog. The worktree’s .git file points here; this directory points back to the
worktree. A bidirectional link.
The one-branch-per-worktree rule
Git enforces that no two active worktrees can be on the same branch simultaneously:
$ git worktree add .worktrees/hotfix2 hotfix/urgent-bug
fatal: 'hotfix/urgent-bug' is already used by worktree at
'/private/tmp/wt-demo/.worktrees/hotfix'
This isn’t a limitation — it’s a safety guarantee. Two worktrees on the same branch would have independent indexes, so they could create divergent staging states that git can’t reconcile. The constraint is what makes worktrees safe.
Working in the worktree
From the worktree directory, everything behaves like a normal git repo:
$ cd .worktrees/hotfix
$ echo "bug fix applied" >> app.rb
$ git add . && git commit -m "fix: urgent bug"
[hotfix/urgent-bug b19ba6f] fix: urgent bug
$ git log --oneline
b19ba6f fix: urgent bug
9ad49c8 add lib.rb
Back in the main directory — nothing changed:
$ cd /tmp/wt-demo
$ git log --oneline
9ad49c8 add lib.rb ← hotfix commit is not here yet
2d045f8 add app.rb
The branches diverged the moment you committed in the worktree. Now you can merge when you’re ready — not when git forces you to.
Cleanup
$ git worktree remove .worktrees/hotfix
$ git worktree list
/private/tmp/wt-demo b19ba6f [main]
Git removes the worktree directory and cleans up .git/worktrees/hotfix/ automatically. The
branch stays — it’s yours to merge or delete when you’re done reviewing.
You can also reattach a branch to a new worktree later:
$ git worktree add .worktrees/hotfix hotfix/urgent-bug
Preparing worktree (checking out 'hotfix/urgent-bug')
The worktree and the branch are independent objects. The worktree is just a checkout location.
Worktrees vs everything else
Here’s the honest comparison:
| Approach | Your WIP | Context switch | Risk |
|---|---|---|---|
git stash |
Hidden in reflog | Manual stash/pop | Conflicts on pop |
New branch + git switch |
Modified files stay | Must be clean first | Easy to forget |
git clone |
Untouched | Full repo copy | Wastes disk, no shared history |
git worktree |
Untouched | Instant, parallel | None |
git stash works for quick one-minute switches. It breaks down when the switch takes longer than
expected, when you stash multiple times, or when the thing you merged conflicts with what you
stashed. Everyone has lost work or time to the stash at some point.
git switch (or checkout) requires a clean working directory. If you have modified tracked
files, git refuses or warns. You end up stashing anyway — back to the same problem.
git clone gives you a fresh directory, but it’s a full copy. Object store, history, remote
refs — all duplicated. Then you need to keep both in sync manually. It’s workaround, not workflow.
Worktrees are the only approach where your main working directory is genuinely untouched and the new context is immediately available at a fixed path, for almost no cost.
What it actually costs
The object store — all the blobs, trees, and commits — is shared. What you pay for per worktree:
- A working copy of the files at the checked-out commit (~= size of your working tree)
- A small metadata directory under
.git/worktrees/(a few KB) - One
indexfile per worktree
For a typical project, adding a worktree costs roughly the same as checking out a branch normally: the working files plus a tiny bit of metadata. Not a clone. Not extra disk for history.
Practical patterns
Hotfix while keeping WIP:
git worktree add .worktrees/hotfix -b hotfix/urgent-bug
cd .worktrees/hotfix
# fix, commit
cd ../..
git merge hotfix/urgent-bug
git worktree remove .worktrees/hotfix
Review a colleague’s branch without losing your place:
git fetch origin
git worktree add .worktrees/review origin/their-branch
# open the app, run tests, look at the code
git worktree remove .worktrees/review
Run two feature experiments in parallel:
git worktree add .worktrees/approach-a -b experiment/approach-a
git worktree add .worktrees/approach-b -b experiment/approach-b
# work on both, compare diffs
git diff experiment/approach-a...experiment/approach-b
For projects with .env or other gitignored config:
cd .worktrees/hotfix
ln -s ../../.env .env
# everything runs as normal
The worktree shares git history but not gitignored files. Symlinking .env is a one-liner and
you’re running the full app in isolation.
The mental model shift
The stash-switch-unstash loop teaches you to think of your working state as fragile — something to hide before doing anything risky. You become conservative about context switches. You defer investigation because “switching now would be annoying.”
Worktrees invert this. Your working state is stable. New contexts are cheap. You can work on two things simultaneously without either affecting the other.
The filesystem becomes your interface. .worktrees/hotfix/ is the hotfix. .worktrees/review/
is the review. You can have both open in different terminal tabs at the same time. Git handles
the isolation; you just navigate directories.
Once you have this model, you stop dreading interruptions.
Happy hacking!