Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/worktrunk/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"hooks": [
{
"type": "command",
"command": "jq -r '.worktree_path' | xargs bash \"${CLAUDE_PLUGIN_ROOT}/hooks/wt.sh\" remove -D --foreground"
"command": "jq -r '.worktree_path' | xargs bash \"${CLAUDE_PLUGIN_ROOT}/hooks/wt.sh\" remove --foreground"
}
]
}
Expand Down
6 changes: 4 additions & 2 deletions skills/wt-switch-create/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ after it. Otherwise the task starts at the second token.

Don't remove the worktree yourself. `ExitWorktree({action: "remove"})` (if the
user asks to leave) or the session-exit prompt routes through this plugin's
`WorktreeRemove` hook → `wt remove -D --foreground`. A worktree with uncommitted
changes won't be auto-removed without confirmation — that's intended.
`WorktreeRemove` hook → `wt remove --foreground`. A worktree with uncommitted
changes won't be auto-removed without confirmation — that's intended. A branch
with committed-but-unmerged work is retained (with a `wt remove -D <branch>`
hint) instead of being silently force-deleted.

## Scope

Expand Down
18 changes: 18 additions & 0 deletions tests/integration_tests/config_show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3979,6 +3979,24 @@ fn test_plugin_layout_is_consolidated() {
}
}

// The WorktreeRemove hook must not force-delete unmerged branches (#2939).
// Claude Code auto-fires WorktreeRemove on session exit for any worktree
// with a clean working tree, so a hook command containing `-D` /
// `--force-delete` silently discards committed-but-unpushed work — the only
// recovery path is `git fsck`. The safe default retains unmerged branches
// and prints a `wt remove -D <branch>` hint for the user to act on.
let hooks = read("plugins/worktrunk/hooks/hooks.json");
let hooks_json = json("plugins/worktrunk/hooks/hooks.json");
let worktree_remove_cmd = hooks_json["hooks"]["WorktreeRemove"][0]["hooks"][0]["command"]
.as_str()
.expect("WorktreeRemove hook must define a command");
assert!(
!worktree_remove_cmd.contains(" -D") && !worktree_remove_cmd.contains("--force-delete"),
"WorktreeRemove hook must not pass -D / --force-delete (silently destroys \
committed-but-unpushed work on session-exit auto-remove; see #2939). \
hooks.json:\n{hooks}"
);

// The product description must not drift across tools. Byte-identical is
// schema-impossible (Codex omits the activity clause, Gemini says
// "extension"), but every manifest shares the canonical opening sentence.
Expand Down
Loading