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 pair of emoji 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.
Where to put them decides how they feel â
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) are the common reflex. They keep the repo clean but litter 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.
We use the third layout. 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.
The trap everyone hits: a worktree isolates code, not state â
A new worktree 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 everyone hits, 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".
Habits that make worktrees cheap â
- 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. - Make creation idempotent, so "create or reuse" is one muscle-memory command and entering a worktree is always
cd "$(worktree add <name>)". - Remove a worktree the moment its branch merges, 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.
- Keep only a few active. Worktrees remove the cost of context switching, not the cognitive load of five parallel tasks.
The agent angle â
Parallel AI coding sessions are the strongest new case for worktrees since their invention: 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.
Wire the create and remove hooks as a pair â
Two details matter when you wire this up:
- 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.
The payoff of one shared convention is that everything downstream gets simpler: the same provisioning runs for you and for agents, the same sweep cleans up after both, and your prompt can tell you at a glance when you are standing in a linked worktree rather than 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.