ADR 0052: Worktrees live in a configurable sibling directory, not nested .claude/worktrees
Vocabulary amendment: Current pointers use
graduate→accept; the decision and reasoning are unchanged.
Status: accepted
Context
§discern is stack- and agent-neutral, but its isolated-worktree workflow placed every linked worktree at <repo>/.claude/worktrees/<name> — a path baked into the Claude Code worktree-create hook. That location was wrong on three counts:
.claude/is Claude-Code-specific. Nesting every agent's worktree under one vendor's folder is a category error for an agent-agnostic tool. A Codex or Gemini session has no reason to live under.claude/.- A worktree nested inside its own repo is a documented anti-pattern. Recursive tools double-count it — including discern's own gate, which globs the tree under
**; IDEs mis-index the nested checkout; and a tool that walks up to find the repo root mis-resolves the worktree's.gitfile (a gitlink, not a directory) when it sits beneath the main checkout. - The main checkout sits on the parent path of the worktree. That makes "accidentally edit or commit in the main repo while meaning to work in the worktree" an easy fat-finger, since the main tree literally contains the worktree.
Crucially, the engine was already location-agnostic by design: it discovers worktrees through git's own registry (git worktree list, the admin dir <common>/.git/worktrees/<key>) and derives identity from the git admin-dir basename / DISCERN_WORKTREE_ID — never the checkout path. So the bad location assumption was contained to the Claude-Code feature layer (the create hook) plus tests and docs, not the stack-neutral core.
Decision
§Place worktrees in a configurable directory, defaulting to a sibling of the repo. For a repo at /path/to/<repo>, worktrees go in /path/to/<repo>.worktrees/<name> — visible, adjacent, and outside the checkout entirely.
- New config key
[worktree].root— the directory under which per-worktree<name>checkouts are created:- empty / unset (the default) ⇒ the computed sibling above;
- a relative path ⇒ resolved against the repo root (
.claude/worktreesrestores the old nesting for anyone who wants it;../wtsa custom sibling); - an absolute path ⇒ used as-is.
- One shared resolver.
resolveWorktreeRoot(repoRoot, config)lives in the feature layer (src/lib/paths.ts), never the engine. Every spawn path goes through it: the create hook buildsjoin(resolveWorktreeRoot(cwd, config), name), andworktree prunepasses the resolved root as the orphan-sweep'sextraDirsso a fully-orphaned root (no registered worktree left to derive its parent from) is still reclaimed. The dispatch layer — notsrc/engine/**— resolves and threads the root, preserving the ADR 0040 split: the engine knows no placement convention. - Flip the default immediately. discern is pre-public (local dogfooding only), so the move is forward-only: new worktrees land in the sibling location, and existing nested worktrees keep working because the engine finds them via git regardless of where they sit. A schema-12→13 migration surfaces the documented
[worktree].rootkey (empty ⇒ sibling) in an existing config; it changes no behaviour (empty already is the new default) and never clobbers a hand-set value.
Consequences
§- The pitfalls are gone. The worktree is outside the repo, so no recursive tool double-counts it, no walk-up mis-resolves its
.gitfile, and the main checkout is no longer the worktree's parent.git statusin the main checkout stays clean the moment a worktree is created — without relying on a gitignore rule to hide it. - The gitignore no longer fences worktrees.
templates/.gitignore.fragment's/.claude/*rule remains (it still covers the materialized.claude/skills/, and a project that opts back into[worktree].root = ".claude/worktrees"), but its stale "worktree checkouts" rationale is dropped. - The convention has exactly one home. Placement is computed in
resolveWorktreeRootand nowhere else; the engine and its agent-agnosticism guard (tests/agent_agnostic_test.ts) are untouched — the guard now also scans the shared engine-test harness so a test can't reintroduce the old.claude/worktreesassumption either. - "Restore the old nesting" is one config line. A project that preferred the nested layout sets
root = ".claude/worktrees". - No data migration of existing worktrees. A worktree created before the flip is found, landed with
accept, and pruned exactly as before — git tracks it by its admin dir, not its path. (Moving the repo would break a worktree's absolute.gitlink, but that is a pre-existing git property, unchanged here.)
Alternatives considered
§- Keep nesting under
.claude/worktrees. Rejected: it is the source of all three pitfalls above, and the.claude/prefix is wrong for an agent-agnostic tool. - Nest under a generic
.discern/worktrees/instead. Drops the Claude-specificity but keeps the worktree inside the repo, so the recursive-glob,.git-file-misresolution, and fat-finger problems all remain. Rejected in favour of a visible sibling. - Teach the engine the sibling convention directly. Rejected: it would reintroduce a location assumption into the stack-neutral core that ADR 0040 deliberately kept out. The engine keeps discovering worktrees from git; only the feature/dispatch layer knows where new ones go.
- Make the move opt-in and keep nesting the default. Rejected: discern is pre-public and the nested default is known-bad, so carrying it forward only to spare a migration that isn't needed (the engine finds old worktrees regardless) trades a clean flip for lasting baggage.