wt hook
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 (~/.config/worktrunk/config.toml) and project (.config/wt.toml) hooks are supported.
Hook types
| Hook | When | Blocking | Fail-fast |
|---|---|---|---|
post-start | After worktree created | No (background) | No |
post-create | After worktree created | Yes | No |
post-switch | After every switch | No (background) | 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 (background) | 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.
post-start
Dev servers, long builds, file watchers, copying caches. Output logged to .git/wt-logs/{branch}-{source}-post-start-{name}.log.
[]
= "wt step copy-ignored"
= "npm run dev -- --port {{ branch | hash_port }}"
post-create
Tasks that must complete before post-start hooks or --execute run: dependency installation, environment file generation.
[]
= "npm ci"
= "echo 'PORT={{ branch | hash_port }}' > .env.local"
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.
[]
= "[ -n \"$TMUX\" ] && tmux rename-window {{ branch | sanitize }}"
pre-commit
Formatters, linters, type checking — runs during wt merge before the squash commit.
[]
= "cargo fmt -- --check"
= "cargo clippy -- -D warnings"
pre-merge
Tests, security scans, build verification — runs after rebase, before merge to target.
[]
= "cargo test"
= "cargo build --release"
post-merge
Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the main worktree.
= "cargo install --path ."
pre-remove
Cleanup tasks before worktree is deleted, saving test artifacts, backing up state. Runs in the worktree being removed, with access to worktree files.
[]
= "tar -czf ~/.wt-logs/{{ branch }}.tar.gz test-results/ logs/ 2>/dev/null || true"
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.
[]
= "lsof -ti :{{ branch | hash_port }} | xargs kill 2>/dev/null || true"
= "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 for the complete pipeline.
Security
Project commands require approval on first run:
▲ repo needs approval to execute 3 commands:
○ post-create install:
echo 'Installing dependencies...'
❯ Allow and remember? [y/N]
- Approvals are saved to user config (
~/.config/worktrunk/config.toml) - If a command changes, new approval is required
- Use
--yesto bypass prompts (useful for CI/automation) - Use
--no-verifyto skip hooks
Manage approvals with wt hook approvals add and wt hook approvals clear.
Configuration
Hooks can be defined in two places: project config (.config/wt.toml) for repository-specific automation, or user config (~/.config/worktrunk/config.toml) for personal automation across all repositories.
Project hooks
Project hooks are defined in .config/wt.toml. They can be a single command or multiple named commands:
# Single command (string)
= "npm install"
# Multiple commands (table) — run sequentially in declaration order
[]
= "cargo test"
= "cargo build --release"
User hooks
Define hooks in ~/.config/worktrunk/config.toml to run for all repositories. User hooks run before project hooks and don't require approval. For repository-specific user hooks, see setting overrides.
# ~/.config/worktrunk/config.toml
[]
= "echo 'Setting up worktree...'"
[]
= "notify-send 'Merging {{ branch }}'"
User hooks support the same hook types and template variables as project hooks.
Key differences from project hooks:
| Aspect | Project hooks | User hooks |
|---|---|---|
| Location | .config/wt.toml | ~/.config/worktrunk/config.toml |
| Scope | Single repository | All repositories (or per-project) |
| Approval | Required | Not required |
| Execution order | After user hooks | Global first, then per-project |
Skip hooks with --no-verify. To run a specific hook when user and project both define the same name, use user:name or project:name syntax.
Use cases:
- Personal notifications or logging
- Editor/IDE integration
- Repository-agnostic setup tasks
Template variables
Hooks can use template variables that expand at runtime:
| Variable | Description |
|---|---|
{{ repo }} | Repository directory name |
{{ repo_path }} | Absolute path to repository root |
{{ branch }} | Branch name |
{{ worktree_name }} | Worktree directory name |
{{ worktree_path }} | Absolute worktree path |
{{ primary_worktree_path }} | Primary worktree path (main worktree for normal repos; default branch worktree for bare repos) |
{{ default_branch }} | Default branch name |
{{ commit }} | Full HEAD commit SHA |
{{ short_commit }} | Short HEAD commit SHA (7 chars) |
{{ remote }} | Primary remote name |
{{ remote_url }} | Remote URL |
{{ upstream }} | Upstream tracking branch (if set) |
{{ target }} | Target branch (merge hooks only) |
{{ base }} | Base branch (creation hooks only) |
{{ base_worktree_path }} | Base branch worktree (creation hooks only) |
Some variables may not be defined: upstream is only set when the branch tracks a remote; target, base, and base_worktree_path are hook-specific. Using an undefined variable directly errors — use conditionals for optional behavior:
[]
# Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature)
= "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}"
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) |
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 hash_port filter is useful for running dev servers on unique ports per worktree:
[]
= "npm run dev -- --host {{ branch }}.lvh.me --port {{ branch | hash_port }}"
Hash any string, including concatenations:
# Unique port per repo+branch combination
= "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:
[]
# Copy config from main worktree
= "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}"
JSON context
Hooks receive context as JSON on stdin, enabling complex logic that templates can't express:
=
# Run extra setup for feature branches on backend repos
The JSON includes all template variables plus hook_type and hook_name.
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.
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.
The --var KEY=VALUE flag overrides built-in template variables — useful for testing hooks with different contexts without switching to that context.
Designing effective hooks
post-start vs post-create
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) |
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.
Background processes spawned by post-start outlive the worktree — pair them with post-remove hooks to clean up. See Dev servers and Databases for examples.
Copying untracked files
Git worktrees share the repository but not untracked files. wt step copy-ignored copies gitignored files between worktrees:
[]
= "wt step copy-ignored"
Use post-create instead if subsequent hooks or --execute command need the copied files immediately.
Dev servers
Run a dev server per worktree on a deterministic port using hash_port:
[]
= "npm run dev -- --port {{ branch | hash_port }}"
[]
= "lsof -ti :{{ branch | hash_port }} | xargs kill 2>/dev/null || true"
The port is stable across machines and restarts — feature-api always gets the same port. Show it in wt list:
[]
= "http://localhost:{{ branch | hash_port }}"
For subdomain-based routing (useful for cookies/CORS), use lvh.me which resolves to 127.0.0.1:
[]
= "npm run dev -- --host {{ branch | sanitize }}.lvh.me --port {{ branch | hash_port }}"
Databases
Each worktree can have its own database. Docker containers get unique names and ports:
[]
= """
docker run -d --rm \
--name {{ repo }}-{{ branch | sanitize }}-postgres \
-p {{ ('db-' ~ branch) | hash_port }}:5432 \
-e POSTGRES_DB={{ branch | sanitize_db }} \
-e POSTGRES_PASSWORD=dev \
postgres:16
"""
[]
= "docker stop {{ repo }}-{{ branch | sanitize }}-postgres 2>/dev/null || true"
The ('db-' ~ branch) concatenation hashes differently than plain branch, so database and dev server ports don't collide.
Jinja2's operator precedence has pipe | with higher precedence than concatenation ~, meaning expressions need parentheses to filter concatenated values.
Generate .env.local with the connection string:
[]
= """
cat > .env.local << EOF
DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ branch | sanitize_db }}
DEV_PORT={{ branch | hash_port }}
EOF
"""
Progressive validation
Quick checks before commit, thorough validation before merge:
[]
= "npm run lint"
= "npm run typecheck"
[]
= "npm test"
= "npm run build"
Target-specific behavior
Different actions for production vs staging:
= """
if [ {{ target }} = main ]; then
npm run deploy:production
elif [ {{ target }} = staging ]; then
npm run deploy:staging
fi
"""
Python virtual environments
Use uv sync to recreate virtual environments (or python -m venv .venv && .venv/bin/pip install -r requirements.txt for pip-based projects):
[]
= "uv sync"
For copying dependencies and caches between worktrees, see wt step copy-ignored.
See also
wt merge— Runs hooks automatically during mergewt switch— Runs post-create/post-start hooks on--createwt config— Manage hook approvalswt config state logs— Access background hook logs
Command reference
wt hook - Run configured hooks
Usage: wt hook [OPTIONS] <COMMAND>
Commands:
show Show configured hooks
post-create Run post-create hooks
post-start Run post-start hooks
post-switch Run post-switch hooks
pre-commit Run pre-commit hooks
pre-merge Run pre-merge hooks
post-merge Run post-merge hooks
pre-remove Run pre-remove hooks
post-remove Run post-remove hooks
approvals Manage command approvals
Options:
-h, --help
Print help (see a summary with '-h')
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
-v, --verbose...
Verbose output (-v: hooks, templates; -vv: debug report)
Subcommands
wt hook approvals
Manage command approvals.
Project hooks require approval on first run to prevent untrusted projects from running arbitrary commands.
Examples
Pre-approve all commands for current project:
Clear approvals for current project:
Clear global approvals:
How approvals work
Approved commands are saved to user config. Re-approval is required when the command template changes or the project moves. Use --yes to bypass prompts in CI.
Command reference
wt hook approvals - Manage command approvals
Usage: wt hook approvals [OPTIONS] <COMMAND>
Commands:
add Store approvals in config
clear Clear approved commands from config
Options:
-h, --help
Print help (see a summary with '-h')
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
-v, --verbose...
Verbose output (-v: hooks, templates; -vv: debug report)