Skip to content

feat!: rationalize hooks — rename post-create, add post-commit, background post-merge - #1679

Merged
max-sixty merged 9 commits into
mainfrom
hooks
Mar 23, 2026
Merged

max-sixty merged 9 commits into
mainfrom
hooks

Conversation

@max-sixty

@max-sixty max-sixty commented Mar 23, 2026 •

Copy link
Copy Markdown
Owner

Implements the hook rationalization from #1670, establishing a symmetric pre- (blocking) / post- (background) pattern for every lifecycle event.

Changes

Rename post-create → pre-start: The old name suggested it ran after creation, but it actually runs before post-start as a blocking dependency step. Both names accepted for one release cycle — config deprecation detection, migration file generation, wt config update support, and CLI alias all in place. merge_with folds old-name hooks into new-name so cross-config combinations don't silently drop hooks.

Add post-commit hook: New background hook firing after successful commits (including squash commits), completing the commit lifecycle pair. Included in approval batches for wt step commit, wt step squash, and wt merge.

Change post-merge to background: Was blocking with Warn strategy, now runs in background like all other post- hooks. --foreground flag available for debugging.

The hook table is now a clean symmetric grid:

Event pre- (blocking) post- (background)
start pre-start post-start
switch pre-switch post-switch
commit pre-commit post-commit
merge pre-merge post-merge
remove pre-remove post-remove

Testing

868 lib + 483 bin + 1227 integration tests pass, all 13 lint checks pass. The deprecation has 23 dedicated unit tests covering detection at all three config scopes, migration, empty table filtering, cross-config merge safety, and integration with wt config show / wt config update.

Closes #1670

This was written by Claude Code on behalf of max-sixty

max-sixty and others added 3 commits March 22, 2026 21:38
…round post-merge

Implements the hook rationalization from #1670. Every lifecycle event now
has a symmetric pre- (blocking) / post- (background) pair:

| Event   | pre- (blocking) | post- (background)        |
|---------|-----------------|---------------------------|
| start   | pre-start       | post-start                |
| switch  | pre-switch      | post-switch               |
| commit  | pre-commit      | post-commit (new)         |
| merge   | pre-merge       | post-merge (was blocking) |
| remove  | pre-remove      | post-remove               |

Changes:

- **Rename post-create → pre-start**: accepts both names for one release
  cycle. Deprecation warning when post-create found in config. Migration
  file generated via `wt config update`. CLI alias `wt hook post-create`
  still works.

- **New post-commit hook**: fires in background after successful commit
  in `wt commit` and `wt merge`. Respects --no-verify. Useful for CI
  triggers, notifications, or background linting.

- **post-merge → background**: was blocking with Warn strategy, now
  spawns in background like all other post-* hooks. Removes PostMerge
  exit code propagation. `--foreground` flag added for debugging.

Ref #1670

Co-Authored-By: Claude <noreply@anthropic.com>
Update `after_long_help` in cli/mod.rs (primary source) and all synced
docs/skills files to reflect the hook rationalization:

- post-create → pre-start in all examples, tables, and section headers
- Add post-commit section with description and example
- Mark post-merge as non-blocking (background) in hook types table
- Update merge pipeline order to show background hooks
- Update cli/step.rs copy-ignored help text

Co-Authored-By: Claude <noreply@anthropic.com>
…table handling

- Add 23 unit tests for post-create deprecation (detection, migration,
  integration, snapshot) matching the coverage pattern of existing
  deprecations (select, commit-generation)
- Fix merge_with to fold post_create into pre_start so hooks from
  unmigrated configs aren't silently dropped when merging with configs
  that use the new name
- Filter empty [post-create] tables from deprecation detection,
  consistent with [select] handling

Co-Authored-By: Claude <noreply@anthropic.com>
Update test fixtures in approval_pty, config_update_pty, and
shell_wrapper to use `pre-start` instead of `post-create`. Accept
12 updated PTY snapshots reflecting the new hook name in approval
prompts and execution output.

Co-Authored-By: Claude <noreply@anthropic.com>
The help test was renamed to pre-start but the old snapshot file
wasn't deleted.

Co-Authored-By: Claude <noreply@anthropic.com>

@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.

Two documentation gaps:

  1. docs/content/merge.md Pipeline section (lines 61–71) needs updating. The hook.md cross-reference correctly says pre-commit → post-commit (background) → pre-merge → pre-remove → post-remove + post-merge (background), but the merge.md Pipeline section — which that cross-reference links to — still describes the old behavior:

    • No mention of pre-commit/post-commit hooks in the pipeline steps
    • Step 7 still describes post-merge as blocking ("Failures are logged but don't abort") instead of background
  2. Stale comment in src/commands/hooks.rs:37 — "Running post-create user hook" should be "Running pre-start user hook".

Both are outside the diff, so offering as a follow-up commit rather than inline suggestions.

max-sixty and others added 2 commits March 22, 2026 23:18
- Add commit step (pre-commit, commit, post-commit) to pipeline
- Fix post-merge description: runs in background, not blocking
- Fix stale comment referencing post-create in hooks.rs

Co-Authored-By: Claude <noreply@anthropic.com>
- Add PostCommit to approval batch in `wt step commit` and
  `wt step squash` so post-commit hooks get user consent
- Spawn post-commit hooks after squash commit, matching the
  direct commit path in CommitOptions::commit()

Co-Authored-By: Claude <noreply@anthropic.com>
max-sixty and others added 2 commits March 23, 2026 00:10
The hook command name completer didn't recognize `post-create` in the
command context, so `wt hook post-create <TAB>` returned no completions.
Map it to `pre-start` for the duration of the deprecation period.

Co-Authored-By: Claude <noreply@anthropic.com>
Move the repeated log path template from each post-* hook section into
the intro paragraph. Each hook section now focuses on use cases rather
than mechanics. Also remove redundant "Failure aborts" from pre-switch
(already stated in intro).

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty
max-sixty merged commit efad931 into main Mar 23, 2026
23 checks passed
@max-sixty
max-sixty deleted the hooks branch March 23, 2026 07:33
max-sixty added a commit that referenced this pull request Mar 23, 2026
Adds a changelog entry for #1679 (hook rationalization) to the 0.31.0
release notes. Covers the rename, new hook, post-merge behavior change,
and deprecation path.

> _This was written by Claude Code on behalf of max-sixty_

Co-authored-by: Claude <noreply@anthropic.com>
max-sixty added a commit that referenced this pull request Sep 7, 2026
… its worktree (#4026)

`wt merge` prints `◎ Running post-commit: …` and then spawns that
pipeline into a path the removal has already emptied, so a `post-commit`
hook can't be relied on to do what it says. Nothing reports this, and
two doc lines promised the behavior that doesn't happen.

`post-commit` is the only hook in the merge's background batch anchored
on the feature worktree — `post-merge`, `post-switch` and `post-remove`
all anchor on the destination. `HookAnnouncer` flushes every pending
pipeline once at the end of the command, which is after
`finish_after_merge` has removed that worktree.

What the runner then finds depends on where the worktree lived, because
the fast removal path renames it into `.git/wt/trash/` and leaves an
empty placeholder at the original path (`changed_directory: true`, so
the shell's `$PWD` stays valid), torn down a second later by the
detached `sleep 1 && rmdir`. `run_pipeline` calls `Repository::at` on
that placeholder, and git discovery walks up from it:

- **Worktree outside the repo** (the default `../{{ repo }}.{{ branch
}}`): no git ancestor, so the runner logs `failed to open repository for
pipeline` to `.git/wt/logs/<branch>/<source>/post-commit/runner.log`,
which nothing reads back. The hook doesn't run.
- **Worktree nested inside the repo** (`worktree-path = "{{ repo_path
}}/.worktrees/{{ branch | sanitize }}"`, one of the config page's own
examples): discovery resolves to the **primary** worktree,
`Repository::at` succeeds, and the steps do run — with their cwd set to
the placeholder `rmdir` unlinks a moment later, and any `git` inside
them resolving against the primary worktree.

It's a regression, not a hook that never worked: #1679 added
`post-commit` and it fired for five weeks, until #2457 collapsed the
merge's two or three `◎ Running …` lines into one. That was a cosmetic
change, and deferring the spawn to a single end-of-command flush moved
it past the removal.

Documented rather than fixed, deliberately. The commit `post-commit`
would fire on is squashed and rebased before the merge lands;
`pre-remove` already covers work that must finish in the feature
worktree, blocking removal until it does; and spawning at commit time
only narrows the window, since `git worktree remove` succeeds against a
live cwd. `post-commit` still runs properly wherever the worktree
survives — `--no-remove`, merging on the target branch, merging from the
primary worktree — and on `wt step commit` / `wt step squash`.

So this adds the explanation at the `HookAnnouncer` construction in
`handle_merge`, and corrects what the docs claimed:

- The merge pipeline's step 1 said "Post-commit hooks run in background"
flatly.
- The hook page said "During `wt merge`, hooks run in this order:
pre-commit → post-commit → pre-merge → pre-remove → post-remove +
post-merge" — wrong twice, since the `post-*` hooks don't run in
sequence either (#4020 corrected that one row above). It now gives the
blocking order, says the `post-*` hooks start together in the worktree
each is anchored on, and points at `pre-remove` or `--no-remove`.
- `test_merge_squash_combines_post_commit_…` said the merge "fires
post-commit"; it announces it. Both it and its auto-commit sibling now
say the announce line is all they pin.

No behavior change.

<details>
<summary>Reproduction</summary>

A scratch repo with a `post-commit` hook, a feature worktree with one
commit and one uncommitted file, then `wt merge main --yes`. Every case
prints `◎ Running post-commit: mark (user)`:

| Layout | Flags | Outcome |
|--------|-------|---------|
| external (`../{{ repo }}.{{ branch }}`) | default (squash) | hook
doesn't run |
| external | `--no-squash` | hook doesn't run |
| external | `--no-remove` | runs correctly |
| nested (`{{ repo_path }}/.worktrees/{{ branch }}`) | default (squash)
| runs in the doomed placeholder |

The external removing cases leave this in
`.git/wt/logs/feat/user/post-commit/runner.log`:

```
✗ failed to open repository for pipeline
  fatal: not a git repository (or any of the parent directories): .git
```

The nested case leaves an empty `runner.log` and the hook's own output
shows where it landed — `pwd` is the removed worktree's path, while `git
rev-parse --show-toplevel` from inside it answers with the primary
worktree:

```
cwd=/…/repo/.worktrees/feat
toplevel=/private/…/repo
```

The commit itself is made in every case — `Changes to dirty.txt` reaches
`main`.

</details>

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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.

Rationalizing hooks

2 participants