Skip to content
Merged
2 changes: 1 addition & 1 deletion dev/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
# worktree-path = ".worktrees/{{ branch | sanitize }}"
# list.full = true
# merge.squash = false
# post-create.env = "cp .env.example .env"
# pre-start.env = "cp .env.example .env"
# aliases.deploy = "make deploy BRANCH={{ branch }}"
#
# ### Custom prompt templates
Expand Down
4 changes: 2 additions & 2 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence

```toml
# .config/wt.toml
[post-create]
[pre-start]
deps = "npm ci"

[pre-merge]
Expand Down Expand Up @@ -239,7 +239,7 @@ Override global user config for a specific project. Scalar values (like `worktre
worktree-path = ".worktrees/{{ branch | sanitize }}"
list.full = true
merge.squash = false
post-create.env = "cp .env.example .env"
pre-start.env = "cp .env.example .env"
aliases.deploy = "make deploy BRANCH={{ branch }}"
```

Expand Down
85 changes: 44 additions & 41 deletions docs/content/hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,21 @@ Hooks are shell commands that run at key points in the worktree lifecycle — au

# Hook Types

| Hook | When | Blocking | Fail-fast |
|------|------|----------|-----------|
| `pre-switch` | Before every switch | Yes | Yes |
| `post-create` | After worktree created | Yes | No |
| `post-start` | After worktree created | No | No |
| `post-switch` | After every switch | No | No |
| `pre-commit` | Before commit during merge | Yes | Yes |
| `pre-merge` | Before merging to target | Yes | Yes |
| `post-merge` | After successful merge | Yes | No |
| `pre-remove` | Before worktree removed | Yes | Yes |
| `post-remove` | After worktree removed | No | No |

**Blocking**: Command waits for hook to complete before continuing.
**Fail-fast**: First failure aborts the operation.

Background hooks show a single-line summary by default. Use `-v` to see expanded command details.
| 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` |

`pre-*` hooks block — failure aborts the operation (except `pre-start`, which warns and continues). `post-*` hooks run in the background with output logged to `.git/wt/logs/{branch}-{source}-{hook}-{name}.log`. Use `-v` to see expanded command details for background hooks.

The most common starting point is `post-start` — it runs background tasks (dev servers, file copying, builds) when creating a worktree.

## pre-switch

Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution). Failure aborts the switch.
Runs before every `wt switch` — before branch resolution or worktree creation. `{{ branch }}` is the destination branch argument as the user typed it (before resolution).

```toml
[pre-switch]
Expand All @@ -49,19 +42,19 @@ fi
"""
```

## post-create
## pre-start

Tasks that must complete before `post-start` hooks or `--execute` run: dependency installation, environment file generation.

```toml
[post-create]
[pre-start]
install = "npm ci"
env = "echo 'PORT={{ branch | hash_port }}' > .env.local"
```

## post-start

Dev servers, long builds, file watchers, copying caches. Output logged to `.git/wt/logs/{branch}-{source}-post-start-{name}.log`.
Dev servers, long builds, file watchers, copying caches.

```toml
[post-start]
Expand All @@ -71,7 +64,7 @@ server = "npm run dev -- --port {{ branch | hash_port }}"

## post-switch

Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current. Output logged to `.git/wt/logs/{branch}-{source}-post-switch-{name}.log`.
Triggers on all switch results: creating new worktrees, switching to existing ones, or staying on current.

```toml
[post-switch]
Expand All @@ -88,6 +81,15 @@ format = "cargo fmt -- --check"
lint = "cargo clippy -- -D warnings"
```

## post-commit

CI triggers, notifications, background linting.

```toml
[post-commit]
notify = "curl -s https://ci.example.com/trigger?branch={{ branch }}"
```

## pre-merge

Tests, security scans, build verification — runs after rebase, before merge to target.
Expand All @@ -100,7 +102,7 @@ build = "cargo build --release"

## post-merge

Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the main worktree.
Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree.

```toml
post-merge = "cargo install --path ."
Expand All @@ -117,15 +119,15 @@ archive = "tar -czf ~/.wt-logs/{{ branch }}.tar.gz test-results/ logs/ 2>/dev/nu

## post-remove

Cleanup tasks after worktree removal: stopping dev servers, removing containers, notifying external systems. All template variables reference the removed worktree, so cleanup scripts can identify resources to clean up. Output logged to `.git/wt/logs/{branch}-{source}-post-remove-{name}.log`.
Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree, so cleanup scripts can identify resources to tear down.

```toml
[post-remove]
kill-server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/dev/null || true"
remove-db = "docker stop {{ repo }}-{{ branch | sanitize }}-postgres 2>/dev/null || true"
```

During `wt merge`, hooks run in this order: pre-commit → pre-merge → pre-remove → post-remove → post-merge. See [`wt merge`](@/merge.md#pipeline) for the complete pipeline.
During `wt merge`, hooks run in this order: pre-commit → post-commit (background) → pre-merge → pre-remove → post-remove + post-merge (background). See [`wt merge`](@/merge.md#pipeline) for the complete pipeline.

# Security

Expand All @@ -134,7 +136,7 @@ Project commands require approval on first run:
```
▲ repo needs approval to execute 3 commands:

○ post-create install:
○ pre-start install:
echo 'Installing dependencies...'

❯ Allow and remember? [y/N]
Expand All @@ -157,7 +159,7 @@ Project hooks are defined in `.config/wt.toml`. They can be a single command or

```toml
# Single command (string)
post-create = "npm install"
pre-start = "npm install"

# Multiple commands (table) — run sequentially in declaration order
[pre-merge]
Expand All @@ -171,7 +173,7 @@ Define hooks in `~/.config/worktrunk/config.toml` to run for all repositories. U

```toml
# ~/.config/worktrunk/config.toml
[post-create]
[pre-start]
setup = "echo 'Setting up worktree...'"

[pre-merge]
Expand Down Expand Up @@ -219,7 +221,7 @@ Hooks can use template variables that expand at runtime:
| `{{ default_branch }}` | Default branch name |
| `{{ remote }}` | Primary remote name |
| `{{ remote_url }}` | Remote URL |
| `{{ hook_type }}` | Hook type being run (e.g. `post-create`, `pre-merge`) |
| `{{ hook_type }}` | Hook type being run (e.g. `pre-start`, `pre-merge`) |
| `{{ hook_name }}` | Hook command name (if named) |

Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side:
Expand All @@ -235,7 +237,7 @@ Pre and post hooks share the same perspective — `{{ branch | hash_port }}` pro
Some variables are conditional: `upstream` requires remote tracking; `base`/`target` are only in two-worktree hooks. Undefined variables error — use conditionals:

```toml
[post-create]
[pre-start]
# Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature)
sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}"
```
Expand Down Expand Up @@ -286,7 +288,7 @@ Templates also support functions for dynamic lookups:
The `worktree_path_of_branch` function returns the filesystem path of a worktree given a branch name, or an empty string if no worktree exists for that branch. This is useful for referencing files in other worktrees:

```toml
[post-create]
[pre-start]
# Copy config from main worktree
setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}"
```
Expand All @@ -296,8 +298,8 @@ setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path
Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express:

```toml
[post-create]
setup = "python3 scripts/post-create-setup.py"
[pre-start]
setup = "python3 scripts/pre-start-setup.py"
```

```python
Expand All @@ -319,7 +321,7 @@ wt hook pre-merge project: # Run all project hooks
wt hook pre-merge user:test # Run only user's "test" hook
wt hook pre-merge project:test # Run only project's "test" hook
wt hook pre-merge --yes # Skip approval prompts (for CI)
wt hook post-create --var branch=feature/test # Override template variable
wt hook pre-start --var branch=feature/test # Override template variable
```

The `user:` and `project:` prefixes filter by source. Use `user:` or `project:` alone to run all hooks from that source, or `user:name` / `project:name` to run a specific hook.
Expand All @@ -328,14 +330,14 @@ The `--var KEY=VALUE` flag overrides built-in template variables — useful for

# Designing Effective Hooks

## post-start vs post-create
## post-start vs pre-start

Both run when creating a worktree. The difference:

| Hook | Execution | Best for |
|------|-----------|----------|
| `post-start` | Background, parallel | Long-running tasks that don't block worktree creation |
| `post-create` | Blocks until complete | Tasks the developer needs before working (dependency install) |
| `pre-start` | Blocks until complete | Tasks the developer needs before working (dependency install) |

Many tasks work well in `post-start` — they'll likely be ready by the time they're needed, especially when the fallback is recompiling. If unsure, prefer `post-start` for faster worktree creation.

Expand All @@ -350,10 +352,10 @@ Git worktrees share the repository but not untracked files. [`wt step copy-ignor
copy = "wt step copy-ignored"
```

Use `post-create` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages:
Use `pre-start` instead if subsequent hooks need the copied files — for example, copying `node_modules/` before `pnpm install` so the install reuses cached packages:

```toml
[post-create]
[pre-start]
copy = "wt step copy-ignored"
install = "pnpm install"
```
Expand Down Expand Up @@ -409,7 +411,7 @@ Jinja2's operator precedence has pipe `|` with higher precedence than concatenat
Generate `.env.local` with the connection string:

```toml
[post-create]
[pre-start]
env = """
cat > .env.local << EOF
DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ branch | sanitize_db }}
Expand Down Expand Up @@ -451,7 +453,7 @@ fi
Use `uv sync` to recreate virtual environments (or `python -m venv .venv && .venv/bin/pip install -r requirements.txt` for pip-based projects):

```toml
[post-create]
[pre-start]
install = "uv sync"
```

Expand All @@ -460,7 +462,7 @@ For copying dependencies and caches between worktrees, see [`wt step copy-ignore
## See also

- [`wt merge`](@/merge.md) — Runs hooks automatically during merge
- [`wt switch`](@/switch.md) — Runs post-create/post-start hooks on `--create`
- [`wt switch`](@/switch.md) — Runs pre-start/post-start hooks on `--create`
- [`wt config`](@/config.md) — Manage hook approvals
- [`wt config state logs`](@/config.md#wt-config-state-logs) — Access background hook logs

Expand All @@ -474,10 +476,11 @@ Usage: <b><span class=c>wt hook</span></b> <span class=c>[OPTIONS]</span> <span
<b><span class=g>Commands:</span></b>
<b><span class=c>show</span></b> Show configured hooks
<b><span class=c>pre-switch</span></b> Run pre-switch hooks
<b><span class=c>post-create</span></b> Run post-create hooks
<b><span class=c>pre-start</span></b> Run pre-start hooks
<b><span class=c>post-start</span></b> Run post-start hooks
<b><span class=c>post-switch</span></b> Run post-switch hooks
<b><span class=c>pre-commit</span></b> Run pre-commit hooks
<b><span class=c>post-commit</span></b> Run post-commit hooks
<b><span class=c>pre-merge</span></b> Run pre-merge hooks
<b><span class=c>post-merge</span></b> Run post-merge hooks
<b><span class=c>pre-remove</span></b> Run pre-remove hooks
Expand Down
15 changes: 8 additions & 7 deletions docs/content/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ wt merge --no-commit

`wt merge` runs these steps:

1. **Squash** — Stages uncommitted changes, then combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/<branch>`. With `--no-squash`, uncommitted changes become a separate commit and individual commits are preserved.
2. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately.
3. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md).
4. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected.
5. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort.
6. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved.
7. **Post-merge hooks** — Hooks run after cleanup. Failures are logged but don't abort.
1. **Commit** — Pre-commit hooks run, then uncommitted changes are committed. Post-commit hooks run in background. With `--no-squash`, this is the only commit step; with squash (default), this is skipped and changes are staged during squash instead.
2. **Squash** — Combines all commits since target into one (like GitHub's "Squash and merge"). Use `--stage` to control what gets staged: `all` (default), `tracked`, or `none`. A backup ref is saved to `refs/wt-backup/<branch>`. With `--no-squash`, individual commits are preserved.
3. **Rebase** — Rebases onto target if behind. Skipped if already up-to-date. Conflicts abort immediately.
4. **Pre-merge hooks** — Hooks run after rebase, before merge. Failures abort. See [`wt hook`](@/hook.md).
5. **Merge** — Fast-forward merge to the target branch. With `--no-ff`, a merge commit is created instead (semi-linear history: rebased commits plus a merge commit). Non-fast-forward merges are rejected.
6. **Pre-remove hooks** — Hooks run before removing worktree. Failures abort.
7. **Cleanup** — Removes the worktree and branch. Use `--no-remove` to keep the worktree. When already on the target branch or in the primary worktree, the worktree is preserved.
8. **Post-remove + post-merge hooks** — Run in background after cleanup.

Use `--no-commit` to skip committing uncommitted changes and squashing; rebase still runs by default and can rewrite commits unless `--no-rebase` is passed. Useful after preparing commits manually with `wt step`. Requires a clean working tree.

Expand Down
4 changes: 2 additions & 2 deletions docs/content/step.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ Reflink copies share disk blocks until modified — no data is actually copied.

Uses per-file reflink (like `cp -Rc`) — copy time scales with file count.

Use the `post-start` hook so the copy runs in the background. Use `post-create` instead if subsequent hooks or `--execute` command need the copied files immediately.
Use the `post-start` hook so the copy runs in the background. Use `pre-start` instead if subsequent hooks or `--execute` command need the copied files immediately.

### Language-specific notes

Expand All @@ -397,7 +397,7 @@ The `target/` directory is huge (often 1-10GB). Copying with reflink cuts first
`node_modules/` is large but mostly static. If the project has no native dependencies, symlinks are even faster:

```toml
[post-create]
[pre-start]
deps = "ln -sf {{ primary_worktree_path }}/node_modules ."
```

Expand Down
2 changes: 1 addition & 1 deletion docs/content/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ When creating a worktree, worktrunk:
1. Runs [pre-switch hooks](@/hook.md#pre-switch) (blocking, fail-fast)
2. Creates worktree at configured path
3. Switches to new directory
4. Runs [post-create hooks](@/hook.md#post-create) (blocking)
4. Runs [pre-start hooks](@/hook.md#pre-start) (blocking)
5. Spawns [post-start hooks](@/hook.md#post-start) (background)
6. Spawns [post-switch hooks](@/hook.md#post-switch) (background)

Expand Down
4 changes: 2 additions & 2 deletions skills/worktrunk/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence

```toml
# .config/wt.toml
[post-create]
[pre-start]
deps = "npm ci"

[pre-merge]
Expand Down Expand Up @@ -230,7 +230,7 @@ Override global user config for a specific project. Scalar values (like `worktre
worktree-path = ".worktrees/{{ branch | sanitize }}"
list.full = true
merge.squash = false
post-create.env = "cp .env.example .env"
pre-start.env = "cp .env.example .env"
aliases.deploy = "make deploy BRANCH={{ branch }}"
```

Expand Down
Loading
Loading