Managing Branches With Git Worktrees

Mon, Sep 7, 2026 4-minute read

I’ve been using git worktrees to work on multiple branches of the same repo side by side, without the usual git stash shuffle. The setup I use comes from this excellent article, which uses a bare clone as the single source of truth, with each branch checked out into its own folder next to it:

project/
├── .bare/          # bare clone — the actual git history
├── .git             # one-line file pointing to .bare
├── main/            # worktree for the main branch
└── feature/
    └── some-branch/ # worktree for feature/some-branch

Bootstrapping a new repo

For a fresh repo, I use a small wtree script that automates the setup:

#!/bin/bash
# wtree - bootstrap a git-worktree "pro setup" for a repository.
#
# Usage: wtree <git-repo-url>
#
# Run this in an empty directory. It will:
#   1. Bare-clone the repo into a hidden .bare folder
#   2. Point the root folder's .git file at .bare
#   3. Configure remote tracking so `git fetch` sees all remote branches
#   4. Fetch everything
#
# After that, create worktrees with: git worktree add <branch>

set -e

REPO_URL=$1
SCRIPT_NAME=$(basename "$0")

if [ -z "$REPO_URL" ]; then
    echo "Error: Missing repository URL."
    echo "Usage: $SCRIPT_NAME <repo-url>"
    exit 1
fi

FILES_IN_DIR=$(ls -A | grep -v "^${SCRIPT_NAME}$" || true)

if [ -n "$FILES_IN_DIR" ]; then
    echo "Error: Current directory is not empty!"
    echo "To keep your worktree setup clean, please run this in a fresh folder."
    exit 1
fi

echo "Initializing git worktree setup..."

# Clone as bare into a hidden directory
git clone --bare "$REPO_URL" .bare

# Link the root to the bare repository
printf 'gitdir: ./.bare' > .git

# Enable remote tracking (the "blind clone" fix)
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"

# Fetch everything
git fetch --all

echo "Setup complete!"
echo "Next step: git worktree add main"

Usage:

mkdir -p ~/Projects/my-repo && cd ~/Projects/my-repo
wtree [email protected]:org/my-repo.git
git worktree add main

This post is about what happens after that initial setup — the day-to-day of adding, updating, and removing branches.

Adding a new worktree

The general form:

git worktree add <path> [-b <new-branch>] [<start-point>]

Check out a branch that already exists (locally or on the remote) into its own folder:

git worktree add feature/better-depends-on

Create a brand new branch, based on another branch:

git worktree add improve-db -b feature/fix-db feature/improve-db

Slashes in the branch name automatically create nested folders, so feature/my-branch becomes feature/my-branch/ on disk.

Removing a worktree

Don’t just rm -rf the folder — git still thinks the worktree exists and will refuse to reuse that path later (it shows up as “prunable” in git worktree list). Use:

git worktree remove <path>

If you did delete the folder manually, clean up the stale reference with:

git worktree prune

Other useful commands:

git worktree list          # see every worktree and which branch it has checked out
git worktree lock <path>    # protect a worktree from being pruned
git worktree unlock <path>

Keeping a branch up to date (the “I cloned this months ago” problem)

This is the part that trips people up. Say you set up the worktree repo a while back, and today you need to open a PR against the current state of main. Since all worktrees share one .git database, you only need to fetch once, from any worktree:

git fetch --all

Gotcha: a bare clone by default only tracks the remote’s default branch. If git fetch looks like it’s doing nothing even though new branches/commits exist upstream, fix the fetch refspec once:

git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --all

Once fetched, every worktree instantly sees the new remote refs. Now, to start a fresh PR branch from the latest main:

git worktree add feature/my-new-thing -b feature/my-new-thing origin/main

This creates a brand-new local branch feature/my-new-thing, based on the current origin/main — not whatever main looked like when you first cloned.

If you already have a long-lived local branch (e.g. an existing main worktree) that’s fallen behind, update it in place:

cd main
git fetch --all
git pull --ff-only    # or: git merge origin/main / git rebase origin/main

--ff-only is a good default here since it fails loudly instead of creating an unexpected merge commit if your local main has diverged.

Summary

  • One git fetch --all refreshes every worktree at once — no need to fetch per-branch.
  • Branch off origin/<branch> (not your possibly-stale local branch) when starting new work to guarantee you’re building on the latest changes.
  • Use git worktree remove, not rm -rf, to avoid the “prunable but not removable” trap.