Blog

Git worktrees explained

A worktree gives one repository several working directories that share a single object store, not several copies of your project. The part people miss is everything that single store still leaves shared between them.

Quick Answer

A git worktree is a second working directory attached to the same repository: one .git object store underneath, a separate checkout and branch on top. git worktree add ../feature-x -b feature-x creates one, git worktree list shows every one on the repository, and git worktree remove or git worktree prune clean them up. The hard rule: a branch can be checked out in only one worktree at a time. What a worktree does not do is isolate your environment. Untracked and gitignored files like .env are never copied into a new one, and hooks, Git config and your credentials are shared across every one of them by default.

What a git worktree actually is

A repository normally has one working directory: the files you see when you cd into it, tied to whatever branch HEAD currently points at. git worktree add gives it a second one, a separate directory on disk with its own HEAD and its own branch, backed by exactly the same .git object database. Git calls the original checkout the main worktree and every one you add after it a linked worktree.

Nothing about the commit history is duplicated. Commits, blobs and trees live once, in one object store, and every worktree reads from it. What differs per worktree is just the pointer into that history: HEAD, the index, and which branch is checked out where. A repository's refs (refs/heads/*, refs/tags/*) are shared across every worktree too, with a small set of exceptions for things like an in-progress bisect.

The bookkeeping for each linked worktree lives in .git/worktrees/<name> inside the main repository, a small directory of administrative files Git uses to track where that worktree is and what it has checked out. You never touch it directly; git worktree list and git worktree remove do.

The commands: add, list, remove, prune

Four subcommands cover almost everything you need day to day.

  • git worktree add ../path -b new-branch creates a worktree at ../path on a new branch. Drop -b to check out an existing branch instead, as long as nothing else already has it checked out.
  • git worktree list shows every worktree attached to the repository, its path, and the branch sitting in it. Add --porcelain for script-friendly output.
  • git worktree remove <path> deletes a worktree, but only a clean one. Uncommitted changes or untracked files need --force, and the main worktree can never be removed this way.
  • git worktree prune clears the administrative entry for a worktree whose directory is already gone. This matters because deleting the folder by hand, with rm -rf, does not tell Git anything; the entry just goes stale.

Why a branch can only live in one worktree at a time

Try to check out a branch that is already checked out somewhere else and Git refuses outright, naming the worktree that has it. That is not a style preference, it is what keeps HEAD meaningful. Each worktree has its own index and its own idea of what is staged, but the branch ref itself is shared history, not a per-worktree copy, so two directories cannot both be its current tip.

--force can override the refusal, and the reason to avoid it is concrete rather than theoretical. Force two worktrees onto the same branch and commit in the first one: the branch ref moves, but the second worktree's index does not know it moved, so Git reports changes there that nobody made, a working tree quietly out of sync with the branch it is still sitting on.

Detached HEAD does not have this problem. Two worktrees can both sit on the exact same commit with git worktree add -d, because neither is tracking that commit as a moving branch tip the other could pull out from under it.

Worktree, a second clone, or git stash

A second git clone gives full isolation: its own .git directory, its own object store, its own remotes to configure, nothing shared with the original. That costs disk proportional to the whole repository's history, and doubles the bookkeeping, since fetching in one clone does nothing for the other. A worktree gets the same two-branches-checked-out-at-once result for a fraction of the cost, because the object store underneath is the one thing it does share.

git stash solves an adjacent problem and is not a substitute. It does not give you a second directory at all. It shelves whatever is uncommitted in the one directory you already have, puts the working tree back to a clean HEAD, and lets you switch branches in place. Stashes are also repository-wide rather than per-worktree, since they are stored as a ref: one made in any worktree shows up in git stash list from every other worktree on that repository.

Reach for a clone when you want independence from whatever happens to the other checkout next. Reach for a worktree when you want several branches checked out at once without paying for several histories. Reach for stash when you want uncommitted work out of the way for a minute, not a second branch open side by side.

What a worktree does not isolate

A worktree isolates files that Git tracks, on the branch it is checked out to. Everything outside that stays exactly where it already was, because a worktree is still the same machine, the same user and the same repository underneath.

  • Untracked and gitignored files. A new worktree is a checkout of what Git tracks, so a .env sitting in your main checkout, gitignored and never committed, does not appear in a new worktree at all. If something needs it, it has to be put there deliberately.
  • Hooks. .git/hooks (or wherever core.hooksPath points) resolves to the directory every worktree shares, not a per-worktree copy, so a pre-commit hook installed once runs in every worktree you create, with no way to scope it to just one.
  • Git config. The repository's config file is shared across every worktree by default, so a setting changed in one is live in all of them unless extensions.worktreeConfig has been turned on for that particular setting.
  • Your credentials. SSH keys, a Git credential helper, a token sitting in your shell environment: none of that is part of the repository, so a worktree does nothing to it either way. Every worktree authenticates as you, because it is you.
  • Ports and local databases. Run a dev server or point at a local database from two worktrees at once and the same collisions show up as from two terminal tabs in one directory, because a worktree changes which files a process reads, not which process, port or database it talks to.

Cleanup pitfalls

The mistake that catches people is deleting a worktree the wrong way. rm -rf the directory and Git has no idea: the entry stays in .git/worktrees, git worktree list keeps showing the path marked prunable, and it sits there until git worktree remove notices the directory is gone and cleans up, or git worktree prune is run directly.

git worktree remove itself refuses anything unclean. Uncommitted changes or untracked files mean --force is required, and a worktree locked with git worktree lock (the right move for one on a removable drive or a network share that might not stay mounted) needs --force passed twice: once to override the dirty check, once to override the lock.

And the main worktree, the original checkout a clone or git init created, cannot be removed through this command at all. Linked worktrees only exist in relation to it; the main one goes away when the repository does.

How this works in Forkbench

This is the exact primitive two other pages on this site build directly on top of: a parallel Claude Code agents workflow and the cross-vendor walkthrough for running several agents on a Mac both start from one worktree per agent, so two of them never write the same directory. Forkbench is a desktop app for running coding agents, and its version of that first step is a toggle in the git panel: putting a session on its own worktree is one click, not a git worktree add typed by hand.

The limit is the one this page has been pointing at throughout, and Forkbench states it directly rather than leaving it to be discovered: an agent in a terminal has the same filesystem access the human running it does, so it can read a .env sitting in the project, and the only thing that keeps that file off a working branch is actually being on a worktree in the first place.

Related: A parallel Claude Code agents workflow that holds up, How to run multiple Claude Code agents in parallel on a Mac, Stop parallel agents from undoing each other's work

Frequently asked

Keep reading

Sources