Work on multiple branches simultaneously without stashing or switching.
Each worktree is a full working directory backed by a single .git repo.
Particularly useful for CMake/C++ projects where each branch carries its own build directory.
A worktree is a linked working directory that shares the same
.git object store as your main repo. Each worktree checks out a
different branch. You can build, test, and edit in parallel —
no stashing, no context-switching overhead.
HEAD, index (staging area), and working files.
A branch can only be checked out in one worktree at a time.
(Config is shared unless you opt in to extensions.worktreeConfig, which lets
each worktree carry a private config.worktree file.)
The cleanest worktree workflow starts from a bare clone. A bare repo
has no working directory of its own — it only holds the .git internals.
Every branch you work on becomes an explicit worktree. This avoids the confusion of having
a "main" working directory that is structurally different from the others.
# Clone as bare — the directory IS the .git store git clone --bare git@github.com:org/myproject.git myproject cd myproject # Fix fetch refspec so 'git fetch' works properly git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" git fetch origin # Add worktrees — ALWAYS name the branch as a second argument git worktree add main main git worktree add develop develop git worktree add cardiac-mesh feature/cardiac-mesh # Bare clones configure no upstreams — wire each branch once so 'git pull' works git branch --set-upstream-to=origin/main main git branch --set-upstream-to=origin/develop develop # refs/heads/* are frozen at clone time — bring each worktree up to date once cd main && git pull && cd ..
1. No fetch refspec. git clone --bare writes no
remote.origin.fetch, so git fetch updates nothing but
FETCH_HEAD and you never get origin/* refs. The
git config line above is not optional.
2. No upstreams. A bare clone sets no
branch.<name>.remote for any branch. The worktree is properly attached
— git status -sb shows ## main rather than
## main...origin/main — but git pull fails with
"There is no tracking information for the current branch" until you run the
--set-upstream-to lines.
3. Two sets of refs that drift apart. A bare clone copies every remote
branch into refs/heads/* and then freezes them. Later fetches only move
refs/remotes/origin/*. A worktree added months after cloning checks out the
clone-day commit and looks silently stale — hence the git pull above.
You can also add worktrees from a normal clone. The original checkout remains and sibling worktrees live alongside it or elsewhere.
git clone git@github.com:org/myproject.git cd myproject git worktree add ../myproject-feature feature/cardiac-mesh
# Checkout an existing branch into a new directory git worktree add <path> <branch> # Create a new branch AND its worktree in one step git worktree add -b <new-branch> <path> # Create from a remote branch: local branch + tracking in one step git worktree add --track -b feature-x <path> origin/feature-x # Shorthand when <path> basename matches a remote branch name AND # no local branch of that name exists (see the note below) git worktree add --guess-remote feature-x # Detached HEAD worktree (useful for inspecting a tag/commit) git worktree add --detach <path> <commit-ish>
git worktree add <path> origin/feature-x does not
create a tracking branch — it checks out a detached HEAD at that commit,
and a later git pull fails with
You are not currently on a branch. Use --track -b
(or --guess-remote) as shown above.
--guess-remote only consults origin/* when no local
branch of that name exists. After a bare clone every remote branch already
exists locally in refs/heads/*, so in the section 02 layout the flag is a no-op
and the local (possibly stale) branch wins. It is useful from a normal clone, where
only the default branch exists locally.
git worktree list /home/user/myproject (bare) /home/user/myproject/main abc1234 [main] /home/user/myproject/develop def5678 [develop]
# Clean removal (fails if there are uncommitted changes) git worktree remove <path> # Force removal (discards uncommitted changes) git worktree remove --force <path> # If you manually deleted the directory, clean up the bookkeeping git worktree prune
git worktree move <old-path> <new-path>
This is where worktrees genuinely shine. With a normal git checkout,
switching branches invalidates your entire build directory because the source tree changed
under CMake's feet. With worktrees, each branch has its own source and build directory,
so builds are always warm.
cd myproject git worktree add feature/cardiac-mesh feature/cardiac-mesh cd feature/cardiac-mesh # Configure & build in-tree build/ directory cmake -B build -S . -DCMAKE_BUILD_TYPE=Release cmake --build build -j$(nproc)
main
compiling with GCC while feature/cardiac-mesh is building with Clang —
simultaneously, in parallel terminals.
If you prefer keeping build artefacts separate from source:
cmake -B ../myproject-builds/feature-cardiac-mesh -S .
build/ directories, make sure build/ is in your
.gitignore. With a bare clone setup, the .gitignore lives inside
each worktree (it is part of the tracked source), so this should already be covered if your
project is set up correctly.
For Python, worktrees are less about build directories and more about keeping separate virtual environments per branch, or running tests on one branch while editing another.
cd myproject/feature-refactor python -m venv .venv source .venv/bin/activate pip install -e . # editable install for this branch
.venv/ to your .gitignore. Each worktree gets its own
.venv with its own dependencies, so you can test incompatible package
versions across branches.
Running a long test suite on main while developing on a feature branch.
Reviewing a colleague's PR in an isolated directory without disrupting your work.
Comparing behaviour across two branches side-by-side in separate terminals.
| Layout | Structure | Best for |
|---|---|---|
| Bare + nested | repo/{main,feat,fix}/ |
CMake/C++ projects. Everything under one roof. Clean and discoverable. |
| Bare + flat | repo-main/, repo-feat/ |
When branch names contain slashes and you want flat sibling dirs. |
| Normal + siblings | repo/, repo-feat/ |
Quick one-off worktrees from an existing clone. Low ceremony. |
feature/cardiac-mesh creates nested directories, which suits the "bare + nested"
layout; a flat path keeps everything one level deep. Either way, name the branch explicitly:
git worktree add feature/cardiac-mesh feature/cardiac-mesh for nested, or
git worktree add cardiac-mesh feature/cardiac-mesh for flat. Never rely on the
path alone to select a branch with a slash in it — see the basename trap in section 08.
# Fetch is repo-wide — run it from anywhere git fetch --all # But pull is per-worktree (it merges into the current HEAD). # Needs an upstream (see 02) and a non-detached HEAD (see 08) cd main && git pull cd ../develop && git pull
# From the target worktree, cherry-pick by commit hash cd hotfix git cherry-pick abc1234 # Or reference a branch (commits are shared across all worktrees) git cherry-pick feature/cardiac-mesh~2
# Step into the feature worktree cd feature/cardiac-mesh git rebase main # main's worktree doesn't need to be "current" for this — # the ref is shared from the repo's object store
# Which branch is checked out in each worktree? git worktree list # Detailed info (shows locked/prunable status) git worktree list --verbose
# Useful if a worktree is on an external drive or NFS mount git worktree lock <path> git worktree unlock <path>
With no branch argument, git worktree add <path> picks a branch named
after $(basename <path>). For feature/cardiac-mesh that
basename is cardiac-mesh, not feature/cardiac-mesh.
No such branch exists, so git helpfully creates one from HEAD — which
in a bare repo is whatever HEAD points at, usually main.
You now sit in a directory called feature/cardiac-mesh, on a branch called
cardiac-mesh, holding main's code, with none of the feature
work and no error message. Check with git worktree list, which prints the
branch actually checked out.
Always pass the branch as a second argument. The one-argument form is only safe when path basename and branch name are identical.
A worktree can be correctly attached to a branch and still have nowhere to pull from. From a bare clone this is the norm, not the exception:
git status -sb ## main # no "...origin/main" — no upstream git pull There is no tracking information for the current branch.
This is not a detached HEAD, and nothing is broken. Fix it per branch with
git branch --set-upstream-to=origin/<branch> <branch>, or check
the whole repo at once with git branch -vv — branches with no
[origin/...] marker still need wiring.
fatal: 'main' is already checked out at '/path/to/main'.
Use --detach if you need a second copy at the same commit without the branch ref.
git status says "HEAD detached at origin/...", the worktree was
likely created directly from a remote-tracking ref or tag (see section 03). Spot these
with git worktree list — they show (detached HEAD) instead
of a branch name. To reattach:
git switch --track origin/<branch> to get onto the remote branch, or
git switch -c <new-branch> to keep any commits you made while detached.
git worktree and submodules interact poorly. Submodules are not
automatically initialised in new worktrees. You must run
git submodule update --init --recursive inside each new worktree manually.
git push works normally from any worktree. But
HEAD of the bare repo itself is just a symbolic ref (usually pointing to
main). This does not affect worktrees but can confuse some tools that
inspect the bare repo directly.
.git is a small text file containing
gitdir: /path/to/bare/repo/worktrees/<name>. Do not delete or
modify it. Tools that check for a .git/ directory may need adjustment.
git stash is global to the repo. If you stash in one worktree and
pop in another, it works — but be aware that the stashed changes may not
apply cleanly to a different branch.
#!/usr/bin/env bash # wt-add.sh — create a worktree and configure its build # Usage: ./wt-add.sh <branch> [cmake-args...] BRANCH="${1:?Usage: wt-add.sh <branch> [cmake-args...]}" shift WT_PATH="${BRANCH//\//-}" # feature/foo → feature-foo git worktree add "$WT_PATH" "$BRANCH" || exit 1 cd "$WT_PATH" # Auto-configure CMake build cmake -B build -S . "$@" echo "Worktree ready: $WT_PATH (branch: $BRANCH)"
Git hooks live in the shared .git/hooks/ (or bare repo hooks/)
and fire for all worktrees. To find which worktree triggered a hook, use
$GIT_WORK_TREE or simply pwd — hooks run with the working
directory set to the worktree root. $GIT_DIR is not what you want here: in a
linked worktree it points at the admin directory
<bare>/worktrees/<name>, not at the checkout.
Note that git worktree add fires post-checkout too, so the script
below runs automatically on every new worktree.
#!/usr/bin/env bash # .git/hooks/post-checkout # Re-run cmake configure after branch switch OLD_HEAD="$1" NEW_HEAD="$2" BRANCH_CHECKOUT="$3" # 1 if branch checkout, 0 if file checkout if [ "$BRANCH_CHECKOUT" = "1" ] && [ -f "CMakeLists.txt" ]; then echo "Branch changed — reconfiguring CMake..." cmake -B build -S . fi
# Add to .bashrc / .zshrc alias wtl='git worktree list' alias wta='git worktree add' alias wtr='git worktree remove' # fzf-powered worktree switcher wt() { local target target=$(git worktree list | fzf --height=40% | awk '{print $1}') [ -n "$target" ] && cd "$target" }
| Task | Command |
|---|---|
| Add worktree (existing branch) | git worktree add <path> <branch> |
| Add worktree (new branch) | git worktree add -b <branch> <path> |
| Add worktree tracking a remote branch | git worktree add --track -b <branch> <path> origin/<branch> |
| Add detached worktree | git worktree add --detach <path> <ref> |
| List all worktrees | git worktree list [--verbose] |
| Remove worktree | git worktree remove <path> |
| Force remove (dirty) | git worktree remove --force <path> |
| Move worktree | git worktree move <old> <new> |
| Clean up stale entries | git worktree prune |
| Fix links after moving repo/worktrees | git worktree repair [<path>...] |
| Lock (prevent prune) | git worktree lock <path> |
| Unlock | git worktree unlock <path> |
| Fix bare clone fetch | git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" |
Set upstream so pull works |
git branch --set-upstream-to=origin/<branch> <branch> |