Owner doc for the fleet's worktree-placement convention. The machine truth is
a git config key, so the convention is readable by anything that can run
git config --get: humans, scripts, CI, and every agent, not just this
plugin. Prose surfaces (a repository's AGENTS.md / CLAUDE.md, skill text)
should cite this key, never copy the path: restating the path in several
places is the drift a git-config-readable convention exists to prevent.
[worktreeroot]
path = ~/worktrees- Name:
worktreeroot.path. Deliberately NOT underworktree.*, because git owns that namespace (worktree.guessRemote,worktree.useRelativePaths; git-worktree(1) Configuration). git-config(1) Variables invites third-party tools to invent their own variables, provided they do not collide with Git or other popular tools and are documented. Popular tools typically name the section after the tool (ghq.root,git-town.*,lfs.*,wt.basedir); a publisher-named key is an org-agnosticism defect, and a plugin-named key still couples consumers to this marketplace. This section is the capability, and it collides with neither Git'sworktree.*nor git-wt'swt.*. - Type: path (read with
--type=path, which expands a leading~). - Multi-valued, last value wins. An include can append rather than
override, which is what makes the
includeIflayering below work. - Retired alias:
scripts/worktree-root-legacy.sh(not this skill) rewrites a leftover publisher-named key ontoworktreeroot.pathat the winning origin and unsets it. Delete that peel after 2026-12-31. Fleet audit stays read-only and does not write. - Value: a directory OUTSIDE every repository, and outside
repository-discovery roots such as a ghq root (
ghq listenumerates each worktree there as a repository of its own; a leading dot does not hide it). On Windows, the same drive as the repositories it serves.
git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || exit # mandatory gate
root=$(git -C "$repo" config --get-all --type=path worktreeroot.path | tail -n 1)Two hazards, both verified on git 2.55 in #2610 and both silent:
- Never pass a scope flag without
--includes. Per git-config(1),--includesdefaults OFF "when a specific file is given (e.g., using--file,--global, etc)" and ON when searching all config files. A scoped read silently skips everyincludeIf, the whole per-identity layer. - Gate on
rev-parse --git-dirfirst. Under dubious ownership (safe.directory),git -C <repo> config --get <key>returns the GLOBAL value as though it were the repository's answer: rc=0, no stderr, and--show-scopereportsglobal.
scripts/worktree-create.sh (shared by the /worktree create skill and the
WorktreeCreate hook) resolves the root most specific first:
- Explicit
--root/--root-file, a per-invocation caller decision. worktreeroot.path, read from the target repository with includes on.includeIfsupplies per-identity and per-repository answers with no new machinery (below).--fallback-root/--fallback-root-file, the machine-globalworktree_rootplugin option, ranked below the key because only this plugin can read the option while every consumer can read the key.- The plugin data directory (
--data-root-file→<data-dir>/worktrees). - Absent all: refuse (exit 3). Never the in-repo
.claude/worktrees/. The nesting invariant theworktreeskill publishes owns that claim.
Whatever rung supplies the root, the helper's containment guard then rejects a root that itself resolves inside a working tree or a git directory. A misconfigured key is a refusal, not a licensed nesting.
Enforcement hooks: hooks/worktree-create-gate.sh (harness-driven
creations) and hooks/worktree-add-containment-gate.sh (a raw Bash
git worktree add targeting a path inside a repository, #2611).
EnterWorktree(name:) is not a Bash call and lands in the in-repo default;
the skill is contractually forbidden from calling the name form, and
harness-driven creation is covered by the WorktreeCreate hook. That pair
is the documented handling of the EnterWorktree(name:) gap.
One machine, several git identities: resolve the root through git's own conditional configuration. Parse order IS precedence (see hazards), so the machine default goes first and each more-specific include after it:
[user]
name = <name>
useConfigOnly = true # NO user.email here — see hazards
[worktreeroot]
path = <machine-default> # plain default FIRST — below an includeIf it would win
[includeIf "gitdir/i:<work-tree-root>/"]
path = ~/.config/git/identity-work.inc
[includeIf "gitdir/i:<personal-tree-root>/"]
path = ~/.config/git/identity-personal.inc
[includeIf "gitdir/i:**/dotfiles/.git"] # per-repo exception, survives re-clone
path = ~/.config/git/repo-dotfiles.incEach .inc sets worktreeroot.path (appending after the default,
so last-wins picks it up) alongside the identity keys. Verified properties
(hermetic lab, git 2.55):
includeIfsplices whole files and is not key-aware, so a custom key resolves exactly asuser.emaildoes.git -Cchdirs before repository discovery: the condition evaluates against the target repository's gitdir, never the cwd, so a fleet tool iterating repositories from elsewhere gets correct answers.- Linked worktrees classify with their repository: a worktree's
$GIT_DIRis always under its main repository, so a tree-anchoredgitdir:gives every worktree of a repository the same answer. Corollary: a pattern anchored at a worktree's own tree path matches nothing, ever. That presents as "includeIf is broken", and it is the likely first misdiagnosis.
- Use
gitdir/i:for the identity layer, nothasconfig:. libgit2 clients (gitui, TortoiseGit, git2/nodegit/pygit2) implementgitdir:,gitdir/i:,onbranch:but NOThasconfig:, and fail unrecognized conditions silently; JGit and go-git resolve noincludeIfat all.hasconfig:is fine forworktreeroot.*, which only CLI-shelling tools read. gitdir:is case-sensitive even on case-insensitive NTFS. Only the pattern author's spelling matters. Alwaysgitdir/i:on Windows.- Precedence is parse order, not specificity. A plain
[worktreeroot] pathbelow theincludeIfblock silently overrides every identity include. Two matching conditions: last parsed wins. - Attribution needs
--show-origin.--show-scopecollapses a conditionally-included file toglobal. - Junction-anchored patterns match nothing in either direction (contradicting the manpage's symlink claim). Anchor patterns at canonical target paths.
- Bare repositories have no
/.gitsuffix, so**/<name>/.gitpatterns silently miss them. - Per-repo exceptions belong in a name-keyed global include, not
.git/config: repo-local config is not cloned, so the exception vanishes on re-clone (twice, for a dotfiles repo with two peer clones). - Version floors:
gitdir:/gitdir/i:2.13,onbranch:2.23,hasconfig:remote.*.url:2.36,worktree:/worktree/i:2.56, unreleased as of 2026-08-26 (latest tag v2.55.0; the 2.55 docs do not list the condition yet, so verify it shipped before authoring for it). A config authored for 2.56 degrades silently on 2.55. - Per-worktree overrides need
config.worktreebehindextensions.worktreeConfig, since nogitdir:pattern can distinguish two worktrees of one repository. - Identity includes must set more than
user.email.user.signingkey,gpg.ssh.allowedSignersFile,core.sshCommand, andurl.*.insteadOfall leak from global otherwise, and a wrong SSH signing key verifies Good locally (git derives the principal from the signature; only the forge shows Unverified).user.useConfigOnlyis inert if a globaluser.emailexists.
scripts/worktree-root-doctor.sh [--repo-dir <dir>] makes the silent
failure classes loud and names which rule supplied the repository's root
(--show-origin, mapped back to its includeIf condition). It checks:
dubious-ownership / non-repository fallback, declared-but-unfired
conditions, include paths pointing at nonexistent files, unrecognized or
version-floor-gated condition keywords, gitdir: without /i on Windows, a
plain value parsed after an include-supplied one, scoped-read divergence, a
root that itself sits inside a repository, and identity partials
(user.email without user.signingkey where signing is configured). Exit 0
clean, 1 with findings. /source-control:worktree audit runs it as part of
its configuration-health step.
Add a pointer, not a path, e.g.:
Worktrees live under the root named by
git config --get-all --type=path worktreeroot.path | tail -n 1(never scope the read without--includes; gate onrev-parse --git-dirfirst). Convention: the source-control plugin'sreference/worktree-root-convention.md.