High Performance Git

Section II ยท History, Rewrite, and Parallel Work

Chapter 6

Worktrees as a Performance Tool

Pencil sketch of a town green with a gazebo, houses, a church steeple, and two people approaching along a path.

A worktree is an additional checkout attached to a repository. Instead of repeatedly switching one checkout, stashing files, or cloning the repository again, you keep a few working trees around and let each one hold onto its own state.

It is one of Git's best workflow tools when you are juggling a feature branch, a hotfix, a benchmark, a docs edit, or an agent run at the same time. It has been underused for years, but now that people are running multiple agent loops, it is much more interesting.


Another Checkout, Attached to One Repository

Linked Worktree
Additional working tree attached to the same repository, sharing the common object store while keeping its own checkout state.

A linked worktree is another checkout attached to the same repository, not another independent repository with its own object database, packfiles, and commit-graph.

That means multiple worktrees share the common object store and most repository-level data, while each worktree still has its own:

That setup lets one worktree sit on main, another hold a release branch, and a third stay in detached HEAD for a benchmark or bisect. They are separate checkouts over one shared repository.

On first use, worktrees mostly register as convenience: no stash dance, no branch switching, no duplicate clone. The bigger gain appears later.

Stable paths mean long-running local state stays attached to the task that created it. Editors, language servers, test runners, benchmark scripts, container mounts, and shell history all behave more predictably when ../repo-hotfix keeps being the hotfix checkout instead of whatever branch happened to be switched into one shared directory today. A stable path is boring in the best possible way.

Separate indexes matter just as much. Each worktree has its own index, so staging and checkout state do not collide across tasks. That is cleaner for humans, and it also avoids a surprising amount of mechanical friction in automation-heavy workflows: fewer accidental staging mistakes, less index rewrite churn, and less contention around one mutable checkout trying to serve several workers at once.

Isolated build state is the third piece. Node modules, Python virtual environments, generated files, caches, and benchmark outputs all tend to stick to the worktree that produced them. That keeps a docs task from inheriting a release build's byproducts and keeps a benchmark run from sharing a half-mutated tree with an unrelated fix.

Give each task its own:

That keeps the unit of work aligned. The branch you are reviewing does not need to share an index with a benchmark run you started an hour earlier. A docs change does not need to share a checkout with a test fix. If you are using local agents, this is usually the cleanest arrangement there too.

Git's Branch Safety Rule Helps

One of Git's most useful safeguards is that a branch generally should only be checked out in one worktree at a time. Git refuses the overlapping checkout by default unless you force it.

That rule can look annoying until you remember what a branch is: a movable ref. If two active workers both treat that branch as their live checkout, it becomes easy for one to move the ref while the other still assumes an older position.

So the safety rule is doing something valuable. It nudges you toward the cleaner pattern:

That discipline scales better than a shared mutable checkout. Shared mutable checkouts are how little annoyances turn into folklore.

Worktrees Feel Faster Because They Preserve Expensive Local State

Without worktrees, an interruption often turns into one of these patterns:

Or, if the second task needs to stay around:

Worktrees avoid both patterns. They let you pay the checkout cost once, keep that context around, and move to another context without tearing the first one down.

That matters more as repositories get larger. Rewriting a large working tree is real work. Rebuilding one large index over and over is real work. Recloning a repository with a large history or large packfiles is definitely real work. Restarting a long-running test or losing a carefully prepared build cache because one shared checkout had to switch branches is also real work. None of this is glamorous. It is just the tax you pay for pretending one checkout can be five things at once. Worktrees turn that repeated setup cost into a persistent local asset.

Be precise about what is shared and what is separate.

Broadly shared across worktrees:

Separate per worktree:

That division is the point. If you run five local tasks against the same repository, you usually want five separate HEADs and five separate indexes. You usually do not want five copies of the packfiles and five redundant maintenance schedules.

--no-checkout and Worktree-Specific Config Matter More Than They Used To

git worktree add --no-checkout is useful when you want to customize a new worktree before the checkout, including sparse-checkout setup.

A practical pattern is:

  1. create a worktree for the task
  2. choose or create the task branch
  3. apply a sparse-checkout pattern if the task only needs part of the repository
  4. run the human or agent workflow inside that prepared context

Not every task needs the same local checkout. A test fix may need one directory. A docs change may need another. A release-prep worktree may need a broader slice. Worktrees plus sparse-checkout let you shape each context to the job instead of forcing one dense checkout to serve everything.

Another important operational detail is that you can enable extensions.worktreeConfig and then use git config --worktree for settings that should stay local to one worktree.

To enable this, first set the extension in the shared repository config:

git config extensions.worktreeConfig true

Then, inside a specific worktree, use --worktree to write settings that apply only there:

git config --worktree core.sparseCheckout true
git config --worktree core.sparseCheckoutCone true
git config --worktree core.fsmonitor false

These settings live in a per-worktree config.worktree file (.git/config.worktree for the main worktree, .git/worktrees/<name>/config.worktree for linked worktrees; git rev-parse --git-path config.worktree prints the exact path). Each worktree can then have its own sparse-checkout cone, its own fsmonitor behavior, or its own experimental flags without affecting other worktrees.

Settings like core.sparseCheckout generally should not be shared unless you really mean to use sparse-checkout across all worktrees. That detail becomes important when each worktree has a different job.

Different worktrees may reasonably want different:

Once you start thinking in terms of several active contexts, config.worktree looks less like an obscure corner of Git and more like necessary hygiene.

Lifecycle Commands Matter More Under Task Churn

The basic add flow gets most of the attention, but the rest of the lifecycle matters if you want a multi-worktree setup to stay healthy over time.

A short lifecycle pass looks like:

git worktree add -b fix ../repo-fix main
git worktree list --porcelain
git worktree remove ../repo-fix
git worktree prune

That creates a task checkout, shows the attached worktrees in machine-readable form, removes the checkout, and cleans stale administrative records. It is a small loop, but it becomes normal fast when worktrees are cheap and disposable.

This was always useful. It becomes more important when worktrees are more disposable and more numerous. Some worktrees stay around for weeks; others exist only for a short benchmark, a reproduction, or an automated task. That makes cleanup and visibility part of normal operation rather than occasional housekeeping.

The older human use cases did not go away, either. Worktrees are still excellent for hotfixes without disturbing an ongoing refactor, release branches that need to stay checked out for days, bisect and benchmark sessions, and side-by-side review. The newer parallel-work story did not replace those cases. It broadened them.

Why Worktrees Usually Beat Duplicate Clones

A second clone duplicates repository administration, object transfer, local maintenance, and storage. A second worktree reuses the repository core and adds another checkout context.

Worktrees are usually the better answer when the problem is:

"I need another active context for the same repository."

A true second clone still has its place. Sometimes you really do want a fully separate repository boundary. But for most local parallel work inside the same repository, worktrees are the more economical and more legible choice.

The difference gets larger as repository size grows and as the number of active workers increases. Shared objects, shared packfiles, and shared metadata mean less duplicated transfer, less duplicated disk use, and less duplicated maintenance. Separate checkouts mean less switching churn and less accidental context mixing.

The Better Mental Model

Without worktrees, the implicit unit of local work is often:

With worktrees, the unit becomes:

That is already a better fit for how many engineers work. It is also a better fit for repositories that support background automation or local agents, where several parallel tasks may be active at once and each one benefits from an isolated checkout state.

I would teach worktrees earlier than I would have a few years ago. They are a workflow feature, but they are also a way to avoid duplicated local work. When you need another active context for the same repository, they are usually the right first answer.