Git worktrees: parallel checkouts for you and your coding agents
You are mid-feature with a dirty tree when a review request lands. So you stash, switch branches, wait for the build caches to churn, review, switch back, pop the stash, and resolve the conflict the stash pop just invented. Every context switch costs ten minutes and a little confidence.
Git has had a built-in answer since 2.5, and most people still have not adopted it: the same repository can have several working directories at once. And it has quietly become relevant for a second audience, because a coding agent working in your checkout has the same problem you do, times the number of agents.
What a worktree is
git worktree add creates a linked checkout: a second working directory attached to the same repository, sharing its object database, refs, and config. Nothing is cloned and nothing is duplicated except the checked-out files themselves. Each worktree has its own HEAD, its own index, and one hard rule: a branch can only be checked out in one worktree at a time.
| Command | What it does |
|---|---|
git worktree add <path> <branch> | check the branch out at that path |
git worktree add -b <new> <path> [base] | create the branch there, from base or HEAD |
git worktree list | every checkout of this repo, with branch and commit |
git worktree remove <path> | delete the checkout (refuses if dirty) |
git worktree prune | drop records of worktrees whose directory is gone |
A commit made in any worktree is instantly visible to all of them, because there is only one repository underneath. That is the whole trick: branches stop competing for your single working directory.
A gardening metaphor captures it nicely. A branch is a sprout on the tree. A worktree takes that sprout and gives it a pot of its own: still the same plant, still fed by the same roots, but growing on its own windowsill.
Layout
Three layouts are in common use, and the one we settled on is <repo>/.worktrees/<name>. The commands are easy; the layout is where workflows diverge, and it is worth choosing deliberately, because worktrees scatter by default: hand-made ones end up as sibling directories with ad-hoc names, and tools bring their own conventions on top.
Sibling directories
../repo-feature-x is the common reflex. It keeps the repo clean but litters the parent directory, and every worktree path is a convention only you remember.
A bare-repo layout
repo/.bare plus repo/main, repo/feature-x as peers makes all checkouts symmetric, no worktree is more "real" than another. It is elegant, but it means restructuring every clone, and bare clones need a fetch-refspec fixup before remote-tracking branches behave.
Inside the repo
<repo>/.worktrees/<name> keeps everything in one predictable place: one convention, trivially discoverable, and the path from any worktree back to the primary checkout is always the same. The catch is that git must be told to ignore the directory, and the right place is .git/info/exclude, which is local to your clone, so your workflow choice never appears in the project's committed .gitignore.
Two honest costs come with it. Tools that are not git-aware (find, a Docker build context, a naive grep -r) will see the extra checkouts unless told otherwise, and git clean -fdx becomes dangerous because -x ignores exclude rules and will happily empty every worktree you have. Know those two, and the convenience wins.
Gitignored state
A worktree isolates code, not state. It contains exactly what git tracks, which means it does not contain your .env, your node_modules, your virtualenv, or any other gitignored state the project needs to actually run. You create a worktree, open it, start the dev server, and the environment variables are missing. This is the first-day worktree surprise, and it is not a bug: git copying ignored files would defeat the point of ignoring them.
The fix is to automate provisioning at creation time, once, instead of remembering steps every time:
- copy (or symlink) the gitignored config the project needs,
.envand friends - run the project's setup command,
pnpm install,uv sync,bin/setup - if you run several dev servers in parallel, give each worktree its own port and, for the ambitious, its own database; the code is isolated, the runtime state is not
Once creation is automated, worktrees change character: from "a thing you set up" to "a directory that appears, ready".
Day-to-day habits
Worktrees stay cheap when naming, creation, and removal are all automatic and you keep the count low.
- Name the worktree after the branch, one name for both, and let the directory be a slugged version when the branch contains a
/. Naming two things is how they drift. - Creation should be idempotent, so "create or reuse" is one muscle-memory command and entering a worktree is always
cd "$(worktree add <name>)". - The moment a branch merges, remove its worktree and delete the branch in the same motion. Better, sweep them: anything merged with a clean tree can go. Abandoned worktrees are the layout's compost heap.
- Only a few should be active at once. Worktrees remove the cost of context switching, not the cognitive load of five parallel tasks.
Coding agents
Parallel agent sessions are the strongest new case for worktrees: two agents sharing one working directory will trample each other's edits, and an agent stashing your half-done work is worse. One worktree per agent session is the emerging consensus, and Claude Code builds it in with claude --worktree <name> and worktree-isolated subagents.
But by default Claude puts its worktrees in .claude/worktrees/, which means your checkouts now live in two conventions: yours and the agent's. Claude Code exposes hooks precisely for this: a WorktreeCreate hook lets a script of yours decide where the worktree goes, and a WorktreeRemove hook cleans up afterwards.
Create and remove hooks
Wire the two as a pair, or you get half a convention. Two details matter:
- A custom create hook replaces Claude's own worktree logic, including its
.worktreeincludefile copying, so your hook must copy the gitignored config itself or agent worktrees silently lose their.env. - Without a remove hook, agent worktrees in a custom location accumulate, because Claude's automatic cleanup is built around its default layout.
One shared convention makes everything downstream simpler: the same provisioning runs for you and for agents, the same sweep cleans up after both, and your prompt tells you at a glance whether you are standing in a linked worktree or the primary checkout.
How Dotfiles Pro ships it
The git role installs a single-file worktree CLI (bare worktree shows the overview) that owns the whole convention: worktrees at <repo>/.worktrees/<name> on a branch named after them, excluded via .git/info/exclude. In the shell it doubles as a selector, the way the kubernetes role's kube-namespace scopes a terminal to a namespace: worktree <name> creates, provisions, and enters in one move.
$ worktree feature-x # select: create, provision, enter
$ worktree # branch-state overview: dirty, ahead/behind, merged/gone
$ worktree rm -d feature-x # remove, delete the branch when merged
$ worktree clean # sweep every merged (or squash-merged), clean worktreeProvisioning is two local git config keys, so nothing executable is committed to the project: wt.copy globs are copied from the primary checkout (default .env and .env.local) and wt.setup runs inside the new worktree with WORKTREE_PATH, WORKTREE_BRANCH, and MAIN_WORKTREE_PATH exported:
$ git config wt.setup 'pnpm install'
$ git config --add wt.copy 'config/*.local.yaml'The claude role points both WorktreeCreate and WorktreeRemove at the same script, so agent worktrees are created, provisioned, and swept exactly like human ones. And both the Starship prompt and the Claude Code status line mark where you are standing: on a branch you see the sprout (🌱, or a branch glyph in Nerd Font style), and inside a linked worktree it becomes the potted plant (🪴, or a link glyph), the same sprout in its own pot.
One rough edge is worth flagging, and it belongs to Claude Code, not the convention. A session that starts in a .worktrees/ worktree behaves normally, but you cannot ask a running session to hop into an existing one: the in-session EnterWorktree tool bypasses the WorktreeCreate hook and only reaches worktrees under Claude's own .claude/worktrees/, and a symlink cannot bridge the gap because the check resolves it back to the real path. It is tracked upstream in #36205 and #48967; when the switch learns to honor the hook, that last gap closes. Until then, launch a fresh session in the worktree rather than switching into it.
One repo, one .worktrees/, humans and agents on the same convention.