[Bug] EnterWorktree tool ignores WorktreeCreate/WorktreeRemove hooks
Bug DescriptionEnterWorktree 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:
- Configure a
WorktreeCreatehook in.claude/settings.json - Start Claude normally (without
--worktree) - Ask Claude to switch to a worktree –
EnterWorktreeruns built-ingit 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
[]
10 Comments
Workaround: use a
PreToolUsehook onEnterWorktreeto trigger your own lifecycle hooks:Similarly for cleanup:
This ensures your lifecycle hooks fire even when using the EnterWorktree tool, until the native WorktreeCreate/WorktreeRemove events are properly connected.
Still reproduces as of Claude Code 2.1.x with Opus 4.7 (1M). Adding one data point: the
yurukusaPreToolUseworkaround only covers user-initiatedEnterWorktreecalls. Auto-spawned side sessions (whose worktree is pre-created by the harness before the session starts) never fire anEnterWorktreetool use, so there's no tool event to hook onto — the session just boots inside a.claude/worktrees/<random>path with the user'sWorktreeCreatehook silently skipped. ASessionStarthook could detect this, but only after the worktree already exists in the wrong place. Native dispatch ofWorktreeCreatefrom everygit worktree addcode path is still the right fix.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.jsondenies things like: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 containrepos/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
WorktreeCreatefrom everygit worktree addcodepath) 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.
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
WorktreeCreatehook that makes ajj workspace, never a plaingit worktree. The auto-spawned background/isolation case (the one @yrichardson-talvy described — noEnterWorktreetool event to hook) bypasses the hook and callsgit worktree adddirectly. The consequences go beyond a missing setup step:.claude/worktrees/<name>is a plain git worktree (.gitisgitdir: <repo>/.git/worktrees/<name>, checked out onclaude/<name>). It is absent fromjj workspace list— jj has no knowledge of it.jj root/jj workspace rootinvoked from inside the spawned worktree resolve to the shared main workspace, not an isolated one.jjwrite 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 checkoutraces against the main workspace and any other session..claude/worktrees/*dir created by the spawn mechanism is likewise a plain git worktree invisible to jj.So for non-git VCS with a
WorktreeCreatehook, 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 ofWorktreeCreatefrom everygit worktree addcode path (including pre-session harness isolation), so VCS-aware hooks can create a real isolated workspace.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 -wat the terminal works correctly, but starting a new Desktop Code conversation and checking the "worktree" box breaks this.Honestly, I have global WorktreeCreate and WorktreeRemove hooks in
~/.claude/specifically for this. I have no idea why Claude _ever_ bypasses them.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.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 —
EnterWorktreefrom the main checkoutcd ~/repo(the main checkout, not a linked worktree)EnterWorktree({"name": "test-main"}).Result: Worktree created at
~/repo/.claude/worktrees/test-main/.WorktreeCreatehook fires correctly — sentinel files / symlinks the hook is responsible for are present.Test B —
EnterWorktreefrom inside an existing linked worktreecd ~/repo/.claude/worktrees/some-existing-worktreeEnterWorktree({"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.WorktreeCreatehook does not fire.So in this version there are actually two problems chained together in Test B:
EnterWorktreedoesn't resolve to the main repo root before creating — it creates relative to the current worktree's path.WorktreeCreatehook is skipped, and the harness silently falls back to nativegit 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/.Any news on this?
Any news on this?