A linked worktree gives one Git repository another working directory, with its own checked-out state. That lets a feature, release, or review stay open alongside other work without repeatedly replacing the same files. The useful boundary is separate working files and staging, not complete isolation. Objects, many references, and ordinary repository configuration are shared, so operations need to respect both the local workspace and the common repository.
Identify the state that belongs to each checkout
Each worktree has its own HEAD and index. A staged change in one directory does not appear in the other’s index, and editing a tracked file does not edit its counterpart next door. Committing in a linked worktree normally advances its selected branch. That new object and shared branch reference are immediately available to Git commands run from the other worktrees, without a fetch between them.
The linked directory’s .git entry is normally a file pointing to its administrative directory. The checked-out source files live at the path chosen for the worktree, not inside the administrative .git/worktrees entry. Tools that assume .git must be a directory can fail here. Ask Git for --git-common-dir, --absolute-git-dir, or --git-path index instead of constructing internal paths yourself.
Objects are shared, but working files and build outputs still consume space separately. A second checkout is not another complete object database, yet it is not a zero-copy view of every source and generated file. Estimate disk use from the actual checkout and build requirements, especially when several release versions remain open.
Shared does not mean every item is shared
Local branches and tags generally belong to the common repository. HEAD and operation-specific state belong to a worktree; some references, including those under refs/bisect, are also per-worktree. Treating every reference as either globally shared or entirely local produces incorrect tooling. Use Git commands to resolve the relevant reference in the intended worktree.
Repository configuration is shared by default. A git config --local change made while standing in the review directory can therefore affect the feature directory too. Git supports explicitly enabled worktree-specific configuration, but enabling that extension is a repository configuration change requiring its documented migration rules. Do not silently assume ordinary local configuration already means local to this checkout.
Likewise, a shared stash list is not a private scratch area for one worktree. Name and inspect any saved work before applying or dropping it. Separate source directories also do not automatically isolate network ports, databases, external caches, or credentials used by commands launched from them.
When it breaks
By default, Git refuses to check out the same local branch in two worktrees. This protects separate indexes and files from competing updates to one moving reference. It is a safeguard that force options can override, not an unbreakable storage law. Prefer a distinct branch for independent changes or a detached checkout for inspecting the same commit, instead of bypassing the guard as routine setup.
A detached checkout has no local branch advancing with new commits. If its experimental work must survive removal, give the desired commit a branch name first. Removing a worktree does not automatically delete its branch, and pruning stale administration is not the same as deleting a live checkout. Use the operation that matches the actual state.
For moves, prefer git worktree move where supported; use repair to reconnect a manually moved checkout. A temporarily unavailable removable volume is not necessarily abandoned. Locking that worktree can prevent its registration from being pruned while it is offline. Avoid manually editing administrative files as a general recovery recipe.
Check it quickly
Run this with Python 3 and Git 2.28 or newer. The temporary repository excludes inherited Git routing and personal configuration, uses example identity, and disables hooks and signing. The only configuration written is a synthetic demonstration key. Two tracked versions are staged independently, then a linked commit becomes visible from the main directory.
import os
import subprocess
import tempfile
from pathlib import Path
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull)
with tempfile.TemporaryDirectory(prefix="git-worktree-state-") as directory:
root = Path(directory)
main, review, detached = root / "main", root / "review", root / "detached"
main.mkdir()
def git(tree, *args, check=True):
return subprocess.run(
["git", "-c", "user.name=Worktree Example",
"-c", "user.email=worktree@example.invalid",
"-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", *args],
cwd=tree, env=env, text=True, capture_output=True, check=check,
)
git(main, "init", "-q", "-b", "main")
(main / "note.txt").write_text("base\n")
git(main, "add", "note.txt")
git(main, "commit", "-qm", "base")
git(main, "worktree", "add", "-qb", "review", str(review), "HEAD")
common = [(tree / git(tree, "rev-parse", "--git-common-dir").stdout.strip()).resolve()
for tree in (main, review)]
indexes = [(tree / git(tree, "rev-parse", "--git-path", "index").stdout.strip()).resolve()
for tree in (main, review)]
print("shared-common-dir:", common[0] == common[1])
print("separate-indexes:", indexes[0] != indexes[1])
(main / "note.txt").write_text("main staged\n")
git(main, "add", "note.txt")
(review / "note.txt").write_text("review staged\n")
git(review, "add", "note.txt")
print("independent-staging:", git(main, "show", ":note.txt").stdout == "main staged\n"
and git(review, "show", ":note.txt").stdout == "review staged\n")
git(main, "config", "--local", "demo.shared", "visible")
print("shared-local-config:", git(review, "config", "--get", "demo.shared").stdout.strip() == "visible")
git(review, "commit", "-qm", "review change")
print("commit-visible-from-main:", git(main, "show", "review:note.txt").stdout == "review staged\n")
print("main-staging-unchanged:", git(main, "show", ":note.txt").stdout == "main staged\n"
and (main / "note.txt").read_text() == "main staged\n")
duplicate = git(main, "worktree", "add", str(root / "duplicate"), "main", check=False)
print("duplicate-branch-refused:", duplicate.returncode != 0)
git(main, "worktree", "add", "-q", "--detach", str(detached), "main")
print("detached-copy-allowed:", git(detached, "symbolic-ref", "-q", "HEAD", check=False).returncode == 1)
git(main, "worktree", "remove", str(review))
git(main, "worktree", "remove", str(detached))
All eight checks should print True. The administrative paths establish a common repository and separate indexes. Each index retains its own staged text, while the synthetic configuration and committed review branch are shared. A duplicate attached branch is rejected, but a detached checkout of the same commit is allowed. The experiment removes only its clean linked checkouts; Python then cleans the disposable main directory.
Sources
- Git worktree reference, shared state, per-worktree state, safeguards, and lifecycle commands, checked 2026-09-07.
- Git repository layout, linked administrative directories, common storage, and index placement, checked 2026-09-07.