fix(hooks): approve post-switch against the removal's destination worktree#2748
Merged
Conversation
…orktree When a worktree is removed, the `post-switch` hook runs in the *destination* worktree — where the user lands — which `prepare_worktree_removal` records as `RemoveResult.main_path` (the primary worktree, except cwd when the primary worktree is itself the removal target). But `wt remove`'s and `wt step prune`'s approval gates collected the `post-switch` commands to prompt for from `repo.home_path()` instead. They agree in the common case (`main_path` == `home_path()`), but in a bare repo, removing the default-branch worktree from a different worktree, the gate approved the primary's `post-switch` while the executor ran cwd's — so an unapproved project command could run. (`wt merge` and the picker's `removal_hooks_approved` already passed the destination.) `collect_remove_hook_commands` now takes `(removed_worktree_paths, destination_paths)` instead of `(primary_repo, removed_worktree_paths)`: `pre-remove` / `post-remove` from each removed worktree, `post-switch` from each (path-deduped) destination — the same `.config/wt.toml` the executor reads. New `RemoveResult::destination_path() -> Option<&Path>` (sibling of `removed_worktree_path()`). `wt remove` passes each plan's `destination_path()`; `wt merge` passes the merge destination; `wt step prune` passes `home_path()` (a prune candidate is never the primary worktree, so that's always its destination). Behavior-neutral in every case the suite exercises — `post-switch` only fires when `changed_directory`, and in all of those sub-cases `main_path` resolves to `home_path()` anyway; the change closes the latent gate/executor decoupling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
worktrunk-bot
approved these changes
May 12, 2026
Merged
max-sixty
added a commit
that referenced
this pull request
May 13, 2026
Release v0.50.0. Highlights: - Experimental Azure DevOps support (#1256, thanks @mikeyroush; fixes #1144 from @dlecan) — `wt switch pr:<N>`, `wt list --full`, and `wt config show --full` recognize Azure DevOps via the `az` CLI. - Experimental Gitea CI-status detection (#2702) on top of the Gitea `pr:` shortcut (#1320, thanks @SjB). - Hooks now resolve `.config/wt.toml` from the worktree they act on — the primary-worktree fallback is gone, and `post-remove` reads the removed worktree's config (snapshotted before removal). Approval prompts collect hook commands from the same worktree. Breaking change for setups that relied on the primary-worktree fallback; the changelog entry has the recovery action. (#2690, #2703, #2714, #2717, #2701, #2708, #2727, #2736, #2748) - The `wt switch` picker's `alt-r` removal no longer runs unapproved project hooks (#2746) — the picker's removal path is now routed through `handle_remove_output` and consults the existing approval state read-only. - `wt config alias show` with no name lists every alias's full definition (#2684, #2691); `wt --help` switches to a compact aliases pointer (#2688). - `wt list --branches` warm-run perf: SHA-keyed cache for `main↕` and `Remote⇅` ahead/behind counts; shared push-remote URL and local-branch scan (#2704, #2718, #2673). - Claude Code plugin ships the `wt-switch-create` skill (#2737, thanks @onetom for #2631). See `CHANGELOG.md` for the full list (8 Improved, 5 Fixed, 5 Internal). semver-checks reports breaking library-API changes (new enum variants without `#[non_exhaustive]`, removed `Branch::github_push_url`, new trait method on `RemoteRefProvider`), which mandates at minimum a minor bump pre-1.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a worktree is removed, the
post-switchhook runs in the destination worktree — where the user lands — whichprepare_worktree_removalrecords asRemoveResult.main_path(the primary worktree, except cwd when the primary worktree is itself the removal target). Butwt remove's andwt step prune's approval gates collected thepost-switchcommands to prompt for fromrepo.home_path()instead. They agree in the common case (main_path == home_path()), so nobody noticed — but in a bare repo, removing the default-branch worktree from a different worktree,main_path == current_pathwhile the gate readhome_path(), so the gate approved the primary'spost-switchwhile the executor ran cwd's — an unapproved project command could run. (wt mergeand the picker'sremoval_hooks_approved, added in #2746, already passed the destination correctly.)What changed
collect_remove_hook_commandsnow takes(removed_worktree_paths: &[&Path], destination_paths: &[&Path])instead of(primary_repo: &Repository, removed_worktree_paths)— it collectspre-remove/post-removefrom each removed worktree andpost-switchfrom each destination (path-deduped, since the common case is the same primary repeated). NewRemoveResult::destination_path() -> Option<&Path>returnsmain_pathforRemovedWorktree,NoneforBranchOnly— a sibling of the existingremoved_worktree_path(). Callers updated:wt remove(single):approve_remove(result.removed_worktree_path().as_slice(), result.destination_path().as_slice(), …). (Multi: the same projections over the plan list.)wt merge: passes&[destination_path]— matchesfinish_after_merge'sRemoveResult { main_path: destination_path, … }.wt step prune: passes&[home_path()]— a prune candidate is never the primary worktree (gather_check_itemsfilters non-linked worktrees and the default-branch worktree), so each candidate's removal destination is alwayshome_path().removal_hooks_approvedgains amain_pathparam and passes&[main_path].The
commands::hooks"Which.config/wt.tomla hook reads" table row forpost-switch after a removalnow points atRemoveResult::destination_pathand notes the bare-repo cwd case.Testing
Behavior-neutral in every case the test suite exercises —
post-switchonly fires whenchanged_directory(the removed worktree was cwd), and in all of those sub-casesmain_pathresolves tohome_path()anyway, so the old and new gates collect the same commands. The change closes the latent decoupling. No new test added: the only observable difference is the bare-repo-remove-the-primary-from-elsewhere corner, and a dedicated integration test for it would be disproportionate; the new code paths (destination_path()on both variants, the destination loop incollect_remove_hook_commands, themain.rsplumbing) are covered by the existingwt remove/wt merge/wt step pruneintegration tests and thetest_do_removal_*picker unit tests.cargo run -- hook pre-merge --yesis green (3693 tests, lints, fmt, doctests).🤖 Generated with Claude Code