Skip to content

fix(plugin): anchor WorktreeRemove at the project dir#3489

Merged
max-sixty merged 1 commit into
max-sixty:mainfrom
judewang:fix-worktree-remove-hook
Jul 16, 2026
Merged

fix(plugin): anchor WorktreeRemove at the project dir#3489
max-sixty merged 1 commit into
max-sixty:mainfrom
judewang:fix-worktree-remove-hook

Conversation

@judewang

Copy link
Copy Markdown
Contributor

Anchor the WorktreeRemove hook at CLAUDE_PROJECT_DIR, mirroring #3453 which did the same for WorktreeCreate.

The hook command inherits the working directory it is fired from. When that directory is not inside the repository — e.g. the agents-view session's worktree was already removed, or the hook is fired from a neutral cwd — wt remove <path> dies with fatal: not a git repository (repo discovery starts from cwd, not from the path argument). Claude Code (>= 2.1.211) surfaces this as WorktreeRemove hook failed and now keeps the session row, so the completed background session becomes undeletable from the claude agents view.

cd "${CLAUDE_PROJECT_DIR:-.}" pins the hook to the repository the session was launched in, falling back to current behavior when the variable is unset — identical to the WorktreeCreate anchor added in #3453.

Reproduction

Current hook, run from a cwd outside the repo (worktree clean and pushed):

✗ Failed to remove worktree
  fatal: not a git repository (or any of the parent directories): .git
# exit 1 → "WorktreeRemove hook failed" → session undeletable

With the anchor (CLAUDE_PROJECT_DIR set to the repo), same cwd:

◎ Removing <branch> worktree...
✓ Removed <branch> worktree & branch
# exit 0

Tests

No test change needed. tests/integration_tests/config_show.rs asserts (a) every hook command uses unbraced $CLAUDE_PLUGIN_ROOT — unchanged, since the added ${CLAUDE_PROJECT_DIR:-.} sits inside the single-quoted bash -c body exactly like the existing WorktreeCreate anchor, and (b) WorktreeRemove passes no -D/--force-delete (#2939) — unchanged. This is a 1-line change, same shape as #3453.

Out of scope

Firing WorktreeRemove for a worktree that no longer exists makes wt remove exit non-zero with ✗ No branch named <path>; the cwd anchor does not affect that. Tracked separately in #3488.

The WorktreeRemove hook inherits the working directory it is fired from.
When that is not inside the repo (e.g. the session's worktree was already
removed, or the hook fires from a neutral cwd), `wt remove <path>` dies with
`fatal: not a git repository` — repo discovery starts from cwd, not the path
argument. Claude Code surfaces this as `WorktreeRemove hook failed` and keeps
the session row, so the background session becomes undeletable.

`cd "${CLAUDE_PROJECT_DIR:-.}"` pins the hook to the launch repo, mirroring
the WorktreeCreate anchor added in max-sixty#3453.

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The change reads as correct and matches the #3453 precedent exactly (WorktreeCreate already uses the same cd "${CLAUDE_PROJECT_DIR:-.}" || exit 1 anchor). The cd only changes where git repo-discovery starts — it doesn't change which worktree is targeted: Claude Code passes worktree_path as an absolute path, and resolve_worktree_arg uses an absolute candidate as-is, resolving against the current directory only for relative paths (src/commands/worktree/resolve.rs, the candidate.is_absolute() branch). The existing config_show assertions still hold — the unbraced-$CLAUDE_PLUGIN_ROOT check is scoped to that specific variable (so the braced ${CLAUDE_PROJECT_DIR:-.} is fine, same as WorktreeCreate), and no -D/--force-delete is introduced.

That said, this edits the worktree-deletion surface — the WorktreeRemove hook in plugins/worktrunk/hooks/hooks.json runs wt remove --foreground "$p". Per this repo's review policy, changes to the deletion hooks are held for a human, so I'm requesting @max-sixty rather than approving. Flagging it explicitly: file plugins/worktrunk/hooks/hooks.json, command wt.sh remove (the auto-fired WorktreeRemove hook). No force flags are added and the change looks safe, but the maintainer should confirm the merge.

@max-sixty
max-sixty merged commit 5ebdcb1 into max-sixty:main Jul 16, 2026
39 checks passed
max-sixty pushed a commit that referenced this pull request Jul 16, 2026
… gone (#3493)

## Summary

Fixes #3488.

Claude Code (>= 2.1.211) auto-fires the plugin's `WorktreeRemove` hook
on session teardown for the recorded worktree path — but that worktree
may already be gone (removed by `wt merge` after a PR, or a manual `wt
remove`). In that case `wt remove <gone-path>` resolves the missing path
to a *branch-only* target and fails with `✗ No branch named <path>`
(exit 1). Claude Code treats a non-zero `WorktreeRemove` as a failed
removal and keeps the session row (`worktree could not be removed
(WorktreeRemove hook failed)`), so the completed background session can
no longer be deleted with Ctrl+X in the `claude agents` view.

Per @max-sixty's call on the issue, this fixes it directly in the hook
rather than adding a `--if-exists` flag (protected CLI surface) or
punting to Claude Code.

## Change

One guard in the `WorktreeRemove` hook command:

```diff
- ... cd "${CLAUDE_PROJECT_DIR:-.}" || exit 1; bash ".../wt.sh" remove --foreground "$p"'
+ ... cd "${CLAUDE_PROJECT_DIR:-.}" || exit 1; [ -e "$p" ] || exit 0; bash ".../wt.sh" remove --foreground "$p"'
```

If the recorded worktree path no longer exists on disk, the hook is a
no-op (exit 0). This is the narrowed form of the issue's Option 1:
leniency is scoped to "the worktree is already gone", not a blanket
success. When the path still exists, `wt remove` runs exactly as before,
so genuine failures (dirty / locked / unmerged branches — the #2939
spirit) still surface loudly and are never masked. It also doesn't
swallow a typo, since a typo'd path doesn't exist on disk only when it
also doesn't exist as a worktree — and Claude Code only ever fires this
hook with a path it recorded, not user input.

The guard lives inside the single-quoted `bash -c '...'` body, so it
doesn't touch outer login-shell (fish/zsh/bash) parsing, and it adds no
`-D`/`--force-delete`.

## Testing

- `test_claude_hook_commands_parse_in_all_shells` (fish/zsh/bash parse)
— passes
- `test_plugin_layout_is_consolidated` (includes the "no
`-D`/`--force-delete`" assertion on the WorktreeRemove command) — passes

This is orthogonal to #3489, which already landed the cwd
(`CLAUDE_PROJECT_DIR`) anchoring.

Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.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.

3 participants