+++
title = "Extending Worktrunk"
description = "Three ways to add custom behavior: hooks for lifecycle automation, aliases for reusable commands, and external subcommands for standalone tools."
weight = 21
[extra]
group = "Reference"
+++
Worktrunk has three extension mechanisms.
**[Hooks](#hooks)** run shell commands at lifecycle events — creating a worktree, merging, removing. They're configured in TOML and run automatically.
**[Aliases](#aliases)** define reusable commands invoked via `wt step <name>`. Same template variables as hooks, but triggered manually.
**[External subcommands](#external-subcommands)** are standalone executables. Drop `wt-foo` on `PATH` and it becomes `wt foo`. No configuration needed.
| **Trigger** | Automatic (lifecycle events) | Manual (`wt step <name>`) | Manual (`wt <name>`) |
| **Defined in** | TOML config | TOML config | Any executable on `PATH` |
| **Template variables** | Yes | Yes | No |
| **Shareable via repo** | `.config/wt.toml` | `.config/wt.toml` | Distribute the binary |
| **Language** | Shell commands | Shell commands | Any |
## Hooks
Hooks are shell commands that run at key points in the worktree lifecycle. Ten hooks cover five events:
| **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.
### Configuration
Hooks live in two places:
- **User config** (`~/.config/worktrunk/config.toml`) — personal, applies everywhere, trusted
- **Project config** (`.config/wt.toml`) — shared with the team, requires [approval](@/hook.md#wt-hook-approvals) on first run
Three formats, from simplest to most expressive.
A single command as a string:
```toml
pre-start = "npm ci"
```
A named table runs commands concurrently for `post-*` hooks and serially for `pre-*`:
```toml
[post-start]
server = "npm start"
watcher = "npm run watch"
```
An array of tables is a pipeline — blocks run in order, commands within a block run concurrently:
```toml
[[post-start]]
install = "npm ci"
[[post-start]]
server = "npm start"
build = "npm run build"
```
### Template variables
Hook commands are templates. Variables expand at execution time:
```toml
[post-start]
```
Core variables include `branch`, `worktree_path`, `commit`, `repo`, `default_branch`, and context-dependent ones like `target` during merge. Filters like `sanitize`, `hash_port`, and `sanitize_db` transform values for specific uses.
See [`wt hook`](@/hook.md#template-variables) for the full variable and filter reference.
### Common patterns
```toml
# .config/wt.toml
# Install dependencies when creating a worktree
[pre-start]
deps = "npm ci"
# Run tests before merging
[pre-merge]
test = "npm test"
lint = "npm run lint"
# Dev server per worktree on a deterministic port
[post-start]
See [Tips & Patterns](@/tips-patterns.md) for more recipes: dev server per worktree, database per worktree, tmux sessions, Caddy subdomain routing.
## Aliases
Aliases are custom commands invoked via `wt step <name>`. They share the same template variables and approval model as hooks.
```toml
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
An `up` alias that fetches all remotes and rebases each worktree onto its upstream:
```toml
[aliases]
up = '''
git fetch --all --prune && wt step for-each -- '
test -d "$g/rebase-merge" -o -d "$g/rebase-apply" && exit 0
git rebase @{u} --no-autostash || git rebase --abort
''''
```
When both user and project config define the same alias name, both run — user first, then project. Project-config aliases require approval, same as project hooks.
Alias names that collide with built-in step commands (`commit`, `squash`, `rebase`, etc.) are shadowed by the built-in.
### Recipe: move or copy in-progress changes to a new worktree
`wt switch --create` lands you in a clean worktree. To carry staged, unstaged, and untracked changes along, wrap it with git's stash plumbing:
```toml
# .config/wt.toml
[aliases]
move-changes = '''
if git diff --quiet HEAD && test -z "$(git ls-files --others --exclude-standard)"; then
wt switch --create {{ to }}
else
git stash push --include-untracked --quiet
wt switch --create {{ to }} --execute='git stash pop --index'
fi
'''
```
Run with `wt step move-changes --to=feature-xyz`. The leading guard avoids touching a pre-existing stash when nothing is in flight; otherwise, `git stash push --include-untracked` captures everything, `wt switch --create` makes the new worktree, and `git stash pop --index` (via `--execute`) restores the changes there with the staged/unstaged split intact.
To copy instead of move (source keeps its changes too), add `git stash apply --index --quiet` right after the push. For staged-only flows, swap the stash for `git diff --cached` written to a tempfile and applied with `git apply --index` in the new worktree — that handles files where staged and unstaged hunks overlap on the same lines, where `git stash --staged` falls short.
Because an inner `wt switch --create` inside an alias [propagates its `cd` to the parent shell](@/step.md#aliases), the alias drops you in the new worktree directly.
### Recipe: tail a specific hook log
`wt config state logs --format=json` emits structured entries — `branch`, `source`, `hook_type`, `name`, `path`. Pipe through `jq` to resolve one entry, then wrap in an alias for quick access:
```toml
[aliases]
hook-log = '''
| select(.branch == "{{ branch | sanitize_hash }}" and .hook_type == "post-start" and .name == $name)
| .path
' | head -1)"
'''
```
Run with `wt step hook-log --name=<hook-name>` (e.g., `wt step hook-log --name=server`) to tail the current worktree's `post-start` hook of that name. The `sanitize_hash` filter produces a filesystem-safe name with a hash suffix that keeps distinct originals unique — the same transformation Worktrunk applies on disk — so the alias resolves the right log even for branch and hook names containing characters like `/`.
See [`wt step` — Aliases](@/step.md#aliases) for the full reference.
## External subcommands
<span class="badge-experimental"></span>
Any executable named `wt-<name>` on `PATH` becomes available as `wt <name>` — the same pattern git uses for `git-foo`. Built-in commands always take precedence.
Arguments pass through verbatim, stdio is inherited, and the child's exit code propagates unchanged. External subcommands don't have access to template variables.
If nothing matches — no built-in, no nested subcommand, no `wt-<name>` on `PATH` — wt prints a "not a wt command" error with a typo suggestion.