Skip to content

feat: share Claude Code's worktree folder, by default reuse existing branches, and guard the main worktree - #8

Open
stilliard wants to merge 7 commits into
mainfrom
feat/claude-worktree-dirs
Open

feat: share Claude Code's worktree folder, by default reuse existing branches, and guard the main worktree#8
stilliard wants to merge 7 commits into
mainfrom
feat/claude-worktree-dirs

Conversation

@stilliard

@stilliard stilliard commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Worktrees now go in .claude/worktrees/

wt mk created worktrees as a sibling of the repo, so wt and Claude Code each kept their own set in different places — wt ls showed one, EnterWorktree made the other, and neither was wrong. The default is now $root/.claude/worktrees/<branch>, the same folder Claude Code uses, so both tools see the same worktrees and wt <name> navigates to them all.

Slashes still become dashes, and an explicit wt mk <branch> <path> still wins. Repos will want .claude/worktrees/ in .gitignore, as the worktrees now sit inside the working copy.

wt.path overrides it where that isn't what you want:

git config wt.path '../{repo}-{name}'      # the previous sibling layout
git config --global wt.path '~/wt/{name}'  # all repos, outside the tree

{name} is the branch with slashes replaced, {repo} the repo's folder name. Read with plain git config, so --local and --global both work with no extra handling, and worktrees share the main repo's config so it resolves the same from any of them. A relative template resolves against the repo root rather than cwd — same reasoning as the nesting bug below: a path that means different things depending on where you're stood is a bug waiting to happen. A template with no {name} is rejected up front, since it would collapse every branch onto one path and fail on the second wt mk with a confusing "already exists".

The default stays .claude/worktrees/ deliberately. Sharing a folder with Claude Code with zero setup is the point of it, and wt already reads Claude's session state for --claude, so this isn't a new dependency. The one cost of living inside the repo is needing .claude/worktrees/ gitignored — Claude Code users have that already, so wt mk warns rather than editing anyone's files:

wt: .claude/worktrees/foo is not gitignored; add it to .gitignore to keep git status clean

wt mk could only ever start a new branch

It always passed -b, so wt mk existing-branch failed outright. Now:

--base given            → git worktree add "$dest" -b "$branch" "$base"   # unchanged
branch exists locally   → git worktree add "$dest" "$branch"
branch on origin only   → git worktree add --track -b "$branch" "$dest" "origin/$branch"
otherwise               → git worktree add "$dest" -b "$branch"           # unchanged

So picking up someone's pushed branch is wt mk their-branch. origin is hardcoded — fine for the single-remote case this is for, and worth revisiting alongside the config work rather than guessing a remote now.

wt mk from inside a worktree nested the next one inside it

_wt_mk ends with cd "$dest", and $root came from git rev-parse --show-toplevel — which, from a linked worktree, is that worktree. So a second wt mk resolved the root to the worktree it had just moved into:

repo/.claude/worktrees/one
repo/.claude/worktrees/one/.claude/worktrees/two   ← two

The sibling layout hid this, since the nested result was just an oddly-placed sibling. Putting worktrees inside the repo makes it structural. Added _wt_root(), which resolves the main worktree via git rev-parse --git-common-dir — relative to cwd in the main worktree, absolute in a linked one, so cd "$(dirname ...)" && pwd handles both. _wt_mk, _wt_rm and the hook-root fallback all use it.

Caught by a live smoke test, not the suite — every existing mk test called wt mk from the repo root.

wt rm would run pre-rm against the main worktree

_wt_resolve matches branch names and directory basenames, so wt rm master — or the repo's own folder name — resolves the main worktree. git worktree remove refuses it, but only after _wt_rm has already cd'd there and run the pre-rm hook. A pre-rm hook is exactly the sort of thing that clears a cache or temp dir; the one I'm adding on the rec side is sudo rm -rf tmp/. So it would have run destructively against the main checkout and then reported a failure.

Guarded on the _wt_root() path before anything destructive:

wt: refusing to remove the main worktree

Worktrees living inside the repo makes a name collision between a worktree and the repo folder that bit more likely, but the bug predates that. The main-worktree checks in _wt_ls --claude and _wt_merged --rm now use the same detection instead of re-parsing git worktree list --porcelain.

Hooks had no way to find the repo they belong to

Hooks got WT_BRANCH and WT_PATH. _WT_HOOK_ROOT is set as a prefix assignment on a function call, so it's scoped to that call and never exported to the hook process — a hook wanting to run a setup script kept in the repo had to rediscover the root itself, from a cwd that is the new worktree, i.e. the same git-common-dir dance wt already does.

Hooks now also receive WT_ROOT, for both .wt-hooks/<event> and ad-hoc --pre-hook/--post-hook scripts.

Tests

79/79 passing (63 → 79), via bats test/.

  • wt_dest() in test/helpers.bash builds the expected default path, so the mk expectations moved with the default rather than being restated 18 times.
  • New: existing local branch reused (asserting the commit and branch name, not just that it worked), origin-only branch getting an upstream, and a branch already checked out elsewhere failing without leaving a directory behind.
  • New: wt mk from inside a worktree lands alongside it, not nested.
  • New: wt rm refuses the main worktree by branch name, and its pre-rm hook does not run — the second is the one that matters, since git refuses either way.
  • New: post-mk receives WT_ROOT pointing at the main worktree when invoked from a linked one.
  • New for wt.path: relative, absolute and {repo} templates; resolving identically from inside another worktree; a template with no {name} rejected without creating anything; and the gitignore warning firing and staying quiet in the matching cases.
  • New: wt mk outside a git repo fails without falling back to / (from Copilot's review).

Each behavioural test was confirmed to fail against the pre-change code. The "fails cleanly when already checked out" case passes both before and after — kept as a regression guard on the new branch-reuse path, not as evidence of the fix.

Also smoke tested by hand outside the suite: two wt mk runs in a scratch repo landing side by side, all three wt.path forms (default, {repo} sibling, ~-expanded) in one repo at once, and a full wt mk / wt rm cycle against a real repo with post-mk/pre-rm hooks doing actual setup and teardown.

🤖 Generated with Claude Code

stilliard and others added 3 commits August 21, 2026 14:50
Worktrees went to a sibling of the repo, so wt and Claude Code each had
their own set in different places. Default to .claude/worktrees/<branch>
inside the repo, where Claude Code puts them, so both tools see the same
worktrees. Repos will want .claude/worktrees/ in .gitignore, as the
worktrees now sit inside the working copy.

wt mk always passed -b, so it could only ever start a new branch. It now
checks out an existing local branch as is, creates a tracking branch for
one that only exists on origin, and creates the branch otherwise.

The repo root is now resolved via git-common-dir rather than
show-toplevel, so running wt mk from inside a worktree creates the next
one alongside it rather than nested in it - which the sibling layout hid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wt rm resolves a name against branch names and directory basenames, so
`wt rm master` or the repo's own folder name resolves the main worktree.
git refuses to remove it, but only after wt has already cd'd there and
run the pre-rm hook - and a pre-rm hook is exactly the kind of thing that
clears caches or temp dirs, so it would have run destructively against
the main checkout. Now worktrees live inside the repo, a name collision
between the two is that bit more likely.

Guard on the git-common-dir root before anything destructive, and use
the same detection for the main-worktree checks in merged/ls rather than
parsing it back out of git worktree list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hooks got WT_BRANCH and WT_PATH but no way to find the repo they belong
to: _WT_HOOK_ROOT is set for the duration of the wt function call, not
exported to the hook process. A hook that wants to run a setup script
kept in the repo had to rediscover the root itself, and doing that from
the new worktree's cwd is exactly the git-common-dir dance wt already
does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 21, 2026 22:18
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

_wt_mk/_wt_rm currently do not fail fast when _wt_root() fails, which can lead to incorrect root/path resolution before aborting.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates wt to align its worktree layout and behavior with Claude Code, while improving safety around main-worktree operations and enriching hook context.

Changes:

  • Default wt mk destination moved to $repo/.claude/worktrees/<branch> (matching Claude Code’s layout) and root detection now works reliably from linked worktrees.
  • wt mk can reuse existing local branches and can create a local tracking branch when the branch only exists on origin.
  • wt rm now refuses to act on the main worktree before running destructive hooks, and hooks now receive WT_ROOT.
File summaries
File Description
wt.sh Adds _wt_root() for main-worktree resolution, changes default worktree destination, reuses/tracks existing branches, guards main worktree removal, and adds WT_ROOT to hook env.
test/mk.bats Updates expectations for new default destination and adds coverage for branch reuse/tracking, nested-worktree prevention, and WT_ROOT hook behavior.
test/rm.bats Adds regression tests ensuring the main worktree cannot be removed and that pre-rm is not run for it.
test/helpers.bash Adds wt_dest() helper to centralize expected default worktree path construction.
README.md Documents the new default .claude/worktrees/<branch> layout, branch reuse behavior, and the new WT_ROOT hook env var.
Review details

Suppressed comments (1)

wt.sh:246

  • If _wt_root fails, _wt_rm will continue with an empty root value, which can break the main-worktree guard and subsequent cd "$root" recovery paths. Fail fast when _wt_root can’t be determined.
  local root; root=$(_wt_root)
  local target
  target=$(_wt_resolve "${1?usage: wt rm <name> [--claude] [--pre-hook P] [--post-hook P]}")
  [ -z "$target" ] && { echo "wt: no worktree matching '$1'" >&2; return 1; }
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread wt.sh Outdated
_wt_root returns non-zero outside a git repo, but mk and rm carried on
with an empty root, so the pre-mk hook lookup and the default dest
resolved against / before git failed on its own. Bail out instead, which
also drops the duplicate 'not a git repository' output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@stilliard stilliard left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@stilliard stilliard changed the title feat: share Claude Code's worktree folder, reuse existing branches, and guard the main worktree feat: share Claude Code's worktree folder, by default reuse existing branches, and guard the main worktree Aug 23, 2026
stilliard and others added 2 commits August 23, 2026 07:31
.claude/worktrees stays the default - sharing a folder with Claude Code
with no setup is the point, and wt already reads Claude's session state
elsewhere - but it shouldn't be the only option for anyone who doesn't
work that way. wt.path takes a template with {name} and {repo}, read
with plain git config so --local and --global both work, and
'../{repo}-{name}' gets the old sibling layout back.

A relative template resolves against the repo root rather than cwd, so
it means the same thing from inside any worktree, and a template with no
{name} is rejected up front rather than collapsing every branch onto one
path.

Worktrees inside the repo also now warn when the destination isn't
gitignored, which Claude Code users already have covered but anyone
pointing wt.path at an untracked folder wouldn't.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A relative wt.path joins onto the root as $root/../repo-foo, which still
string-matches "$root/*", so a sibling worktree looked like it was
inside the repo. check-ignore was then handed a path outside the work
tree and failed with 128, which the guard read as "not ignored" - so
the '../{repo}-{name}' layout the README recommends printed a git fatal
plus a bogus warning.

Collapse . and .. textually before the containment test - the
destination doesn't exist yet, so realpath/cd aren't options - and only
treat check-ignore's exit 1 as "not ignored" so an error can't
masquerade as one. The tests missed this by calling wt mk without run,
so nothing was asserted on the output; they now check both messages are
absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants