Parallel Development with Git Worktrees (and Why AI Agents Changed the Game)
You are forty minutes into a refactor. Eleven files are dirty, three of them half-renamed, and the test suite is red in the way that means "still working." Then the pager goes off: production is broken and the fix is a one-line change on main.
Historically both options were bad. Stash the pile, switch, fix, switch back, git stash pop, and hope it applies against a base that moved under you. Or clone the repository again and spend an hour discovering the clone has no .env, no node_modules, and a different remote setup — a copy that starts diverging the moment it exists.
git worktree deletes the whole category of problem. One repository, several working directories, each on its own branch, all live at once.
## What a worktree actually is
A worktree is an additional working directory attached to an existing repository. The checkout you cloned into is itself a worktree — the main one. git worktree add creates linked worktrees beside it.
The split is simple: history is one, checkout state is many. The object database, every ref under refs/ (branches, tags, remote-tracking refs, even the stash), the config, and the hooks live once in the repository and are shared by all of them. What each worktree owns privately is its HEAD, its index, and the files on disk.
That is why creating one is instant and nearly free: no network round trip, no object copying — Git writes a small metadata directory and checks out a tree. A repo with 2 GB of history and a 200 MB working tree costs roughly 200 MB per extra worktree, and the disk cost that actually matters is node_modules, not Git.
One rule to internalize before anything else: a branch can be checked out in at most one worktree at a time. Every worktree gets its own branch, or no branch at all.
## The hotfix, worktree edition
Back to the pager. Leave the dirty refactor exactly where it is and give the hotfix its own directory. If the branch already exists, name it:
git worktree add ../myapp-hotfix hotfix
git worktree listIf the branch doesn't exist yet, -b creates it and checks it out in one step. Pass an explicit start point so it branches from main rather than from whatever your dirty HEAD happens to be:
git worktree add -b hotfix/login-500 ../myapp-hotfix main../myapp-hotfix is now a complete, clean checkout with the new branch on it, and your refactor sits untouched in the original directory. Fix, commit, push — the two directories never learn about each other's working state.
cd ../myapp-hotfix
# edit the file, then:
git commit -am "Fix null session on login"
git push -u origin hotfix/login-500git worktree list is your map. It prints every working tree of the repository — main first — with path, commit, and branch, plus locked and prunable annotations under -v. The output is identical no matter which worktree you run it from, so you never have to remember where you left things.
Reviewing someone else's branch is the same move. If feature-x exists on exactly one remote but not locally, git worktree add creates the tracking branch for you. Fetch first, or there is nothing to match against:
git fetch origin
git worktree add ../myapp-review feature-xAnd when you only need the files at some commit — building an old release, reproducing a regression — skip the branch entirely with a detached worktree. Just remember that commits made on a detached HEAD belong to no branch and become garbage-collectable once the worktree is removed and pruned; run git switch -c keep-this inside it first if the experiment turned out to matter.
git worktree add -d ../myapp-v2.1 v2.1.0## Where to put them
Worktrees must live outside the repository's own working tree. Two conventions cover almost everyone:
- Siblings:
../myapp-hotfix,../myapp-feature-authnext tomyapp/. Zero setup, easy tocdbetween, clutters the parent directory once you pass three or four. - A container directory:
../myapp-worktrees/author~/worktrees/myapp/auth. Keeps the parent clean and makes bulk cleanup trivial — the layout to pick if you run agent fleets. - Inside the repo (
.worktrees/) works too, but the directory must be gitignored and excluded from your build, lint, test, and search tooling, or every scan walks into a copy of your own codebase.
Name the directory after the work, not after the day: myapp-hotfix-login-500 still means something next week, myapp-tmp2 does not. Matching the directory name to the branch name (agent/auth in agents/auth) means git worktree list reads as a task list.
## One agent per worktree
Worktrees were a niche convenience for a decade. AI coding agents made them load-bearing.
An agent is not a reader. It edits files, stages them, commits, starts dev servers, and runs test suites. Point two agents at one checkout and you get every failure mode simultaneously: conflicting edits to the same file, one agent staging the other's half-finished work into a commit, contention on .git/index.lock, and file watchers rebuilding on changes that belong to someone else's task. None of that surfaces as a clean error — it surfaces as confusing output an hour later.
A worktree per agent removes all of it. Each agent gets its own directory, index, HEAD, and branch while sharing one repository. Because worktree add is a local, near-instant operation, fanning out is just a loop:
for t in auth search billing; do
git worktree add -b agent/$t ../myapp-agents/$t main
done
git worktree list// note: Whatever launches the agent must start inside the worktree. An agent started in the main checkout will happily edit the main checkout no matter which branch you intended. Pass the directory explicitly — tmux new-window -c ../myapp-agents/auth — or cd before launching.
tmux new-session -d -s agents -n auth -c ../myapp-agents/auth
tmux new-window -t agents -n search -c ../myapp-agents/search
tmux attach -t agentsCoding CLIs have started building this in. Claude Code accepts claude --worktree <name>, which starts the session inside a managed worktree branched from your current commit, and its subagents can run in ephemeral worktrees that are cleaned up automatically when left unchanged. Orchestrators create a worktree per task and hand each one to an agent. For anything without built-in support the manual recipe works everywhere: create the worktree and branch, launch the agent with its working directory inside it, review the branch when it finishes.
The payoff beyond parallelism is blast radius. An agent that goes sideways — deletes the wrong directory, force-resets a branch, leaves a broken build — did it inside one directory on one branch. Your main checkout, your uncommitted work, and the other agents' trees are all still intact, and the recovery is git worktree remove --force rather than an afternoon of forensics.
Reviewing is done from your main checkout. Branches are shared refs, so every agent's commits are visible the moment they're made — no push, no pull, no waiting:
git diff main...agent/auth
git log --oneline main..agent/auth
git diff agent/auth agent/search -- src/// note: Use three dots, not two, when reviewing an agent's work. git diff main..agent/auth also includes everything that landed on main after the branch point, which inflates the diff with changes the agent never made. main...agent/auth shows only what the agent did.
## The gotchas that actually bite
### A branch can only be checked out once
Add a worktree for a branch that is already live somewhere and Git stops you: fatal: 'hotfix' is already checked out at '/path/to/myapp-hotfix'. That is a safeguard, not an inconvenience — if two directories held the same branch, a commit in either would leave the other's files silently stale. Three ways out: check out a different branch, use git worktree add -d when you only need the files at that state, or --force if you genuinely know what you are doing. You usually don't.
### Nothing untracked follows
A new worktree contains tracked files only. .env, .env.local, certificates, seed databases, build caches, and node_modules all stay behind in the original checkout — which means the failure shows up at runtime, not at checkout, and reads like an application bug. Provision each worktree right after creating it, and give each dev server its own port:
cp .env .env.local ../myapp-agents/auth/ 2>/dev/null
pnpm install --dir ../myapp-agents/auth
git -C ../myapp-agents/auth status --shortpnpm makes this nearly free — packages are hard-linked from a global content-addressable store, so ten worktrees cost roughly one copy of disk. npm and yarn copy full trees every time, which is where the "worktrees use too much space" complaint actually comes from.
// note: Don't symlink node_modules or .env between worktrees to save time. Different branches carry different lockfiles, postinstall scripts write machine state into the tree, and a shared .env means one agent rewriting config silently changes every other worktree. Run a real install per worktree.
### Hooks and config are shared
There is one .git/hooks directory (or one core.hooksPath) for the whole repository, so your Husky or pre-commit setup runs inside agents' worktrees too — usually exactly what you want. For an exception, enable extensions.worktreeConfig and set the value with git config --worktree, which gives that worktree a private config file.
### Moved repos and removable media
Worktree links are absolute paths by default. Move or rename the main repository and every linked worktree starts reporting fatal: not a git repository; git worktree repair run from the main worktree relinks them. A worktree on an external drive or network share looks deleted whenever the volume is unmounted, and pruning would then destroy its metadata — create it with git worktree add --lock so it is protected from the start rather than locking it afterwards.
## Getting the work back
Because everything lives in one repository, integration needs no push or pull. From the worktree that has main checked out, merge the winner, or cherry-pick the good commits out of several competing attempts. Push the branch and open a PR when you want CI and human review:
git merge agent/auth
git cherry-pick 4f2a91c
git push -u origin agent/searchRun that merge from the checkout that actually holds main. Git refuses to move a branch that is checked out in another worktree, so you cannot fast-forward main from inside an agent's directory while your main checkout is sitting on it.
## Cleanup discipline
Worktrees are cheap to create, which is exactly why they accumulate. The teardown after a branch merges is three commands in a fixed order:
git worktree remove ../myapp-agents/auth
git worktree prune
git branch -d agent/authThe order matters: git branch -d refuses to delete a branch that any worktree has checked out, so the worktree goes first. remove deletes the directory and its repository-side metadata together; prune sweeps up entries for directories that vanished some other way; branch -d only deletes merged branches, and -D forces.
remove also refuses to touch an unclean tree, and "unclean" includes untracked files — a single build artifact or a node_modules directory is enough to block it. Glance at git -C <path> status first, then use --force when the leftovers are disposable. Locked worktrees demand --force twice, on purpose. Removing a worktree never deletes its branch or its commits; the branch simply stops being checked out anywhere.
// note: If you ever delete a worktree directory with rm -rf, the metadata in .git/worktrees survives and Git still believes that branch is checked out there. That is the source of the classic fatal: 'X' is already checked out at ... pointing at a directory that no longer exists — and of cannot delete branch errors for branches you retired weeks ago. git worktree prune clears both.
## The point
Worktrees turn context switching from state management into cd. The stash, the throwaway WIP commit, the half-applied pop, the second clone that drifts — all of it was overhead invented to work around the assumption that a repository has exactly one checkout. Drop that assumption and the dirty refactor stays dirty in one directory while the hotfix ships from another, and five agents each own a tree nobody else can touch. The only new obligation is cleaning up after yourself, and that is three commands.