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.
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 # Now add worktrees for the branches you need git worktree add main git worktree add develop git worktree add feature/cardiac-mesh
git config remote.origin.fetch line is essential after a bare clone. Without
it, git fetch will not update remote tracking branches, and you will not be able
to create worktrees from remote branches. This is the single most common bare-clone pitfall.
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 (auto-tracks) git worktree add <path> origin/feature-x # Detached HEAD worktree (useful for inspecting a tag/commit) git worktree add --detach <path> <commit-ish>
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 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), git will
create nested directories for the worktree path if you pass the branch name directly.
This is usually what you want with the "bare + nested" layout. If not, provide an
explicit flat path: git worktree add cardiac-mesh feature/cardiac-mesh.
# Fetch is repo-wide — run it from anywhere git fetch --all # But pull is per-worktree (it merges into the current HEAD) 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>
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 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[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. You can use the $GIT_DIR environment variable
inside hooks to determine which worktree triggered them.
#!/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 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 |
| 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/*" |