worktrunk 0.42.0

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
+++
title = "wt hook"
description = "Run configured hooks."
weight = 17

[extra]
group = "Commands"
+++

<!-- ⚠️ AUTO-GENERATED from `wt hook --help-page` — edit cli.rs to update -->

Run configured hooks.

Hooks are shell commands that run at key points in the worktree lifecycle — automatically during `wt switch`, `wt merge`, & `wt remove`, or on demand via `wt hook <type>`. Both user and project hooks are supported.

# Hook Types

| Event | `pre-` — blocking | `post-` — background |
|-------|-------------------|---------------------|
| **switch** | `pre-switch` | `post-switch` |
| **start** | `pre-start` | `post-start` |
| **commit** | `pre-commit` | `post-commit` |
| **merge** | `pre-merge` | `post-merge` |
| **remove** | `pre-remove` | `post-remove` |

`pre-*` hooks block — failure aborts the operation. `post-*` hooks run in the background with output logged (use [`wt config state logs`](@/config.md#wt-config-state-logs) to find and manage log files). 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) without blocking worktree creation. Prefer `post-start` over `pre-start` unless a later step needs the work completed first.

| Hook | Purpose |
|------|---------|
| `pre-switch` | Runs before branch resolution or worktree creation. `{{ branch }}` is the destination as typed (before resolution) |
| `post-switch` | Triggers on all switch results: creating, switching to existing, or staying on current |
| `pre-start` | Tasks that must complete before `post-start`/`--execute`: dependency install, env file generation |
| `post-start` | Dev servers, long builds, file watchers, copying caches |
| `pre-commit` | Formatters, linters, type checking — runs during `wt merge` before the squash commit |
| `post-commit` | CI triggers, notifications, background linting |
| `pre-merge` | Tests, security scans, build verification — runs after rebase, before merge to target |
| `post-merge` | Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree |
| `pre-remove` | Cleanup before worktree deletion: saving test artifacts, backing up state. Runs in the worktree being removed |
| `post-remove` | Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree |

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

# Security

Project commands require approval on first run:

{% terminal() %}
<span class="y">▲ <b>repo</b> needs approval to execute <b>3</b> commands:</span>

<span class="d">○</span> pre-start <b>install</b>:
<span style='background:var(--bright-white,#fff)'> </span> <span class="d"><span class="b">npm</span> ci</span>
<span class="d">○</span> pre-start <b>build</b>:
<span style='background:var(--bright-white,#fff)'> </span> <span class="d"><span class="b">cargo</span> build <span class="c">--release</span></span>
<span class="d">○</span> pre-start <b>env</b>:
<span style='background:var(--bright-white,#fff)'> </span> <span class="d"><span class="b">echo</span> <span class="g">'PORT={{ branch | hash_port }}'</span> <span class="c">></span> .env.local</span>

<span class="c">❯</span> Allow and remember? <b>[y/N]</b>
{% end %}

- Approvals are saved to `~/.config/worktrunk/approvals.toml`
- If a command changes, new approval is required
- Use `--yes` to bypass prompts — useful for CI and automation
- Use `--no-hooks` to skip hooks

Manage approvals with `wt config approvals add` and `wt config approvals clear`.

# Configuration

Hooks can be defined in project config (`.config/wt.toml`) or user config (`~/.config/worktrunk/config.toml`). Both use the same format.

## Hook forms

Hooks take one of three forms, determined by their TOML shape.

A string is a single command:

```toml
pre-start = "npm install"
```

A table is multiple commands that run concurrently:

```toml
[post-start]
server = "npm run dev"
watch = "npm run watch"
```

A pipeline is a sequence of `[[hook]]` blocks run in order. Each block is one step; multiple keys within a block run concurrently. A failing step aborts the rest of the pipeline:

```toml
[[post-start]]
install = "npm ci"

[[post-start]]
build = "npm run build"
server = "npm run dev"
```

Here `install` runs first, then `build` and `server` run together.

Most hooks don't need `[[hook]]` blocks. Reach for them when there's a dependency chain — typically setup that must complete before later steps, like installing dependencies before running a build and dev server concurrently.

Table form for pre-* hooks is deprecated and its behavior will change in a future version — use `[[hook]]` blocks instead.

## Project vs user hooks

| Aspect | Project hooks | User hooks |
|--------|--------------|------------|
| Location | `.config/wt.toml` | `~/.config/worktrunk/config.toml` |
| Scope | Single repository | All repositories (or [per-project](@/config.md#user-project-specific-settings)) |
| Approval | Required | Not required |
| Execution order | After user hooks | First |

Skip all hooks with `--no-hooks`. To run a specific hook when user and project both define the same name, use `user:name` or `project:name` syntax.

## Template variables

Hooks can use template variables that expand at runtime:

| Kind | Variable | Description |
|------|----------|-------------|
| active    | `{{ branch }}`                | Branch name |
|           | `{{ worktree_path }}`         | Worktree path |
|           | `{{ worktree_name }}`         | Worktree directory name |
|           | `{{ commit }}`                | Branch HEAD SHA |
|           | `{{ short_commit }}`          | Branch HEAD SHA (7 chars) |
|           | `{{ upstream }}`              | Branch upstream (if tracking a remote) |
| operation | `{{ base }}`                  | Base branch name |
|           | `{{ base_worktree_path }}`    | Base worktree path |
|           | `{{ target }}`                | Target branch name |
|           | `{{ target_worktree_path }}`  | Target worktree path |
|           | `{{ pr_number }}`             | PR/MR number (when creating via `pr:N` / `mr:N`) |
|           | `{{ pr_url }}`                | PR/MR web URL (when creating via `pr:N` / `mr:N`) |
| repo      | `{{ repo }}`                  | Repository directory name |
|           | `{{ repo_path }}`             | Absolute path to repository root |
|           | `{{ owner }}`                 | Primary remote owner path (may include subgroups) |
|           | `{{ primary_worktree_path }}` | Primary worktree path |
|           | `{{ default_branch }}`        | Default branch name |
|           | `{{ remote }}`                | Primary remote name |
|           | `{{ remote_url }}`            | Remote URL |
| exec      | `{{ cwd }}`                   | Directory where the hook command runs |
|           | `{{ hook_type }}`             | Hook type being run (e.g. `pre-start`, `pre-merge`) |
|           | `{{ hook_name }}`             | Hook command name (if named) |
|           | `{{ args }}`                  | Tokens forwarded from the CLI — see [Running Hooks Manually](#running-hooks-manually) |
| user      | `{{ vars.<key> }}`            | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) |

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:

| Operation | Bare vars | `base` | `target` |
|-----------|-----------|--------|----------|
| switch/create | destination | where you came from | = bare vars |
| commit (during merge/squash) | worktree being squashed | = bare vars | integration target |
| merge | feature being merged | = bare vars | merge target |
| remove | branch being removed | = bare vars | where you end up |

Pre and post hooks share the same perspective — `{{ branch | hash_port }}` produces the same port in `post-start` and `post-remove`. `cwd` is the worktree root where the hook command runs. It differs from `worktree_path` in three cases: pre-switch, where the hook runs in the source but `worktree_path` is the destination; post-remove, where the active worktree is gone so the hook runs in primary; and post-merge with removal, same — the active worktree is gone, so the hook runs in target.

Some variables are conditional: `upstream` requires remote tracking; `base` only appears in switch/create hooks; `target_worktree_path` requires the target to have a worktree; `pr_number`/`pr_url` are populated for `post-switch`, `pre-start`, and `post-start` hooks when creating via `pr:N` or `mr:N`; `vars` keys may not exist. Undefined variables error — use conditionals or defaults for optional behavior:

```toml
[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 %}"
```

Run any hook-firing command with `-v` to see the resolved variables for the actual invocation — each hook prints a `template variables:` block showing every in-scope variable and its value (`(unset)` for conditional vars that didn't populate, like `target_worktree_path` during `wt switch -`). Aliases do the same under `-v`: `wt -v <alias>` prints the alias's in-scope variables before the pipeline runs.

Variables use dot access and the `default` filter for missing keys. JSON object/array values are parsed automatically, so `{{ vars.config.port }}` works when the value is `{"port": 3000}`:

```toml
[post-start]
dev = "ENV={{ vars.env | default('development') }} npm start -- --port {{ vars.config.port | default('3000') }}"
```

## Worktrunk filters

Templates support Jinja2 filters for transforming values:

| Filter | Example | Description |
|--------|---------|-------------|
| `sanitize` | `{{ branch \| sanitize }}` | Replace `/` and `\` with `-` |
| `sanitize_db` | `{{ branch \| sanitize_db }}` | Database-safe identifier with hash suffix (`[a-z0-9_]`, max 63 chars) |
| `sanitize_hash` | `{{ branch \| sanitize_hash }}` | Filesystem-safe name with hash suffix for uniqueness |
| `hash_port` | `{{ branch \| hash_port }}` | Hash to port 10000-19999 |

The `sanitize` filter makes branch names safe for filesystem paths. The `sanitize_db` filter produces database-safe identifiers — lowercase alphanumeric and underscores, no leading digits, with a 3-character hash suffix to avoid collisions and reserved words. The `sanitize_hash` filter produces a filesystem-safe name and appends a 3-character hash suffix when sanitization changed the input, so distinct originals never collide — already-safe names pass through unchanged. The `hash_port` filter is useful for running dev servers on unique ports per worktree:

```toml
[post-start]
dev = "npm run dev -- --host {{ branch }}.localhost --port {{ branch | hash_port }}"
```

Hash any string, including concatenations:

```toml
# Unique port per repo+branch combination
dev = "npm run dev --port {{ (repo ~ '-' ~ branch) | hash_port }}"
```

Variables are shell-escaped automatically — quotes around `{{ ... }}` are unnecessary and can cause issues with special characters.

## Worktrunk functions

Templates also support functions for dynamic lookups:

| Function | Example | Description |
|----------|---------|-------------|
| `worktree_path_of_branch(branch)` | `{{ worktree_path_of_branch("main") }}` | Look up the path of a branch's worktree |

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
[pre-start]
# Copy config from main worktree
setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}"
```

## JSON context

Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express:

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

```python
import json, sys, subprocess
ctx = json.load(sys.stdin)
if ctx['branch'].startswith('feature/') and 'backend' in ctx['repo']:
    subprocess.run(['make', 'seed-db'])
```

# Running Hooks Manually

`wt hook <type>` runs hooks on demand — useful for testing during development, running in CI pipelines, or re-running after a failure.

{{ terminal(cmd="wt hook pre-merge              # Run all pre-merge hooks|||wt hook pre-merge test         # Run hooks named __WT_QUOT__test__WT_QUOT__ from both sources|||wt hook pre-merge test build   # Run hooks named __WT_QUOT__test__WT_QUOT__ and __WT_QUOT__build__WT_QUOT__|||wt hook pre-merge user:        # Run all user hooks|||wt hook pre-merge project:     # Run all project hooks|||wt hook pre-merge user:test    # Run only user's __WT_QUOT__test__WT_QUOT__ hook|||wt hook pre-merge --yes        # Skip approval prompts (for CI)|||wt hook pre-start --branch=feature/test    # Override a template variable|||wt hook pre-merge -- --extra args     # Forward tokens into __WT_OPEN2__ args __WT_CLOSE2__") }}

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.

## Passing values

`--KEY=VALUE` binds `KEY` whenever `{{ KEY }}` appears in any command of the hook — the same smart-routing rule `wt <alias>` uses. Built-in variables can be overridden: `--branch=foo` sets `{{ branch }}` inside hook templates (the worktree's actual branch doesn't move). Hyphens in keys become underscores: `--my-var=x` sets `{{ my_var }}`.

Any `--KEY=VALUE` whose key isn't referenced by a hook template forwards into `{{ args }}` as a literal `--KEY=VALUE` token. Tokens after `--` also forward into `{{ args }}` verbatim. `{{ args }}` renders as a space-joined, shell-escaped string; index with `{{ args[0] }}`, loop with `{% for a in args %}…{% endfor %}`, count with `{{ args | length }}`.

The long form `--var KEY=VALUE` is deprecated but still supported. It force-binds regardless of whether any hook template references `KEY` — useful when a template only references the key conditionally (e.g. `{% if override %}…{% endif %}`).

# Designing Effective Hooks

## Copying untracked files

Git worktrees share the repository but not untracked files. [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored) copies gitignored files between worktrees:

```toml
[post-start]
copy = "wt step copy-ignored"
```

## More recipes

- Copy gitignored files between worktrees: `wt step copy-ignored` in `post-start` shares build caches and dependencies; use a `[[post-start]]` pipeline when a later hook depends on the copy — see [Tips & Patterns](@/tips-patterns.md#eliminate-cold-starts)
- Dev server per worktree: `hash_port` in `post-start` for launch and `post-remove` for cleanup, with optional subdomain routing — see [Tips & Patterns](@/tips-patterns.md#dev-server-per-worktree)
- Database per worktree: a `post-start` pipeline stores container name, port, and connection string as [per-branch vars](@/config.md#wt-config-state-vars) that later hooks reference — see [Tips & Patterns](@/tips-patterns.md#database-per-worktree)
- Progressive validation: quick lint/typecheck in `pre-commit`, expensive tests and builds in `pre-merge` — see [Tips & Patterns](@/tips-patterns.md#progressive-validation)
- Target-specific behavior: branch on `{{ target }}` in `post-merge` for per-environment deploys — see [Tips & Patterns](@/tips-patterns.md#target-specific-hooks)

## See also

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

## Command reference

{% terminal() %}
wt hook - Run configured hooks

Usage: <b><span class=c>wt hook</span></b> <span class=c>[OPTIONS]</span> <span class=c>&lt;COMMAND&gt;</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-switch</span></b>  Run post-switch 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>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
  <b><span class=c>post-remove</span></b>  Run post-remove hooks

<b><span class=g>Options:</span></b>
  <b><span class=c>-h</span></b>, <b><span class=c>--help</span></b>
          Print help (see a summary with &#39;-h&#39;)

<b><span class=g>Global Options:</span></b>
  <b><span class=c>-C</span></b><span class=c> &lt;path&gt;</span>
          Working directory for this command

      <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
          User config file path

  <b><span class=c>-v</span></b>, <b><span class=c>--verbose</span></b><span class=c>...</span>
          Verbose output (-v: info logs + hook/alias template variable &amp; output; -vv: debug logs +
          diagnostic report + trace.log/output.log under .git/wt/logs/)

  <b><span class=c>-y</span></b>, <b><span class=c>--yes</span></b>
          Skip approval prompts
{% end %}

<!-- END AUTO-GENERATED from `wt hook --help-page` -->