git worktree cheatsheet

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.

01 Mental Model

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.

single .git repo worktree: main/ worktree: feature-x/ worktree: hotfix/
Key insight
All worktrees share commits, refs, stash, and config. Each worktree has its own HEAD, index (staging area), and working files. A branch can only be checked out in one worktree at a time.

02 Bare Clone Setup (Recommended)

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
Critical
The 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.

Non-bare alternative (simpler, less clean)

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

03 Core Commands

Create a worktree

# 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>

List worktrees

git worktree list
/home/user/myproject         (bare)
/home/user/myproject/main    abc1234 [main]
/home/user/myproject/develop def5678 [develop]

Remove a worktree

# 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

Move a worktree

git worktree move <old-path> <new-path>

04 CMake / C++ Workflow

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.

Recommended layout

myproject/ ← bare repo ├── main/ ← worktree├── build/ ← in-tree build dir├── CMakeLists.txt │ ├── src/ │ └── ... ├── feature/cardiac-mesh/ ← worktree├── build/ ← its own build dir├── CMakeLists.txt │ └── ... └── hotfix/ └── build/

Setting up a C++ worktree from scratch

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)
Why this works
Each worktree directory is a completely independent source tree. CMake's build directory sits inside it and never gets confused by branch switches. You can have main compiling with GCC while feature/cardiac-mesh is building with Clang — simultaneously, in parallel terminals.

Out-of-tree builds (alternative)

If you prefer keeping build artefacts separate from source:

myproject/ ← bare repo + worktrees ├── main/ └── feature/cardiac-mesh/ myproject-builds/ ← sibling build root ├── main/ └── feature-cardiac-mesh/
cmake -B ../myproject-builds/feature-cardiac-mesh -S .
.gitignore
If using in-tree 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.

05 Python / General Workflow

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.

Per-worktree virtual environments

cd myproject/feature-refactor
python -m venv .venv
source .venv/bin/activate
pip install -e .    # editable install for this branch
Tip
Add .venv/ to your .gitignore. Each worktree gets its own .venv with its own dependencies, so you can test incompatible package versions across branches.

Typical use cases beyond C++

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.

06 Directory Layouts Compared

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.
Naming convention
When branch names contain slashes (e.g. 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.

07 Common Operations

Fetching & pulling across worktrees

# 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

Cherry-picking between worktrees

# 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

Rebasing a feature branch

# 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

Checking what is where

# Which branch is checked out in each worktree?
git worktree list

# Detailed info (shows locked/prunable status)
git worktree list --verbose

Locking a worktree (prevent pruning)

# Useful if a worktree is on an external drive or NFS mount
git worktree lock <path>
git worktree unlock <path>

08 Gotchas & Pitfalls

One branch, one worktree
You cannot check out the same branch in two worktrees. If you try, git will refuse: 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.
Submodules
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.
Bare clone + push
In a bare repo, 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 file, not a directory
In a linked worktree, .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.
Stash is shared
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.
IDE configuration
Open each worktree as a separate project/workspace in your IDE. Do not open the bare repo root as a project — most IDEs will not know what to do with it. VSCode, CLion, and PyCharm all handle worktree directories correctly if opened individually.

09 Hooks & Automation

Auto-setup script for CMake worktrees

#!/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)"

post-checkout hook (per-worktree)

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

Shell alias for quick worktree navigation

# 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"
}

10 Quick Reference Table

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/*"