Working on more than one Git branch often creates an awkward routine:
- Stop what you are doing.
- Stash incomplete changes.
- Check out another branch.
- Investigate or fix something.
- Switch back.
- Restore the stash and hope nothing was misplaced.
This gets especially painful with stacked pull requests, urgent fixes, long-running test suites, or reviews that require comparing several versions of the same code.
Git worktrees offer a cleaner approach. They let you check out multiple branches from one repository into separate directories at the same time.
I first paid attention to worktrees after noticing AI coding tools using them to run multiple agents against the same repository in parallel, each in its own checkout, without overwriting the others’ working files, with no need to push anything to the cloud just to isolate it. That was reason enough to learn how the mechanism actually works.
I put them to real use during a stacked-branch repair: a parent branch needed fixing, and a child branch built on top of it needed rebasing afterward. I was running AI coding tools alongside my own changes, and repeatedly stashing and switching made it increasingly difficult to track which branch and working-tree state I was looking at. Moving the parent and child into their own worktrees, leaving my original checkout, including its untracked review notes, untouched, made the rebase far easier to reason about.
The mental model
A normal Git repository combines two kinds of state:
- Shared repository state: commits, branches, tags, remote references, and repository configuration by default.
- Checkout state: the current branch, working files, staged changes, and in-progress operations.
A worktree adds another checkout while continuing to use the same shared repository data.
Each worktree has its own checked-out files, index, and HEAD. Commits and branch references remain shared.
This means one directory can remain on a base branch while another contains a feature branch and a third contains its dependent child branch. There is no need to stash or repeatedly switch between them.
A simple example
Suppose a repository lives in a directory named project, and a feature branch needs to be inspected without disturbing the current checkout.
First, refresh the remote references:
git fetch origin
Then add a worktree for an existing local branch:
git worktree add ../project-feature feature-branch
The resulting directories might look like this:
workspace/
├── project/ main checkout
└── project-feature/ feature branch checkout
The ../ is what keeps it a sibling. Drop it and run git worktree add project-feature feature-branch from inside project, and Git will happily create the new worktree nested inside your current directory instead, where it then shows up as an untracked folder in project's own git status.
Both directories belong to the same Git repository, but each has independent working files.
Alternatively, to create a new local branch from a remote branch while setting up a worktree in a dedicated directory:
git worktree add \
-b repair/feature \
../project-repair \
origin/feature-branch
Now enter the new directory and work normally:
cd ../project-repair
git status
git log --oneline --decorate -10
bundle exec rake test # or npm test
Commits created there are immediately available to the other worktrees because they share the same Git object database.
Inspecting active worktrees
List the registered worktrees with:
git worktree list
Typical output looks like this:
/path/to/project 1a2b3c4 [main]
/path/to/project-feature 5d6e7f8 [feature-branch]
For scripts or detailed troubleshooting, use the machine-readable form:
git worktree list --porcelain
What is shared and what is isolated?
Understanding the boundary prevents most surprises.
| State | Shared between worktrees? |
|---|---|
| Commits and Git objects | Yes |
| Local branch references | Yes |
| Remote references | Yes |
| Tags | Yes |
| Repository configuration | Shared by default |
| Stash list | Yes |
Current branch and HEAD
|
No |
| Checked-out files | No |
| Staging index | No |
| Untracked files | No |
| In-progress rebase or merge | No |
| Build output inside the directory | No |
The shared stash list is easy to overlook. Running git stash stashes uncommitted changes from the current worktree, but the stash entry is stored in the shared Git repository state. This means git stash list, git stash apply, and git stash pop operate on the same shared stash stack from every worktree. Using descriptive messages helps avoid popping a stash into the wrong branch:
git stash push -m "feature branch before conflict resolution"
Repository configuration is shared by default. Git can also maintain worktree-specific settings when extensions.worktreeConfig is enabled and values are written using git config --worktree.
Untracked files are the other easy thing to overlook, and the one that stalls a new worktree the fastest. A worktree only contains files Git tracks; anything ignored or generated locally has to be created again. In a Rails app, that often includes config/master.key, .env, local database.yml overrides, node_modules, and any worktree-local bundle path such as vendor/bundle:
cd ../project-feature
cp ../project/config/master.key config/master.key
cp ../project/.env .env
bundle install
Symlinking instead of copying keeps credentials and environment files in sync automatically, which is useful when every worktree should run against the same local environment. A relative symlink target resolves from the symlink's own directory, not from your shell's current directory, so config/master.key and .env need different numbers of ../ to reach the same place. Use an absolute path instead to avoid that arithmetic:
PROJECT_ROOT="$(cd ../project && pwd)"
ln -s "$PROJECT_ROOT/config/master.key" config/master.key
ln -s "$PROJECT_ROOT/.env" .env
Only symlink files you want shared. A worktree deliberately testing a different .env or database configuration should keep its own copy instead.
The most important restriction
Git generally prevents the same branch from being checked out in two worktrees simultaneously.
Attempting it produces an error similar to:
fatal: 'feature-branch' is already checked out at '/path/to/project-feature'
This restriction protects the branch from being moved independently by two checkouts. When a branch is already checked out elsewhere, you have four main options: use that existing worktree, create another branch, use a detached worktree for inspection, or remove the obsolete worktree.
When another view of the same commit is needed, create a detached worktree:
git worktree add --detach ../project-inspect origin/feature-branch
Detached worktrees are useful for inspection and testing. If commits are created there, attach them to a branch before relying on them:
git switch -c investigation/result
Why worktrees are useful for stacked pull requests
Consider a stack with a base branch, a parent feature, and a child feature. The child must be rebased after correcting the parent.
Without worktrees, the process often becomes:
stash → switch to parent → repair → test → switch to child
→ rebase → resolve → test → restore previous work
With worktrees, each branch remains available in its own directory:
workspace/
├── project-base/
├── project-parent/
└── project-child/
A practical workflow is:
- Repair and test the parent in its worktree.
- Commit and push the corrected parent.
- Rebase the child in its own worktree.
- Consult the parent directory while resolving conflicts.
- Test the exact child commit intended for push.
- Repeat for the next descendant.
This is safer than repeatedly switching branches because the corrected parent remains visible throughout conflict resolution. It also reduces the chance of accidentally restoring stale code from the child, while eliminating the repeated dependency and build churn caused by switching a single directory between different branches.
A reusable stacked-branch setup
Create dedicated worktrees for a parent and child branch:
git fetch origin
git worktree add \
-b parent-branch \
../project-parent \
origin/parent-branch
git worktree add \
-b child-branch \
../project-child \
origin/child-branch
After repairing and committing the parent, rebase the child:
cd ../project-child
git rebase parent-branch
During conflicts, the parent worktree remains available as a trustworthy reference. After resolving and testing, the rewritten child branch can be pushed safely:
git push --force-with-lease origin child-branch
Parallel testing and investigation
Worktrees are also useful when no branch rewrite is involved.
They make it possible to:
- Run a long test suite on one branch while editing another.
- Compare generated output between two branches.
- Review a pull request without disturbing unfinished work.
- Keep a production hotfix separate from feature development.
- Test multiple dependency or framework upgrade branches.
- Open parent and child implementations side by side in an editor.
Each worktree has separate filesystem output, so test artifacts and build products do not overwrite those in another worktree unless they use an external shared service or cache.
Because branches live in separate directories, you can also use standard terminal diffing tools to compare implementations directly across worktrees:
# Compare specific files between parent and child worktrees
diff -u ../project-parent/src/service.rb ../project-child/src/service.rb
Databases, Redis instances, containers, ports, and global caches are not automatically isolated merely because the source directories are separate. Parallel test environments may still need distinct database names, ports, or namespaces.
Merging a worktree's branch back
Merging only picks up what's committed, so confirm the feature worktree is clean first:
cd ../project-feature
git status
Commit or stash anything outstanding. An uncommitted change sitting in the feature worktree won't be part of the merge, and it's easy to assume it was included just because the branch was.
While still in the feature worktree, rebasing onto main first keeps the merge itself trivial, often a fast-forward instead of a merge commit:
git rebase main
Resolve any conflicts here, in the feature worktree, rather than during the merge itself.
Because commits and branch references are shared across worktrees, merging one worktree's branch into another doesn't require pushing or fetching first. From the worktree that has the target branch checked out, commonly the main worktree, merge directly:
cd ../project
git switch main
git merge feature-branch
When the workflow goes through a pull request instead, push the branch, merge on GitHub, then run git pull in the worktree tracking main to bring the merge commit in locally.
Once the branch is merged, its worktree has done its job and can be removed.
Removing a worktree safely
Before removing a worktree, check for local changes:
cd ../project-feature
git status
Then move to another worktree and remove it through Git:
cd ../project
git worktree remove ../project-feature
Using git worktree remove is preferable to deleting the directory manually because Git also removes its internal registration.
Removing the worktree doesn't delete the branch it had checked out. Branches are shared repository state, not checkout state. Once a branch is merged and its worktree is gone, delete the branch itself if it's no longer needed:
git branch -d feature-branch
If a directory was deleted outside Git, clean up stale metadata with:
git worktree prune
Force removal is available, but it can discard modified and untracked files:
git worktree remove --force ../project-feature
Use it only after confirming that nothing in the worktree needs to be preserved.
Worktrees are not clones
A second clone also provides another checkout, but it duplicates repository objects and has independent local branches, tags, stashes, and configuration. Keeping those clones synchronized requires additional fetching and coordination.
A worktree is lighter and more tightly connected:
- Objects are stored once.
- New commits are immediately visible everywhere.
- Local branches and tags are shared.
- Repository configuration is shared by default.
Use separate clones when true repository-level isolation is required. Use worktrees when the goal is simply to work with multiple branches from the same repository at once.
A note on project layout: Bare repositories
While the examples in this post place worktrees in sibling directories (../project-parent), developers who use worktrees heavily often clone repositories as bare repositories instead:
mkdir project
cd project
git clone --bare git@github.com:user/project.git .bare
git --git-dir=.bare config remote.origin.fetch \
'+refs/heads/*:refs/remotes/origin/*'
git --git-dir=.bare fetch origin
git --git-dir=.bare worktree add main main
git --git-dir=.bare worktree add feature feature-branch
A bare clone places the remote's branch heads directly under local branches and does not create the ordinary origin/* remote-tracking references on its own. The explicit fetch refspec above restores that familiar layout, which the git fetch origin and origin/feature-branch references used earlier in this post depend on.
The resulting layout keeps all worktree checkouts cleanly separated under a single workspace folder:
project/
├── .bare/
├── main/
└── feature/
Unlike a normal clone, a bare repository has no default working tree of its own, acting purely as the shared repository storage for linked worktrees.
Practical safety rules
A few habits make worktrees predictable:
- Use clear directory names that identify the branch or task.
- Run
git statusbefore rebasing, resetting, or removing a worktree. - Run
git worktree listbefore deleting or renaming branches. - Remember that branch references are shared: committing, resetting, rebasing, or otherwise moving a branch in one worktree is immediately visible from the others.
- Avoid manually deleting worktree directories.
- Keep important backup branches until rewritten branches have been pushed and verified.
- Test from the worktree containing the exact commit intended for push.
- Isolate external services when running worktrees concurrently.
- Open each worktree in its own editor or IDE window so language server (LSP) indexes remain isolated.
- Remove finished worktrees so the workspace does not become cluttered.
Final takeaway
A worktree is another independent checkout of the same Git repository, allowing multiple branches to remain open simultaneously without cloning the repository multiple times.
It replaces the repeated stash-and-switch routine with a stable directory per branch. That makes reviews, stacked pull requests, difficult rebases, parallel testing, and urgent fixes easier to reason about and much less likely to disturb unrelated work.
For many multi-branch workflows, the most useful first commands are simply:
git worktree add ../project-feature feature-branch
git worktree list
git worktree remove ../project-feature
Once that mental model clicks, worktrees become one of Git's most practical tools.

Top comments (0)