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. (Config is shared unless you opt in to extensions.worktreeConfig, which lets each worktree carry a private config.worktree file.)

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

# 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 ..
Critical — the three bare-clone traps

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.

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: 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>
Detached HEAD trap
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.
When --guess-remote actually fires
--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.

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 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
Path and branch are two separate arguments, and slashes behave differently in each. A path of 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.

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).
# Needs an upstream (see 02) and a non-detached HEAD (see 08)
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

The $(basename) trap — silently the wrong branch

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.

Attached, but no upstream

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.

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.
Detached HEAD rescue
If 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.
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 <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)"

post-checkout hook (per-worktree)

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

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