Git Worktree: One Repo, Every Branch, No Stashing
You are halfway through a refactor. Nothing compiles. Someone reports production is broken and needs a one-line fix on main.
So you git stash, switch branch, fix, switch back, git stash pop, and spend the next ten minutes working out which of your changes came back and which were already there. Or you keep a second clone of the repo, and now you maintain two sets of hooks, two .env files, two IDE indexes, and 4 GB of duplicated history.
git worktree solves this properly: several branches checked out at once, in separate directories, sharing one object database. It shipped in Git 2.5 in 2015 and a lot of people still have not touched it.
I picked up the layout below from Git Worktree Like a Boss by Metal3d, then spent an evening finding the parts that bite. Three parts as usual: how we got here, how it actually works, then a recipe that I have actually run.
Part 1 — How We Got Here
The switching problem
Git’s default model gives you one working directory per clone. HEAD points at one branch, and the files on disk are that branch. Switching means rewriting those files in place.
That is fine until your working directory has state you cannot cheaply reproduce: uncommitted work, a running dev server, a warm build cache, node_modules for a different dependency set, an IDE that will reindex the moment the files change underneath it.
git stash was the historical answer. It is a stack of diffs, and it works, but it is a stack — anonymous entries, no branch association, and a pop that can conflict. Stashes are also easy to lose track of; most long-lived repos have a stash@{7} that nobody dares drop.
A second clone was the other answer, and it is the one most teams actually use. It works too, but each clone is an island: its own object database, its own hooks, its own config, its own remotes. Nothing you fix in one shows up in the other, and a large repo costs you its full history every time.
What worktree changed
Git 2.5 added git worktree, which splits a concept that had always been welded together: the repository (objects, refs, config) and the working tree (files on disk at some commit).
One repository, many working trees. Each has its own directory, its own HEAD, its own index. All of them share one object store, one set of remotes, one config, one stash list, one set of hooks.
Git also adds a safety property you do not get from multiple clones: the same branch cannot be checked out in two worktrees at once. It refuses. Two clones will happily let you commit to main in both and then work out the mess at push time.
Part 2 — How It Works
Shared history, separate files
Why a bare clone, and what the .git file does
You can use worktrees on an ordinary clone. git worktree add ../hotfix works today in any repo you have. But then one branch is privileged: it lives in the original directory, with .git inside it, and the others hang off it as siblings. Delete or move that directory and you have broken the others.
The bare layout removes the privilege. The repository lives in .bare/, and every branch is an equal sibling directory next to it.
The one-line trick that makes it ergonomic:
echo "gitdir: ./.bare" > .git
A .git file — rather than a directory — containing a gitdir: pointer is a documented Git mechanism (it is how submodules and worktrees themselves work internally). It means git commands run from the root directory find the repository, so git fetch and git worktree list work from the top rather than only inside a worktree.
Part 3 — The Recipe
Everything below was run on Git 2.50.1 against a scratch repository. Where the widely-shared version of this setup falls short, I have said so.
Yes — make the directory first
The commonly posted recipe starts with git clone --bare … .bare, which quietly assumes you are already inside a directory dedicated to this one repository. If you run it in ~/projects, you get ~/projects/.bare, ~/projects/.git, and your worktrees as siblings of every unrelated project you own.
That is not just untidy. It changes what git thinks your other directories are:
$ cd ~/projects/some-other-project
$ git rev-parse --git-dir
/Users/you/projects/.bare
$ git status
fatal: this operation must be run in a work tree
some-other-project is not a git repository, so git walked up the tree, found ~/projects/.git, and resolved it to the wrong repository. Every git command you run in any non-repo folder under ~/projects now points at that bare repo.
So: create and enter the folder first. One mkdir that most write-ups omit.
The setup
mkdir myapp && cd myapp
git init --bare .bare
echo "gitdir: ./.bare" > .git
git remote add origin git@github.com:user/repo.git
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git config worktree.guessRemote true
git fetch origin
git worktree add main
That is deliberately git init --bare plus a remote, not git clone --bare. The reason is the next section.
A script that does it for you
Seven commands is few enough to type, but not few enough to type correctly at 2am. I put the whole thing in a script — it works out the directory name from the URL, detects the remote’s actual default branch instead of assuming main, and refuses to touch a directory that already has anything in it.
Download git-worktree-init.sh, or straight from the shell:
curl -fsSL https://vzav.eu/files/git-worktree-init.sh -o git-worktree-init.sh
chmod +x git-worktree-init.sh
Using it:
$ ./git-worktree-init.sh git@github.com:user/repo.git
==> creating bare repository in .bare
==> configuring origin
==> fetching
==> adding worktree for 'main'
==> ready — /Users/you/repo
/Users/you/repo/.bare (bare)
/Users/you/repo/main 09356e2 [main]
cd repo/main
The directory name comes from the URL, so git@github.com:user/repo.git gives you repo/. Pass a second argument to override it:
./git-worktree-init.sh https://github.com/cli/cli.git ~/code/gh-cli
Three things it does that are easy to forget by hand: it reads the remote’s default branch from origin/HEAD rather than assuming main (verified against a master repo), it converts slashes in that branch name into dashes for the directory, and if the fetch fails it removes the half-built directory instead of leaving a broken layout behind.
The rest of this section is what the script is protecting you from.
Four things the short recipe leaves out
1. A bare clone gives you local branches with no upstream.
git clone --bare copies the remote’s refs/heads/* into your local refs/heads/*. You end up with local branches that look right and are not wired to anything:
$ git worktree add main
Preparing worktree (checking out 'main')
$ cd main && git status -sb
## main
$ git pull
There is no tracking information for the current branch.
git init --bare plus an explicit remote add never creates those local heads, so the first git worktree add main creates the branch fresh and tracks origin/main properly:
$ git worktree add main
Preparing worktree (new branch 'main')
branch 'main' set up to track 'origin/main'.
$ cd main && git status -sb
## main...origin/main
2. Setting the fetch refspec does nothing until you fetch.
The remote.origin.fetch line is what fixes the “blind clone” problem — a bare repository has no refspec mapping remote branches into refs/remotes/origin/*, so you cannot see what exists on the server. But setting config does not populate anything:
$ git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
$ git branch -r
# nothing
$ git fetch origin
$ git branch -r
origin/HEAD -> origin/main
origin/feature/login
origin/main
origin/release/1.0
3. worktree.guessRemote is off by default.
Without it, git worktree add somebranch for a branch that only exists on the remote does not do what you want. It does this:
$ git worktree add main
No possible source branch, inferring '--orphan'
Preparing worktree (new branch 'main')
An orphan branch with no history. git config worktree.guessRemote true makes git worktree add <name> find origin/<name> and set up tracking. Without it you have to be explicit every time:
git worktree add --track -b main main origin/main
4. Slashes in branch names are read as directory paths.
This is the one that will catch you, because it fails silently and looks like it worked:
$ git worktree add release/1.0
Preparing worktree (new branch '1.0')
You asked for release/1.0 and got a branch called 1.0, in a nested directory release/1.0/, tracking nothing. Git took the argument as a path and named the branch after its basename.
Name the branch explicitly and flatten the directory:
$ git worktree add --track -b release/1.0 release-1.0 origin/release/1.0
Preparing worktree (new branch 'release/1.0')
branch 'release/1.0' set up to track 'origin/release/1.0'.
$ cd release-1.0 && git status -sb
## release/1.0...origin/release/1.0
Day-to-day
# start a new branch off main, in its own directory
git worktree add -b feature/rate-limit feature-rate-limit origin/main
# check out an existing remote branch to review it
git worktree add review origin/feature/login
# what do I have open?
git worktree list
# done — remove the directory and deregister it
git worktree remove feature-rate-limit
# clean up entries whose directories you deleted by hand
git worktree prune
git worktree list is the one to remember:
$ git worktree list
/Users/you/myapp/.bare (bare)
/Users/you/myapp/main 09356e2 [main]
/Users/you/myapp/release-1.0 5285487 [release/1.0]
Two habits worth forming. Use git worktree remove rather than rm -rf — it deregisters the worktree as well as deleting the files; rm -rf leaves a stale entry until you prune. And fetch from anywhere: there is one object database, so a fetch in any worktree updates the refs every other worktree sees.
Where it earns its keep
- Reviewing a colleague’s branch while your own work stays untouched. No stash, no rebuild, no IDE reindex. This is the case that converts people.
- A hotfix during a long refactor. The exact scenario at the top of this post, without touching the refactor’s state.
- Comparing two branches with real tooling — a diff tool, a test run, two dev servers on different ports — rather than flipping back and forth.
- Long builds. If a clean build costs you fifteen minutes, keeping build caches per branch pays for the setup almost immediately.
- Release maintenance. A permanent
release-1.0/worktree alongsidemain/for backporting.
Where it does not
- A repo you touch once a month. The setup cost is small but not zero, and this layout is unusual enough that you will re-learn it each time.
- Tooling that assumes
.gitis a directory. Most things handle thegitdir:file fine — it is a standard mechanism — but occasionally something homegrown will not, and the failure is confusing. - Anything hardcoding absolute paths. Config files, IDE settings and
.envfiles with absolute paths need attention per worktree, since each is a real directory. Files that are gitignored do not come along either — a new worktree starts without your.env. - Disk-constrained machines with huge working trees. Worktrees share history but not checked-out files. Five worktrees of a large repo is five copies of the files, even though it is one copy of the history.
Closing
The idea is older than most people’s git habits: the repository and the working tree were always separable, and Git 2.5 exposed that. The bare-clone layout takes it one step further and stops treating any single branch as the real one.
The setup is seven commands, and the one people leave out is the first — mkdir. Then worktree.guessRemote, an actual fetch, and remembering that a slash in a branch name means something different to git worktree add than it does to git checkout.
Credit where due: the layout comes from Metal3d’s write-up. The corrections above are what I hit running it.
