Work on multiple branches at once with git worktrees
You're working on a new feature, a colleague's pull request needs a look, you git stash your work, switch branch, build, review, switch back, and git stash pop to get back to where you were. There is a better way, and it's called git worktrees.
$ git worktree add -b stock-fix ../sundae-stock-fix main $ cd ../sundae-stock-fix && zed .
A brand-new branch in its own folder, opened in your editor of choice (I use Zed; but you can swap zed . with whatever command your IDE uses). Your previous working branch is left exactly as it was without stashing changes or switching branches.
A worktree is a separate working copy of your repo, checked out to a different branch, all sharing the one repository git init set up. This means we get another folder (or several) we can edit, build, and run in, without them disturbing each other.
Making one
Try this from inside your repo and we'll walk through what's happening:
$ git worktree add -b stock-fix ../sundae-stock-fix main Preparing worktree (new branch 'stock-fix') HEAD is now at 9c15fdf short/seo title
That makes a new branch, stock-fix, off main and checks it out into a sibling folder, ../sundae-stock-fix. Now we just cd into it and we're on stock-fix, fresh off main.
The path can go anywhere, but a sibling of the repo is the usual choice so the branches/folders sit together. Name the folder the same way we'd name a branch. These commands will cover most use cases:
git worktree add -b <new-branch> <path> maincuts a new branch offmain(what we just did) and checks it out, in one go.git worktree add <path> <branch>checks out a branch that already exists (a colleague's PR, say).git worktree add <path>with no branch gives us a new branch named after the last part of the path (add ../hotfixmakes ahotfixbranch).
Git won't let us check out the same branch in two worktrees at once: try and we'll get fatal: 'stock-fix' is already checked out. This saves on conflicts and other messy behaviour.
What's shared, what isn't
The original clone (the "main" worktree) keeps the core .git directory; each new worktree gets a tiny .git file pointing back into it. Branches, commits, and config are all shared, so there's no second copy of your history sat on disk.
What isn't shared is everything git doesn't track, like your node_modules, .env, or your build output. A fresh worktree has the source and nothing else (but we have a solution to this below).
Listing and tidying up
A few commands keep the set of worktrees in order:
$ git worktree list /Users/you/sundae 9c15fdf [main] /Users/you/sundae-stock-fix 1a2b3c4 [stock-fix] $ git worktree remove ../sundae-stock-fix # bin the folder, keep the branch $ git worktree prune # tidy up after a folder you deleted by hand
remove deletes the worktree's folder but leaves the branch alone, so the work is still on stock-fix whenever we want it. prune clears the bookkeeping for the times we rm -rf a worktree folder in a hurry (as one does) instead of removing it properly.
Now we're going to explore a couple of ways of working with worktrees.
Workflow 1: Keep a few around
The first way we can use worktrees is to keep a handful standing. One folder per long-lived branch: main in one, our current feature in another, maybe a release branch in a third. Each with its own dev server/port/terminal/db. The idea being that we switch folders instead of branches.
This solves the pain of a colleague's PR needing a look whilst we're mid-feature. It also makes handling hotfixes really easy. However, with each branch running its own resources, it comes at a cost of potentially making your machine slow and sluggish.
This workflow works well with screen. One session, one window per worktree with each named for the task (feature, pr-review, hotfix).
Workflow 2: Spin one up, throw it away
The second way is to keep them ephemeral (and how I mainly use them). Some smaller tickets on the side whilst I focus on my main task. We can deal with these without too much cognitive overhead, as we can dip into them when we want. We spin up a worktree, do the one thing, and then bin it. It's a few commands each way by hand, but we'll get both down to a single command by the end.
We mentioned earlier about an issue of a fresh worktree having no node_modules. Ideally we don't want to npm install on each small task... so let's not. Let's borrow instead!
Our main checkout already has a working node_modules, so we can point the new worktree straight at it with a symlink:
$ git fetch origin $ git worktree add -b quick-fix ../sundae-quick-fix origin/main $ ln -s ../sundae/node_modules ../sundae-quick-fix/node_modules $ cd ../sundae-quick-fix # no install, ready to run
Note the base this time: origin/main, not the plain main we used earlier, with a git fetch first. Once we start living in worktrees we stop running git checkout main && git pull, because we're always off on a branch somewhere, so our local main could be stale. A throwaway we want built on the latest code should branch off the freshly fetched origin/main, not a rotting local copy.
The same trick covers anything else we want to share that's ignored by git: a gitignored .env, local certs, a build cache. Link or copy those in and the worktree behaves like our main one.
The symlink is borrowing main's dependencies, so it only works while the dependencies match. If our throwaway branch touches package.json (installs something, bumps a version), that borrowed node_modules is now wrong. If we need to install something on a throwaway, we can just drop the symlink and run npm install in the worktree: it's still gitignored, so no issues. This works best for changes that don't touch dependencies.
When the ticket's done, we bin the worktree by hand:
$ cd ../sundae # step out first (git won't remove the worktree we're in) $ git worktree remove --force ../sundae-quick-fix # bin the folder, keep the branch
remove --force because the symlink makes git read the worktree as "dirty"; the branch and any commits we pushed survive, only the folder goes. The side task is gone from our disk and out of our heads, and our main checkout is sat exactly where we left it.
Automate worktree creation and destruction
If we want to be really efficient, we can make a shell function to quickly spin up our throwaway worktrees:
# spin up an install-free worktree off origin/main and move into it
wt() {
local branch="$1"
local main; main="$(git rev-parse --show-toplevel)"
local dir="$main/../$(basename "$main")-$branch"
git -C "$main" fetch origin
git -C "$main" worktree add -b "$branch" "$dir" origin/main
ln -s "$main/node_modules" "$dir/node_modules"
cd "$dir"
}What each of those eight lines does
wt() { ... }defines a shell function calledwt. Runwt quick-fixand the lines inside run, withquick-fixhanded in as the first argument.local branch="$1"grabs that first argument (quick-fix) and stores it in a variable.$1is shell shorthand for "the first thing passed in", andlocalkeeps the variable inside the function so it doesn't leak out into our shell afterwards.local main; main="$(git rev-parse --show-toplevel)"records where the repo lives.git rev-parse --show-toplevelprints the absolute path to the root of the repo we're currently in, and the$(...)around it captures that printed path into themainvariable.local dir="$main/../$(basename "$main")-$branch"works out where to put the new worktree: a sibling of the repo.basename "$main"is just the repo's folder name (sundae), the/..hops up to its parent folder, so the whole thing resolves tosundae-quick-fixsitting right next tosundae.git -C "$main" fetch originupdates our remote-tracking branches so the next line can branch off the latestorigin/main. The-C "$main"tells git to run as if we'd started it in the repo root, sowtworks no matter which subfolder we call it from.git -C "$main" worktree add -b "$branch" "$dir" origin/mainis the line that makes a new branch offorigin/mainand checks it out at$dir.ln -s "$main/node_modules" "$dir/node_modules"handles the symlink, borrowing the main checkout's dependencies into the new worktree.cd "$dir"moves us into the new worktree, ready to code.
Why is local main; main="$(...)" split across two statements, rather than the tidier local main="$(...)"? Because written as one, the exit status we get back is local's, not the command's, so if git rev-parse failed (we ran wt outside a repo, say) the line would still report success and main would silently be empty. Splitting them keeps git's own exit status intact. It's a well-worn shell footgun, and harmless here, but the habit is worth keeping. (The same instinct is why the folder is dir and not path: in zsh, lowercase path is the array tied to $PATH, so a local path=... would quietly replace the command search path for the rest of the function, and the next git would vanish with "command not found".)
Now wt quick-fix drops us into a ready-to-run checkout, on a new branch off the latest origin/main, no install, nothing disturbed where we came from.
And the teardown we ran by hand a moment ago wraps up just the same. The one twist: since git won't remove the worktree we're standing in, unwt hops back to the main checkout for us first. The shared .git lives there, so the parent of git rev-parse --git-common-dir is where it lands:
# from inside a throwaway: hop back to the main checkout and bin it
unwt() {
local here; here="$(git rev-parse --show-toplevel)"
cd "$(dirname "$(git rev-parse --git-common-dir)")"
git worktree remove --force "$here"
}You're left with two shortcuts, wt quick-fix to set you up, and unwt to tear it down.
Summary
We're talked through two ways add worktrees to your workflow. Long-lived and ephemeral, with wt and unwt helpers to quickly work with throwaway worktrees.
One point I haven't mentioned yet, but which you can read more about in another note, is that throwaway worktrees (isolated and install-free) work incredibly well with AI agents, where you can hand this pattern to a background agent, and spin up a side task without breaking our own focus.
Cheatsheet
git worktree add <path> <branch>checks out an existing branch in a new foldergit worktree add -b <new> <path> origin/mainnew branch off main, checked out theregit worktree add <path>new branch named after the path's last partgit worktree listevery worktree, its folder, and its branchgit worktree remove <path>bins the folder, keeps the branch (--forceif it has symlinks or untracked files)git worktree prunetidy the records after deleting a worktree folder by handln -s <main>/node_modules <worktree>/node_modulesborrow dependencies instead of reinstalling (only while they matchpackage.json)