Originally published at heycc.cn. This is a mirrored copy — the canonical version is kept up to date at the source.
Git Worktree Mechanics in 2026: Every Command, Every Error, Decoded
git worktree shipped in Git 2.5 in July 2015 as an experimental replacement for the old git-new-workdir contrib script, and for a decade it stayed a niche power tool, the thing you reached for when a hotfix interrupted a half-finished refactor. Then AI coding agents made parallel checkouts mainstream: a single Git checkout has exactly one working directory, one HEAD, and one index, so two agents editing the same checkout is a race condition, not a workflow. We covered that multi-agent pattern — worktree-per-agent, branch naming, launch and cleanup commands — in Parallel AI Agent Workflows with Git Worktrees. This article is the mechanics and error reference behind it: what's actually on disk, what's shared and what isn't, the DWIM branch rules that decide which branch you land on, the full subcommand surface including the parts tutorials skip (lock, prune, repair, --porcelain, --orphan, per-worktree config and sparse-checkout), and every refusal error reproduced verbatim on Git 2.52.0, plus the exact version gates (2.5 / 2.16 / 2.17 / 2.20 / 2.42) that explain a decade of contradictory tutorials.
The mental model: one object store, many checkouts
Per the official documentation, every repository has exactly one main worktree (the one created by git init or git clone) and zero or more linked worktrees created with git worktree add. Linked worktrees share everything except per-worktree files such as HEAD and the index. Concretely:
Shared across all worktrees (stored once, in the main repo's .git):
- The object database: every commit, tree, and blob. Adding a worktree copies no history.
- All refs: branches, tags, remotes. Perhaps surprisingly, that includes the stash (more on that below).
- Repository config (
.git/config), hooks, and remotes.
Private to each worktree:
- The working files themselves.
-
HEAD. Each worktree can have a different branch (or detached commit) checked out. - The index (staging area).
git addin one worktree doesn't touch another.
The plumbing that makes this work is a pair of pointers. In a linked worktree, .git is not a directory; it's a file containing a single line:
$ cat ../app-hotfix/.git
gitdir: .../wtdemo/app/.git/worktrees/app-hotfix
That points into an admin directory $GIT_DIR/worktrees/<id>/ inside the main repo, which holds the linked worktree's private HEAD, index, an optional locked file (containing the lock reason as plain text), and a gitdir file pointing back at the linked worktree's .git file. The <id> is normally the basename of the worktree path, with a numeric suffix appended if two worktrees would collide. This two-way pointer pair is exactly what breaks when you move directories by hand, and exactly what git worktree repair exists to fix.
Worktree vs branch-switching vs clone vs stash vs submodule
These five get conflated constantly. They solve different problems:
git checkout / switch
|
git stash |
Second git clone
|
git worktree add |
Submodules | |
|---|---|---|---|---|---|
| Problem it solves | Move one working dir between branches | Shelve uncommitted changes temporarily | Fully independent second copy | Multiple simultaneous checkouts, one repo | Embed a different repo at a path |
| Parallel checkouts | No, one at a time | No | Yes | Yes | N/A |
| Disk cost | None extra | None extra | Full duplicate of history + files | Working files only; objects/refs shared | Per-submodule clone |
| Refs/branches shared | — | — | No; must push/pull between clones | Yes, instantly visible everywhere | No |
| Stash shared | Yes (same repo) | — | No | Yes (stash is a ref) | No |
| Uncommitted work isolation | No; carries across switch or blocks it | Sort of, but easy to lose track | Yes | Yes | Yes |
| Cleanup story | N/A | stash drop |
Delete directory; local-only branches die with it |
worktree remove + prune; branches survive |
deinit, painful |
| Same branch in two places | — | — | Allowed (they're separate repos) | Refused by default | — |
Two rows deserve emphasis because they're the classic surprises:
The stash is shared. Stashes are implemented as refs, and refs are shared. Reproduced directly (session below): git stash run in the main worktree immediately shows up in git stash list from inside a linked worktree. Worktrees isolate working files and the index, not stashes. If your muscle memory is "stash to keep things separate," worktrees change that contract.
A branch can only be checked out in one worktree at a time. Git refuses double-checkout because two working directories committing to the same branch ref would silently diverge each other's HEAD. The refusal message is identical whether you hit it via git checkout or git worktree add, and it names the offending path. This is the error that confuses people most, and it's a feature.
Against a second clone specifically: the clone gives you total isolation (separate config, separate hooks, separate refs) at the price of duplicated history, no shared branch visibility, and push/pull ceremony to move commits between copies. A worktree gives you a new checkout in about a second with zero history duplication, and a commit made in one worktree is instantly visible to all others. For monorepos measured in gigabytes, that difference is decisive.
The command surface
git worktree add and the DWIM rules that pick your branch
git worktree add -b hotfix ../app-hotfix # new branch 'hotfix' off HEAD
git worktree add ../feature-x # DWIM: creates branch 'feature-x'
git worktree add ../review-pr-42 pr-42 # check out an existing branch
git worktree add --detach ../build-test v2.3.1 # detached HEAD; throwaway builds/bisects
git worktree add --lock --reason "slow NFS" ../wt # born locked — no window for prune to race
git worktree add --orphan fresh-docs ../docs-site # unborn branch, empty working tree (2.42+)
Three flags most tutorials skip: --track / --no-track pass through to branch creation exactly as they do for git branch, controlling whether the new branch gets an upstream; --lock (optionally with --reason) locks the worktree atomically at creation; the docs note this avoids the race window of add followed by a separate lock; and --orphan (Git 2.42+) creates an unborn branch with an empty tree, useful for gh-pages-style disjoint histories.
The do-what-I-mean rules decide what branch you end up on, and they're worth knowing precisely:
-
git worktree add <path>with no branch argument creates a new branch named$(basename <path>), based onHEAD. - If a remote-tracking branch matches that name,
--guess-remotebases the new local branch on it and sets upstream (checkout.defaultRemotedisambiguates when several remotes match). Setworktree.guessRemote = trueto make this the default. - The orphan fallback: when the repository has no valid local branches at all — a fresh clone of an empty repository, or one you haven't fetched yet — a bare
add <path>behaves as if--orphanwere passed, creating an unborn branch and warning that you may want to fetch first (behavior pinned in the Git 2.42.0 release notes). In a normal repository this fallback never triggers: a typo'd commit-ish argument fails loudly withfatal: invalid reference: <name>, and a typo in the path just creates a correctly-misspelled branch offHEAD. The empty-worktree surprise is real, but it bites people who typo a branch name in a just-cloned or not-yet-fetched repo, not in everyday use. - A bare
-as the commit-ish means@{-1}, the previously checked-out branch, same asgit checkout -.
Since Git 2.16.0, worktree add also runs the post-checkout hook, just as git clone runs it on initial checkout (2.17.0 then fixed the directory the hook runs in, which is why some references misdate the feature to 2.17). Relevant if your team's hook bootstraps .env files or dependency installs.
git worktree list: human and machine formats
git worktree list # table: path, short SHA, [branch], annotations
git worktree list --verbose # adds lock reasons / prunable explanations on indented lines
git worktree list --porcelain # stable, script-safe format
The porcelain format is one attribute per line (worktree <path>, HEAD <full-sha>, branch refs/heads/<name>), with a blank line between records; boolean states like locked and prunable appear as bare labels or label-plus-reason lines, and -z NUL-terminates for paths with newlines. If you're building agent tooling on top of worktrees, parse this, never the human table.
git worktree remove, prune, lock, move, repair
git worktree remove ../feature-x # deletes working files + admin entry (clean trees only)
git worktree prune -v # garbage-collects admin entries whose directories vanished
git worktree lock --reason "on portable drive" ../feature-x
git worktree unlock ../feature-x
git worktree move ../feature-x ../active/feature-x
git worktree repair [path...] # re-links pointers after manual moves
Version trivia that explains a decade of confusing tutorials: remove and move didn't exist until Git 2.17 (April 2018). worktree remove landed as git/git commit cc73385 ("worktree remove: new command"). For the first three years, the only cleanup was rm -rf the directory and then git worktree prune. Older guides teach exactly that dance, and it still works; it's just no longer the recommended path.
lock exists for worktrees on removable or network drives: a locked worktree's admin files won't be pruned even when the path is unreachable, and it can't be moved or removed without extra force. repair handles the pointer pairs: run it in the main worktree after the main repo moved (fixes all back-pointers), run it inside a moved linked worktree to fix the main repo's forward pointer, and if both sides moved, run it from the main worktree listing each linked worktree's new path.
A reproducible session: every command and every error, with real output
The following transcript was captured on Git 2.52.0.windows.1 in a throwaway repo (one commit, master default). Output is verbatim with two cosmetic adjustments: absolute path prefixes are abbreviated to .../wtdemo, and the tab Git prints before prunable:/locked: annotation lines is rendered as spaces.
$ git worktree add -b hotfix ../app-hotfix
Preparing worktree (new branch 'hotfix')
HEAD is now at 4b6ba8f init
$ git worktree add ../feature-x # DWIM: branch named after basename
Preparing worktree (new branch 'feature-x')
HEAD is now at 4b6ba8f init
$ cat ../app-hotfix/.git # .git is a FILE in linked worktrees
gitdir: .../wtdemo/app/.git/worktrees/app-hotfix
$ git worktree list
.../wtdemo/app 4b6ba8f [master]
.../wtdemo/app-hotfix 4b6ba8f [hotfix]
.../wtdemo/feature-x 4b6ba8f [feature-x]
Now the refusals. These are the errors people actually search for:
$ git checkout hotfix # from the main worktree
fatal: 'hotfix' is already used by worktree at '.../wtdemo/app-hotfix'
$ git worktree remove ../does-not-exist
fatal: '../does-not-exist' is not a working tree
$ git worktree remove . # from the main worktree
fatal: '.' is a main working tree
$ touch ../app-hotfix/scratch.txt
$ git worktree remove ../app-hotfix
fatal: '../app-hotfix' contains modified or untracked files, use --force to delete it
$ git worktree lock --reason "on portable drive" ../feature-x
$ git worktree remove --force ../feature-x
fatal: cannot remove a locked working tree, lock reason: on portable drive
use 'remove -f -f' to override or unlock first
And the prunable lifecycle, meaning what happens when a worktree directory is deleted without Git:
$ rm -rf ../app-hotfix # deleted behind Git's back
$ git worktree list --verbose
.../wtdemo/app 4b6ba8f [master]
.../wtdemo/app-hotfix 4b6ba8f [hotfix]
prunable: gitdir file points to non-existent location
.../wtdemo/feature-x 4b6ba8f [feature-x]
$ git worktree prune -v
Removing worktrees/app-hotfix: gitdir file points to non-existent location
$ git branch # branches SURVIVE worktree removal
+ feature-x
hotfix
* master
$ git stash # after editing README in the main worktree
Saved working directory and index state WIP on master: 4b6ba8f init
$ cd ../feature-x && git stash list # stash is visible from the LINKED worktree
stash@{0}: WIP on master: 4b6ba8f init
Note the + prefix in git branch output: modern Git marks branches checked out in other worktrees with + (the current one keeps *), so you can see at a glance which branches are pinned.
The errors, decoded
fatal: '<path>' is not a working tree. The path you handed to remove, lock, unlock, or move isn't a registered linked worktree. Three usual causes: a typo'd or relative-vs-absolute path mismatch, an entry that was already pruned, or pointing at a plain directory that was never a worktree. Diagnosis is always the same: git worktree list and use a path exactly as shown there.
fatal: '<branch>' is already used by worktree at '<path>'. This is the single-checkout rule. Either work in the worktree that owns the branch, remove that worktree first, or (rarely justified) override with --force and accept that two HEADs now point at one branch ref.
fatal: '<path>' is a main working tree. You tried to remove the original checkout. Only linked worktrees are removable; the main worktree is the repository.
contains modified or untracked files, use --force to delete it. remove only deletes clean trees: no modifications to tracked files, no untracked files. This is why removing a JS project's worktree almost always needs --force: node_modules/ counts as untracked. Worktrees containing submodules also require --force even when clean.
cannot remove a locked working tree ... use 'remove -f -f' to override. Locked worktrees need force twice. A single --force is not enough; the same double-force rule applies to move. The lock reason printed in the error comes straight from the plain-text locked file in the admin directory.
prunable: gitdir file points to non-existent location is not an error but a state: the admin entry survives but its directory is gone. git worktree prune -v clears it (add --dry-run to preview, --expire <time> to only prune entries older than a threshold). Even if you never run prune, stale entries are eventually removed automatically per gc.worktreePruneExpire.
The gotcha tutorials omit: removing or pruning a worktree does not delete its branch. The transcript above shows hotfix alive and well after its worktree was pruned. Every worktree cleanup has a second step — git branch -d <name> — or your branch list grows a fossil per task.
Per-worktree configuration and sparse-checkout
By default all worktrees read the same $GIT_DIR/config. If you need settings that differ per worktree, say a different user.email in a client-work worktree, enable the extension:
git config extensions.worktreeConfig true
git config --worktree user.email agent-2@example.com
Per-worktree values land in worktrees/<id>/config.worktree. Two documented warnings: core.worktree should never live in shared config once this extension is on, and core.bare must not be shared if true; both must be migrated into the main worktree's config.worktree. Also note the compatibility cliff: pre-2.20-era Git versions refuse to touch a repository with this extension enabled at all.
Sparse-checkout composes with this: git sparse-checkout set enables extensions.worktreeConfig automatically and records both its config and its pattern file per worktree. That means two worktrees of one monorepo can materialize different directory subsets on top of the same shared object store: one agent gets services/auth/, another gets services/billing/.
Why AI agents revived a 2015 feature
The full worktree-per-agent workflow (branch naming, launching one agent per directory, reviewing and merging across worktrees, two-step cleanup) is the subject of our companion piece, Parallel AI Agent Workflows with Git Worktrees. The mechanics above explain why that pattern works: worktrees give each agent hard filesystem isolation for working files and index, with zero-copy sharing of objects and refs, so one agent's commits are instantly visible to a reviewer in the main worktree without a push/pull hop.
One mechanic deserves restating here because it's a safety property, not a convenience: the single-checkout rule is a per-branch mutex. An agent physically cannot check out a branch another agent is working on. Git refuses with the already used by worktree error before any damage happens. Multi-agent workflows get this for free from a rule added in 2015 to protect humans from themselves.
Tooling has since productized the whole lifecycle. As of mid-2026, Claude Code's official worktree documentation covers: a --worktree <name> flag that creates an isolated worktree under .claude/worktrees/<name>/ on a branch named worktree-<name>; documented EnterWorktree/ExitWorktree tools so the agent can move itself between worktrees mid-session; a .worktreeinclude file (gitignore syntax) that copies gitignored files like .env into every new worktree, a direct fix for the untracked-files cost below; and, notably, while an agent is running, Claude Code executes git worktree lock on that agent's worktree so a concurrent cleanup sweep can't remove it. That is the same lock subcommand from this guide, used as a runtime mutex rather than a portable-drive guard.
Which tool for which situation
| Situation | Right tool | Why |
|---|---|---|
| Interrupted by a hotfix mid-refactor | git worktree add -b hotfix ../hotfix |
No stash roulette; refactor stays untouched |
| Running 2+ AI agents on one repo | One worktree per agent | File/index isolation + shared refs + branch mutex |
| Reviewing a PR without disturbing your work | git worktree add --detach ../review origin/pr-branch |
Detached, disposable, no branch bookkeeping |
| Comparing runtime behavior of two branches side by side | Two worktrees | Both running simultaneously |
| Need different repo config/hooks per copy | Second clone (or extensions.worktreeConfig) |
Worktrees share config and hooks by default |
| Repo uses submodules heavily | Second clone | Official docs recommend against multiple checkouts of a superproject |
| Quickly shelving uncommitted noise for 5 minutes | git stash |
Worktrees don't shelve; they parallelize |
| Vendoring another project into this one | Submodule/subtree | Different problem entirely |
The real costs (what the advocacy posts skip)
-
Dependency directories don't come along. A fresh worktree has no
node_modules, no.venv, no build cache. Every worktree needs its own install, which costs time and disk, often more disk than the checkout itself. Mitigations: pnpm's shared content-addressable store, apost-checkouthook (runs onworktree addsince Git 2.16) that symlinks caches or runs installs, or tools like direnv per directory. -
Untracked env files don't either.
.envand friends must be copied per worktree, a classic "why does the agent's server not boot" moment. Claude Code's.worktreeincludeautomates exactly this copy for worktrees it creates; for manual worktrees it's still on you. - Ports and databases collide. Two worktrees running the same dev server fight over port 3000. Parametrize ports per worktree or per agent.
-
Submodules are explicitly second-class. The official documentation states support is incomplete; in its words, "It is NOT recommended to make multiple checkouts of a superproject." And
git worktree moveoutright refuses to move a worktree containing submodules. - Shared stash and refs cut both ways. An agent that stashes or force-updates a shared branch ref affects the whole repository, not just its sandbox. Worktrees isolate files, not refs.
Version gate cheat sheet
| Git version | What arrived |
|---|---|
| 2.5 (Jul 2015) |
git worktree (add/list/prune), replacing the git-new-workdir symlink script from contrib, labeled experimental at launch |
| 2.16 (Jan 2018) |
worktree add runs the post-checkout hook (2.17 fixed the hook's working directory) |
| 2.17 (Apr 2018) |
worktree remove and worktree move (remove: commit cc73385) |
| 2.20 (Dec 2018) |
extensions.worktreeConfig; older Gits refuse repos with it enabled |
| 2.42 (Aug 2023) |
worktree add --orphan, and the no-valid-branches fallback that auto-creates an orphan with a warning |
If your CI image or a teammate runs something ancient, the missing-remove and worktree-config cliffs above are the two that bite.
How every claim here was checked
Every command transcript in this article was executed on Git 2.52.0.windows.1 in a scratch repository; outputs are verbatim except for the two cosmetic adjustments declared above (abbreviated absolute path prefixes; the tab before annotation lines rendered as spaces). Git's newest stable release at the time of writing is 2.55.0 (June 2026); nothing in this article depends on behavior newer than 2.42. Behavioral claims — shared vs per-worktree files, the single-checkout refusal, double-force for locked worktrees, submodule restrictions on move/remove, prune expiry via gc.worktreePruneExpire, and the repair pointer semantics — were checked against the official git-worktree documentation. Version claims were checked against primary sources: the post-checkout hook running on worktree add is announced in the Git 2.16.0 release notes (with the working-directory fix noted in 2.17.0); worktree remove is git/git commit cc73385, shipped in 2.17.0; --orphan and the orphan fallback are in the 2.42.0 release notes; the July 2015 launch and experimental labeling come from the 2.5.0 release notes. Claude Code's --worktree flag, EnterWorktree/ExitWorktree tools, .worktreeinclude, and its use of git worktree lock while agents run were checked against the official Claude Code worktrees documentation on the same date.
Sources
- git-worktree official documentation
- Git 2.5.0 release notes: git worktree introduced, labeled experimental
- Git 2.16.0 release notes: worktree add runs the post-checkout hook
- git/git commit cc73385: "worktree remove: new command"
- Git 2.42.0 release notes: worktree add --orphan
- git-sparse-checkout official documentation
- githooks documentation: post-checkout
- Claude Code worktrees documentation: --worktree, EnterWorktree/ExitWorktree, .worktreeinclude

Top comments (0)