Git Worktree for Parallel Development: Why It's Suddenly Useful Again
git worktree has been around since 2015, but coding agents made it relevant again. A practical guide to running multiple branches in one repo without thrashing your stash.
By Toolery Team · April 27, 2026
The git worktree command shipped in Git 2.5 in July 2015. For most of the decade since, it was an obscure feature that showed up in conference talks and rarely on people's keyboards. The standard "one branch at a time" workflow with stash, a few feature directories, or a second clone covered most cases well enough.
Then coding agents arrived. As soon as you have a Claude Code session, a Codex run, and a teammate all wanting to touch the same repository at the same time, the single-checkout workflow falls apart. git stash pop on a half-completed agent change is how repositories end up with three weeks of work jammed into a stash list nobody trusts to inspect. Worktree sidesteps the problem by giving each parallel task its own working directory, all sharing one set of objects. This guide walks through the practical pattern, the pitfalls, and a checklist you can copy.
What worktree actually is
A worktree is a working directory that points at the same Git object database as your main checkout. The connection lives in a single file inside the worktree's .git — which, unusually, is a file, not a directory. That file contains a single line like gitdir: /Users/you/repos/project/.git/worktrees/auth-feat, and Git follows it back to the shared object store on every command.
That detail matters because it explains the constraints. There's only one object store, so a fetch on one worktree is visible to all others immediately. There's only one ref namespace, so two worktrees cannot have the same branch checked out at the same time — Git will refuse with fatal: '<branch>' is already checked out at <path>. And operations that write to the index (commit, merge, rebase) are local to one worktree, so two worktrees can't deadlock each other's commits.
The basic command set is small:
# Add a worktree at ../auth-feat tracking an existing branch
git worktree add ../auth-feat feat/auth
# Add a worktree on a new branch off of main
git worktree add -b fix/login-302 ../bugfix-302 main
# List active worktrees
git worktree list
# Remove a worktree (only when it's clean)
git worktree remove ../auth-feat
# Garbage-collect references to deleted worktrees
git worktree prune
That's roughly 80% of what most people ever need. The interesting question is the directory layout you adopt around it.
The bare-repo layout
The default pattern — running git worktree add from inside a normal clone — works, but it puts your "main" working tree at the same level as the others, and the repo's name doubles as the directory name for the main checkout. That's awkward as the number of worktrees grows. The cleaner pattern, taken from the Git docs and widely used by engineers running multiple agents, is to make the bare repository the source of truth and put every working directory next to it as a worktree:
# Clone bare — only the .git contents, no checkout
git clone --bare https://github.com/example/project.git project.git
cd project.git
# Main development checkout
git worktree add ../main-dev main
# Feature branches, each independent
git worktree add ../auth-feat feat/auth
git worktree add ../payment feat/payment
git worktree add ../admin-panel feat/admin
# Hotfix and release lanes
git worktree add ../hotfix hotfix/login
git worktree add ../staging release/2026.4
git worktree list
# /Users/you/repos/project.git (bare)
# /Users/you/repos/main-dev abc123 [main]
# /Users/you/repos/auth-feat def456 [feat/auth]
# ...
Now every working directory is symmetric. There's no "main repo" that's structurally different from a feature directory; they're all peers pointing at project.git. Two consequences fall out of this. First, you can drop or recreate any worktree without worrying about losing your "real" checkout — the object database lives separately. Second, your shell history and IDE workspaces don't have to be ambiguous about which branch you're in: the directory name is the branch.
One small footgun: git fetch in a worktree updates the shared remote refs, which is what you want. But git push from a worktree pushes the branch checked out there, which usually but not always is what you want. If you have push.default set to matching across an old config, it'll try to push every matching branch and that's almost never the intent for a per-worktree workflow. Set push.default = current globally before you start using worktrees seriously.
The agent-parallelism use case
The reason worktree showed up in 2026 productivity conversations is coding agents. A single repository with three Claude Code sessions, two Codex runs, and an open IDE is a contention disaster on a single checkout. The agents will trip over .git/index.lock within minutes. Stash-and-restore breaks because nobody knows which agent the top stash belongs to. Branch switching mid-edit triggers cascades of dirty-tree refusals.
The worktree pattern fixes this by giving each agent its own directory:
cd ../project.git
git worktree add ../agent-a -b agent/refactor-auth main
git worktree add ../agent-b -b agent/add-tests main
# Agent A runs in ../agent-a — its index, its working tree
# Agent B runs in ../agent-b — completely independent
# Both share objects, so a fetch in either is visible everywhere
Two practical wins. (1) When agent A commits and force-pushes a branch, agent B's branch state is unaffected — different branches, different working directories. (2) When agent A finishes and you merge to main from ../main-dev, agent B's git pull --rebase picks up the same objects without re-fetching.
Three pitfalls show up in this mode. First, hook scripts in .git/hooks are shared across all worktrees, because hooks live in the shared .git directory. If a hook depends on the working-tree path, it has to read $GIT_WORK_TREE rather than assume $PWD. Second, sub-modules are per-worktree by default but their object store lives next to the parent's — running git submodule update from two worktrees concurrently can race on the submodule index. Third, large LFS repos can rack up disk usage fast because LFS smudge filters write the working-tree copy per worktree (the LFS object store is shared, but each checkout still materializes its own files).
The branches-and-HEAD trap
Worktrees expose a corner of Git semantics that single-checkout workflows hide. HEAD, HEAD^, and HEAD~ are per-worktree: each working directory has its own HEAD pointer. That's intuitive once you say it out loud, but it bites when scripts assume "the HEAD" is global. A pre-commit hook that calls git rev-parse HEAD resolves to the worktree's HEAD, not the bare repo's. A CI script that runs git log HEAD..origin/main in one worktree gives a different answer than the same script in another.
Two specific notations confuse people: HEAD^ means "first parent of HEAD" (so on a merge commit, it's the branch that was merged into), and HEAD~ means "follow the first-parent chain back N steps" (HEAD~3 = three commits back along first parents). Worktree doesn't change those semantics, but because each worktree has its own HEAD, scripts that compare HEAD across two paths need to specify which one. Use git -C ../auth-feat rev-parse HEAD rather than cd juggling.
When NOT to use worktree
Worktree adds friction in two cases worth flagging. If your repository is small (under ~100 MB) and your changes are small, a second clone is simpler and the disk overhead is trivial — there's no shared-object-store benefit big enough to pay back the conceptual cost. If your team relies heavily on Git hooks that assume a single working tree (lint-staged in pre-commit hooks is a common one), audit them first; many third-party hook frameworks were never tested under worktree and break in subtle ways.
One absolute limitation: a single branch can be checked out in at most one worktree at a time. If two agents need to work on feat/auth simultaneously, give them sibling branches off the same base (feat/auth-a, feat/auth-b) and merge later, or run one of them in detached-HEAD mode (git worktree add ../scratch <commit-sha> creates a worktree pointing at a commit with no branch, useful for read-only inspection but not for committing back).
A copy-paste checklist
For an existing repository you already have cloned:
cd existing-clone
git config --global push.default current # one-time
# Create a sibling worktree for the next feature
git worktree add ../$(basename $PWD)-newfeat -b feat/something main
# When you're done
cd ../$(basename existing-clone)
git worktree remove ../$(basename existing-clone)-newfeat
For a fresh setup using the bare-repo layout:
git clone --bare git@github.com:example/project.git project.git
cd project.git
# Tell git to fetch ALL remote branches (bare repos default to none)
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin
# First worktree
git worktree add ../project-main main
And a deletion sanity check before removing a worktree directory by hand:
git -C ../auth-feat status # is it clean?
git worktree remove ../auth-feat # use this, not rm -rf
git worktree prune # if you did rm -rf, run this
Limitation worth saying out loud
This article is about the typical case: a single developer (or a developer plus agents) working in a repo on one machine. Worktrees synced across multiple machines via shared filesystems (NFS, Dropbox, iCloud) are not a supported pattern — the .git/worktrees/<name>/locked file gets confused, and the object store can race in ways that produce unrecoverable index corruption. Keep worktrees on a local SSD, and use normal clones if you need a copy on another machine.
For more on the surrounding workflow tooling, see dotfiles & shell productivity for shell aliases that pair well with the bare-repo layout, SSH config patterns for managing remotes across multiple GitHub identities, and the Obsidian + Claude Code workflow for how worktree fits into a multi-agent setup.