[Bug] EnterWorktree tool ignores WorktreeCreate/WorktreeRemove hooks

Status Open
Reported on v2.1.79
Maintainer reply None cached
Activity 10 comments · opened Mar 19, 2026

Bug Description
EnterWorktree tool does not invoke WorktreeCreate/WorktreeRemove hooks

When WorktreeCreate and WorktreeRemove hooks are configured in .claude/settings.json, the claude --worktree CLI flag correctly invokes them. However, the in-session EnterWorktree tool (used when asking Claude to switch to a worktree mid-conversation, or via Agent(isolation: "worktree")) ignores these hooks entirely and uses built-in git worktree add instead.

This means any repo that needs custom worktree setup (e.g. git-crypt key symlinking, sparse checkout, post-checkout dependency installation) works with --worktree but fails mid-session.

Repro:

  1. Configure a WorktreeCreate hook in .claude/settings.json
  2. Start Claude normally (without --worktree)
  3. Ask Claude to switch to a worktree – EnterWorktree runs built-in git worktree add, hook is never called

Expected: EnterWorktree should invoke WorktreeCreate/WorktreeRemove hooks the same way the --worktree CLI flag does.

Environment Info

  • Platform: darwin
  • Terminal: WarpTerminal
  • Version: 2.1.79
  • Feedback ID: 0f4e09a9-eb11-4fa5-bd88-7bf08c6261ea

Errors

[]

View original on GitHub ↗

10 Comments

yurukusa · 5 months ago

Workaround: use a PreToolUse hook on EnterWorktree to trigger your own lifecycle hooks:

INPUT=$(cat)
WORKTREE_PATH=$(echo "$INPUT" | jq -r '.tool_input.path // empty')
if [ -n "$WORKTREE_PATH" ]; then
    echo "[WorktreeLifecycle] Entering worktree: $WORKTREE_PATH" >&2
    if [ -f "$WORKTREE_PATH/.claude/worktree-setup.sh" ]; then
        bash "$WORKTREE_PATH/.claude/worktree-setup.sh" 2>&1
    fi
fi
exit 0

Similarly for cleanup:

INPUT=$(cat)
echo "[WorktreeLifecycle] Exiting worktree" >&2
exit 0

This ensures your lifecycle hooks fire even when using the EnterWorktree tool, until the native WorktreeCreate/WorktreeRemove events are properly connected.

yrichardson-talvy · 4 months ago

Still reproduces as of Claude Code 2.1.x with Opus 4.7 (1M). Adding one data point: the yurukusa PreToolUse workaround only covers user-initiated EnterWorktree calls. Auto-spawned side sessions (whose worktree is pre-created by the harness before the session starts) never fire an EnterWorktree tool use, so there's no tool event to hook onto — the session just boots inside a .claude/worktrees/<random> path with the user's WorktreeCreate hook silently skipped. A SessionStart hook could detect this, but only after the worktree already exists in the wrong place. Native dispatch of WorktreeCreate from every git worktree add code path is still the right fix.

yrichardson-talvy · 4 months ago

Adding a permissioning angle

This bug also has a subtle safety implication for users who scope dangerous capabilities by path glob in permissions.deny.

My ~/.claude/settings.json denies things like:

"Bash(*repos/sensitive_terraform_repo/*)",
"Bash(*repos/sensitive_terraform_repo.worktrees/*)"

The intent: Claude can write Terraform inside that repo or its sibling worktrees, nowhere else. When EnterWorktree / harness-isolation creates a worktree of that repo under <repo>/.claude/worktrees/<random> instead of the configured sibling layout, the path still happens to contain repos/sensitive_terraform_repo/, so the deny still fires. Lucky.

But the natural workaround for users hit by this — point Claude Desktop's "worktree base path" to a single shared dir like ~/repos/claude-worktrees/ because the field is one literal value, not a per-repo template — produces worktree paths that contain no repo identifier at all. Path-based deny rules silently stop matching. The user thinks they've contained Terraform-write to that repo; they haven't.

The native fix (firing WorktreeCreate from every git worktree add codepath) avoids this entirely because users can keep their per-repo sibling layout. The PreToolUse and SessionStart workarounds don't help — by the time they run, the worktree path is established and the in-session permissions evaluation context is loaded.

Flagging in case it raises priority — silently broken scoping rules are a worse failure mode than a missing setup hook.

KingMob · 3 months ago

New data point: jj-colocated repos suffer silent data-integrity corruption, not just a skipped setup hook.

Reproduced on a Jujutsu-colocated repo (jj on top of git) whose project config requires worktrees to be created via a custom WorktreeCreate hook that makes a jj workspace, never a plain git worktree. The auto-spawned background/isolation case (the one @yrichardson-talvy described — no EnterWorktree tool event to hook) bypasses the hook and calls git worktree add directly. The consequences go beyond a missing setup step:

  • The spawned dir under .claude/worktrees/<name> is a plain git worktree (.git is gitdir: <repo>/.git/worktrees/<name>, checked out on claude/<name>). It is absent from jj workspace list — jj has no knowledge of it.
  • Because jj only recognizes registered workspaces, jj root / jj workspace root invoked from inside the spawned worktree resolve to the shared main workspace, not an isolated one.
  • Therefore any jj write run from the session (jj new, jj commit, …) operates on the shared main workspace. Observed results:
  • Internal error: Failed to check out commit <hash> Caused by: Concurrent checkout races against the main workspace and any other session.
  • Cross-workspace edit entanglement: unrelated uncommitted edits sitting in the main workspace get folded into the session's commits.
  • This is systemic, not a one-off: every sibling .claude/worktrees/* dir created by the spawn mechanism is likewise a plain git worktree invisible to jj.

So for non-git VCS with a WorktreeCreate hook, the failure mode is silent corruption / contention of the shared main checkout and entanglement of unrelated work — a data-integrity bug, not merely an un-run setup script. Reinforces that the fix must be native dispatch of WorktreeCreate from every git worktree add code path (including pre-session harness isolation), so VCS-aware hooks can create a real isolated workspace.

KingMob · 3 months ago

Notably, I only see this bug in Claude Desktop (Claude 1.7196.3 (ca0c62)) at the moment, not Claude Code in the terminal. Running claude -w at the terminal works correctly, but starting a new Desktop Code conversation and checking the "worktree" box breaks this.

KingMob · 3 months ago

Honestly, I have global WorktreeCreate and WorktreeRemove hooks in ~/.claude/ specifically for this. I have no idea why Claude _ever_ bypasses them.

zhangyanglei · 3 months ago

I've encountered the same problem. Previously, when using bg, it would automatically enter the worktree. Now, when I execute it, it says "Do not call EnterWorktree" in the prompt.

ffittschen · 3 months ago

Still reproduces on v2.1.145 with a useful wrinkle the original report doesn't cover: the bug appears to be partial now, and the trigger matters.

Test A — EnterWorktree from the main checkout

  1. cd ~/repo (the main checkout, not a linked worktree)
  2. Start a Claude Code session.
  3. Invoke EnterWorktree({"name": "test-main"}).

Result: Worktree created at ~/repo/.claude/worktrees/test-main/. WorktreeCreate hook fires correctly — sentinel files / symlinks the hook is responsible for are present.

Test B — EnterWorktree from inside an existing linked worktree

  1. cd ~/repo/.claude/worktrees/some-existing-worktree
  2. Start a Claude Code session.
  3. Invoke EnterWorktree({"name": "test-nested"}).

Result: Worktree created at ~/repo/.claude/worktrees/some-existing-worktree/.claude/worktrees/test-nested/nested under the current worktree, not at the repo root. WorktreeCreate hook does not fire.

So in this version there are actually two problems chained together in Test B:

  1. EnterWorktree doesn't resolve to the main repo root before creating — it creates relative to the current worktree's path.
  2. The WorktreeCreate hook is skipped, and the harness silently falls back to native git worktree add.

A fix that only handles the hook dispatch would still leave Test B broken because the resulting worktree would land in the wrong place. The two need to be fixed together: resolve to the canonical repo root first, then run the hook.

This matches the docs' promise that WorktreeCreate "Replaces default git behavior", which Test A now honors and Test B doesn't. Looks like the fix landed partially between 2.1.79 (original report) and 2.1.145.

Environment: v2.1.145, macOS (Darwin 25.4.0), repo with several existing linked worktrees under .claude/worktrees/.

KingMob · 2 months ago

Any news on this?

KingMob · 2 months ago

Any news on this?