Hooks automate setup and validation at worktree lifecycle events. They're defined in .config/wt.toml (project config) and run automatically during wt switch --create and wt merge.
Hook types
| Hook | When | Blocking? | Fail-Fast? | Execution |
|---|---|---|---|---|
| post-create | After worktree created | Yes | No | Sequential |
| post-start | After worktree created | No | No | Parallel (background) |
| pre-commit | Before commit during merge | Yes | Yes | Sequential |
| pre-merge | Before merging to target | Yes | Yes | Sequential |
| post-merge | After successful merge | Yes | No | Sequential |
Blocking: Command waits for hook to complete before continuing. Fail-fast: First failure aborts the operation.
Configuration formats
Hooks can be a single command or multiple named commands. All hooks support both formats in .config/wt.toml:
Single command (string)
= "npm install"
Multiple commands (table)
[]
= "npm install"
= "npm run build"
Named commands appear in output with their labels:
$ wt switch --create feature-x
🔄 Running post-create install:
uv sync
Resolved 24 packages in 145ms
Installed 24 packages in 1.2s
✅ Created new worktree for feature-x from main at ../repo.feature-x
🔄 Running post-start dev:
uv run dev
Template variables
Hooks can use template variables that expand at runtime:
Basic variables (all hooks)
{{ repo }}— Repository name (e.g., "my-project"){{ branch }}— Branch name (slashes replaced with dashes){{ worktree }}— Absolute path to the worktree{{ worktree_name }}— Worktree directory name (e.g., "my-project.feature-foo"){{ repo_root }}— Absolute path to the repository root{{ default_branch }}— Default branch name (e.g., "main")
Git variables (all hooks)
{{ commit }}— Current HEAD commit SHA (full 40-character hash){{ short_commit }}— Current HEAD commit SHA (short 7-character hash){{ remote }}— Primary remote name (e.g., "origin"){{ upstream }}— Upstream tracking branch (e.g., "origin/feature"), if configured
Merge variables (pre-commit, pre-merge, post-merge)
{{ target }}— Target branch for the merge (e.g., "main")
# Tag builds with commit hash
= "echo '{{ short_commit }}' > .version"
# Reference merge target
= "echo 'Merging {{ branch }} into {{ target }}'"
Hook details
post-create
Runs after worktree creation, blocks until complete. The worktree switch doesn't finish until these commands succeed.
Use cases: Installing dependencies, database migrations, copying environment files — anything that must complete before work begins.
[]
= "npm ci"
= "npm run db:migrate"
= "cp .env.example .env"
Behavior:
- Commands run sequentially in declaration order
- Failure shows error but doesn't abort (worktree already created)
- User cannot work in worktree until complete
post-start
Runs after worktree creation, in background. The worktree switch completes immediately; these run in parallel.
Use cases: Long builds, dev servers, file watchers, downloading assets too large for git — anything slow that doesn't need to block.
[]
= "npm run build"
= "npm run dev"
= "./scripts/fetch-large-assets"
Behavior:
- All commands start immediately in parallel
- Output logged to
.git/wt-logs/{branch}-post-start-{name}.log - Failures don't affect the user session
pre-commit
Runs before committing during wt merge, fail-fast. All commands must exit with code 0 for the commit to proceed.
Use cases: Formatters, linters, type checking — quick validation before commit.
[]
= "cargo fmt -- --check"
= "cargo clippy -- -D warnings"
Behavior:
- Commands run sequentially
- First failure aborts the commit
- Runs for both squash and no-squash merge modes
pre-merge
Runs before merging to target branch, fail-fast. All commands must exit with code 0 for the merge to proceed.
Use cases: Tests, security scans, build verification — thorough validation before merge.
[]
= "cargo test"
= "cargo build --release"
Behavior:
- Commands run sequentially
- First failure aborts the merge (commit remains)
- Runs after commit succeeds, before merge
post-merge
Runs after successful merge and cleanup, best-effort. The merge has already completed and the feature worktree has been removed, so failures are logged but don't abort.
Use cases: Deployment, notifications, installing updated binaries — post-merge automation.
= "cargo install --path ."
Behavior:
- Commands run sequentially in the main worktree
- Runs after cleanup completes
- Failures show errors but don't affect the completed merge
When hooks run during merge
- pre-commit — After staging, before squash commit
- pre-merge — After rebase, before merge to target
- post-merge — After cleanup completes
See wt merge for the complete pipeline.
Security & approval
Project commands require approval on first run. When a project defines hooks, the first execution prompts for approval:
🟡 repo needs approval to execute 3 commands:
⚪ post-create install:
echo 'Installing dependencies...'
⚪ post-create build:
echo 'Building project...'
⚪ post-create test:
echo 'Running tests...'
❓ Allow and remember? [y/N]
Approval behavior:
- Approvals are saved to user config (
~/.config/worktrunk/config.toml) - If a command changes, new approval is required
- Use
--forceto bypass prompts (useful for CI/automation)
Manage approvals with wt config approvals:
Skipping hooks
Use --no-verify to skip all project hooks:
Logging
Background operations log to .git/wt-logs/ in the main worktree:
| Operation | Log file |
|---|---|
| post-start | {branch}-post-start-{name}.log |
| Background removal | {branch}-remove.log |
Logs overwrite on repeated runs for the same branch/operation. Stale logs from deleted branches persist but are bounded by branch count.
Running hooks manually
Use wt step to run individual hooks:
Example configurations
Node.js / TypeScript
[]
= "npm ci"
[]
= "npm run dev"
[]
= "npm run lint"
= "npm run typecheck"
[]
= "npm test"
= "npm run build"
Rust
[]
= "cargo build"
[]
= "cargo fmt -- --check"
= "cargo clippy -- -D warnings"
[]
= "cargo test"
= "cargo build --release"
[]
= "cargo install --path ."
Python (uv)
[]
= "uv sync"
[]
= "uv run ruff format --check ."
= "uv run ruff check ."
[]
= "uv run pytest"
= "uv run mypy ."
Python (pip/venv)
[]
= "python -m venv .venv"
= ".venv/bin/pip install -r requirements.txt"
[]
= ".venv/bin/black --check ."
= ".venv/bin/ruff check ."
= ".venv/bin/pytest"
Monorepo
[]
= "cd frontend && npm ci"
= "cd backend && cargo build"
[]
= "docker-compose up -d postgres"
[]
= "cd frontend && npm test"
= "cd backend && cargo test"
= "./scripts/integration-tests.sh"
Common patterns
Fast dependencies + slow build
Install dependencies blocking (must complete before work), build in background:
= "npm install"
= "npm run build"
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 behavior based on merge target:
= """
if [ "{{ target }}" = "main" ]; then
npm run deploy:production
elif [ "{{ target }}" = "staging" ]; then
npm run deploy:staging
fi
"""
Symlinks and caches
Set up shared resources that shouldn't be duplicated. The {{ repo_root }} variable points to the main worktree:
[]
= "ln -sf {{ repo_root }}/node_modules node_modules"
= "cp {{ repo_root }}/.env.local .env"