Hooks

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

HookWhenBlocking?Fail-Fast?Execution
post-createAfter worktree createdYesNoSequential
post-startAfter worktree createdNoNoParallel (background)
pre-commitBefore commit during mergeYesYesSequential
pre-mergeBefore merging to targetYesYesSequential
post-mergeAfter successful mergeYesNoSequential

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)

post-create = "npm install"

Multiple commands (table)

[post-create]
install = "npm install"
build = "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)

Git variables (all hooks)

Merge variables (pre-commit, pre-merge, post-merge)

# Tag builds with commit hash
post-create = "echo '{{ short_commit }}' > .version"

# Reference merge target
pre-merge = "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.

[post-create]
install = "npm ci"
migrate = "npm run db:migrate"
env = "cp .env.example .env"

Behavior:

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.

[post-start]
build = "npm run build"
server = "npm run dev"
assets = "./scripts/fetch-large-assets"

Behavior:

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.

[pre-commit]
format = "cargo fmt -- --check"
lint = "cargo clippy -- -D warnings"

Behavior:

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.

[pre-merge]
test = "cargo test"
build = "cargo build --release"

Behavior:

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.

post-merge = "cargo install --path ."

Behavior:

When hooks run during merge

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:

Manage approvals with wt config approvals:

wt config approvals list           # Show all approvals
wt config approvals clear <repo>   # Remove approvals for a repo

Skipping hooks

Use --no-verify to skip all project hooks:

wt switch --create temp --no-verify    # Skip post-create and post-start
wt merge --no-verify                   # Skip pre-commit, pre-merge, post-merge

Logging

Background operations log to .git/wt-logs/ in the main worktree:

OperationLog 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:

wt step post-create    # Run post-create hooks
wt step pre-merge      # Run pre-merge hooks
wt step post-merge     # Run post-merge hooks

Example configurations

Node.js / TypeScript

[post-create]
install = "npm ci"

[post-start]
dev = "npm run dev"

[pre-commit]
lint = "npm run lint"
typecheck = "npm run typecheck"

[pre-merge]
test = "npm test"
build = "npm run build"

Rust

[post-create]
build = "cargo build"

[pre-commit]
format = "cargo fmt -- --check"
clippy = "cargo clippy -- -D warnings"

[pre-merge]
test = "cargo test"
build = "cargo build --release"

[post-merge]
install = "cargo install --path ."

Python (uv)

[post-create]
install = "uv sync"

[pre-commit]
format = "uv run ruff format --check ."
lint = "uv run ruff check ."

[pre-merge]
test = "uv run pytest"
typecheck = "uv run mypy ."

Python (pip/venv)

[post-create]
venv = "python -m venv .venv"
install = ".venv/bin/pip install -r requirements.txt"

[pre-merge]
format = ".venv/bin/black --check ."
lint = ".venv/bin/ruff check ."
test = ".venv/bin/pytest"

Monorepo

[post-create]
frontend = "cd frontend && npm ci"
backend = "cd backend && cargo build"

[post-start]
database = "docker-compose up -d postgres"

[pre-merge]
frontend-tests = "cd frontend && npm test"
backend-tests = "cd backend && cargo test"
integration = "./scripts/integration-tests.sh"

Common patterns

Fast dependencies + slow build

Install dependencies blocking (must complete before work), build in background:

post-create = "npm install"
post-start = "npm run build"

Progressive validation

Quick checks before commit, thorough validation before merge:

[pre-commit]
lint = "npm run lint"
typecheck = "npm run typecheck"

[pre-merge]
test = "npm test"
build = "npm run build"

Target-specific behavior

Different behavior based on merge target:

post-merge = """
if [ "{{ target }}" = "main" ]; then
    npm run deploy:production
elif [ "{{ target }}" = "staging" ]; then
    npm run deploy:staging
fi
"""

Set up shared resources that shouldn't be duplicated. The {{ repo_root }} variable points to the main worktree:

[post-create]
cache = "ln -sf {{ repo_root }}/node_modules node_modules"
env = "cp {{ repo_root }}/.env.local .env"