fix(plugin): anchor WorktreeRemove at the project dir#3489
Conversation
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
left a comment
There was a problem hiding this comment.
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.
… 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>
Anchor the
WorktreeRemovehook atCLAUDE_PROJECT_DIR, mirroring #3453 which did the same forWorktreeCreate.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 asWorktreeRemove hook failedand now keeps the session row, so the completed background session becomes undeletable from theclaude agentsview.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):
With the anchor (
CLAUDE_PROJECT_DIRset to the repo), same cwd:Tests
No test change needed.
tests/integration_tests/config_show.rsasserts (a) every hook command uses unbraced$CLAUDE_PLUGIN_ROOT— unchanged, since the added${CLAUDE_PROJECT_DIR:-.}sits inside the single-quotedbash -cbody 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
WorktreeRemovefor a worktree that no longer exists makeswt removeexit non-zero with✗ No branch named <path>; the cwd anchor does not affect that. Tracked separately in #3488.