| name | worktrunk |
| description | Use when: worktrunk, `wt` commands, `.config/wt.toml`, git worktrees for parallel agents, worktree hooks, LLM commit messages, agent handoffs, `hash_port`/`sanitize` filters, "run agents in parallel", "set up worktrees", managing multiple Claude Code sessions.
|
Worktrunk — Git Worktree Manager for Parallel AI Agents
Worktrunk is a CLI that makes git worktrees as easy as branches. Each AI agent (Claude Code, Codex, etc.) gets its own worktree so they don't step on each other's changes. GitHub: https://github.com/max-sixty/worktrunk
Installation
brew install worktrunk && wt config shell install
cargo install worktrunk && wt config shell install
winget install max-sixty.worktrunk
git-wt config shell install
paru worktrunk-bin && wt config shell install
Shell integration (wt config shell install) is required for directory switching — it wraps the binary so wt switch can change the shell's working directory.
If installation fails with C compilation errors: cargo install worktrunk --no-default-features (disables syntax highlighting but keeps all core functionality).
Core Commands
wt switch — Switch to worktree; create if needed
wt switch feature-auth
wt switch -
wt switch --create new-feature
wt switch --create hotfix --base production
wt switch pr:123
wt switch mr:42
wt switch
Flags:
-c, --create — Create a new branch
-b, --base <BASE> — Base branch (defaults to default branch)
-x, --execute <CMD> — Run command after switch (replaces wt process, gets full terminal). Supports template variables.
-- <ARGS> — Additional arguments passed to --execute command
Shortcuts: ^ (default branch), - (previous), @ (current)
Key pattern — alias for new worktree + agent:
alias wsc='wt switch --create -x claude'
wsc feature-branch
wsc feature -- 'Fix GH #322'
Lifecycle on create: pre-switch (blocking) → create worktree → cd → pre-start (blocking, warns on fail) → post-start (background) → post-switch (background)
wt list — List worktrees and status
wt list
wt list --full
wt list --branches
wt list --format=json
Columns: Branch, Status, HEAD± (uncommitted changes), main↕ (ahead/behind default), main…± (line diffs, --full), Summary (LLM, --full), Remote⇅, CI (--full), Path, URL, Commit, Age, Message
Status symbols:
| Symbol | Meaning |
|---|
+ | Staged files |
! | Modified (unstaged) |
? | Untracked files |
✘ | Merge conflicts |
⤴ | Rebase in progress |
↑/↓ | Ahead/behind default branch |
⇡/⇣ | Ahead/behind remote |
_ | Same as default branch |
⊂ | Integrated (safe to delete) |
^ | Is the default branch |
| ` | ` |
The table renders progressively — branch names appear instantly, status columns fill in as git operations complete.
wt remove — Remove worktree; delete branch if merged
wt remove
wt remove feature-branch
wt remove --no-delete-branch feat
wt remove -D experimental
wt remove --force feat
Branch cleanup logic (checks in order): same commit → ancestor → no added changes → trees match → merge adds nothing. Branches showing _ or ⊂ in wt list are safe to delete.
Flags: -f, --force (untracked files), -D, --force-delete (unmerged branches), --no-delete-branch, --foreground (block until complete)
wt merge — Merge current into target
Unlike git merge, this merges current into target (like GitHub's merge button). Target defaults to the default branch.
wt merge
wt merge develop
wt merge --no-remove
wt merge --no-squash
wt merge --no-ff
wt merge --no-commit
Pipeline: Commit → Squash → Rebase → Pre-merge hooks → FF Merge → Pre-remove hooks → Cleanup → Post-remove + Post-merge hooks (background)
- Default behavior: squash + rebase + fast-forward merge + remove worktree
--stage all|tracked|none controls what gets staged
- Backup ref saved to
refs/wt-backup/<branch>
- Conflicts during rebase abort immediately
wt step — Individual Operations
Building blocks of wt merge, plus standalone utilities.
wt step commit
wt step squash
wt step rebase
wt step push
wt step diff
wt step copy-ignored
wt step eval '{{ branch | hash_port }}'
wt step for-each -- echo '{{ branch }}'
wt step promote
wt step prune
wt step relocate
commit / squash flags: --stage all|tracked|none, --show-prompt (debug LLM prompt)
copy-ignored uses reflink (copy-on-write) when available. Language notes:
- Rust:
target/ is huge but reflink cuts build from ~68s to ~3s
- Python: venvs contain absolute paths and can't be copied — use
uv sync instead
- Node:
node_modules/ copies well with reflink
prune flags: --min-age 1h (default), --dry-run, --foreground
Aliases — custom commands in config, run via wt step <alias-name>:
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
port = "echo http://localhost:{{ branch | hash_port }}"
Hook System
Hooks are shell commands at key lifecycle points. 10 types across 5 events:
| Event | pre- (blocking) | post- (background) |
|---|
| start | pre-start (warns on fail) | post-start |
| switch | pre-switch | post-switch |
| commit | pre-commit | post-commit |
| merge | pre-merge | post-merge |
| remove | pre-remove | post-remove |
pre-* hooks block and abort on failure. post-* hooks run in background with logs at .git/wt/logs/{branch}-{source}-{hook}-{name}.log.
Project hooks go in .config/wt.toml (committed to repo, require approval on first run):
[pre-start]
deps = "npm ci"
[pre-merge]
test = "npm test"
build = "npm run build"
[post-start]
server = "npm run dev -- --port {{ branch | hash_port }}"
copy = "wt step copy-ignored"
User hooks go in ~/.config/worktrunk/config.toml (personal, no approval needed, run before project hooks).
Security: Project hooks need approval on first run. Changed commands need re-approval. --yes bypasses prompts. --no-verify skips hooks entirely. Manage with wt hook approvals add/clear.
Template Variables
| Variable | Description |
|---|
{{ branch }} | Active branch name |
{{ worktree_path }} | Active worktree path |
{{ worktree_name }} | Worktree directory name |
{{ commit }} / {{ short_commit }} | HEAD SHA (full / 7 chars) |
{{ base }} / {{ base_worktree_path }} | Base branch name / path |
{{ target }} / {{ target_worktree_path }} | Target branch name / path |
{{ default_branch }} | Default branch name |
{{ repo }} / {{ repo_path }} | Repo directory name / absolute path |
{{ primary_worktree_path }} | Primary worktree path |
{{ remote }} / {{ remote_url }} | Remote name / URL |
{{ hook_type }} / {{ hook_name }} | Current hook type / name |
Bare variables (branch, worktree_path) refer to the branch the operation acts on. base and target give the other side. Variables are shell-escaped automatically.
Filters
| Filter | Example | Description |
|---|
sanitize | {{ branch | sanitize }} | Replace / \ with - (path-safe) |
sanitize_db | {{ branch | sanitize_db }} | DB-safe identifier, hash suffix, max 63 chars |
hash_port | {{ branch | hash_port }} | Deterministic port 10000-19999 |
Hash concatenations for unique ports: {{ (repo ~ '-' ~ branch) | hash_port }}. Parentheses needed because | has higher precedence than ~.
Functions
worktree_path_of_branch("main") — returns the filesystem path of a branch's worktree, or empty string if none exists.
For detailed hook design patterns (dev servers, databases, tmux sessions, progressive validation, Caddy subdomain routing, etc.), read references/tips-patterns.md.
Configuration
| File | Location | Shared |
|---|
| User config | ~/.config/worktrunk/config.toml | No |
| Project config | .config/wt.toml | Yes (commit it) |
| System config | Platform-specific (wt config show) | Org-wide |
User config example
worktree-path = ".worktrees/{{ branch | sanitize }}"
[commit.generation]
command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --disable-slash-commands --setting-sources='' --system-prompt=''"
[merge]
squash = true
commit = true
rebase = true
remove = true
verify = true
no-ff = false
[list]
summary = true
Project config example
[pre-start]
deps = "npm ci"
[post-start]
server = "npm run dev -- --port {{ branch | hash_port }}"
copy = "wt step copy-ignored"
[pre-merge]
lint = "npm run lint"
test = "npm test"
[pre-remove]
server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/dev/null || true"
[list]
url = "http://localhost:{{ branch | hash_port }}"
[ci]
platform = "github"
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
Config subcommands
wt config shell install
wt config shell uninstall
wt config create
wt config create --project
wt config show
wt config show --full
wt config state default-branch
wt config state marker set "🚧"
wt config state logs get
Environment variables
All user config options can be overridden with WORKTRUNK_ prefix. Special variables:
| Variable | Purpose |
|---|
WORKTRUNK_BIN | Override binary path |
WORKTRUNK_CONFIG_PATH | Override user config location |
WORKTRUNK_SYSTEM_CONFIG_PATH | Override system config location |
WORKTRUNK_MAX_CONCURRENT_COMMANDS | Max parallel git commands (default: 32) |
NO_COLOR | Disable colored output |
LLM Commit Messages
Worktrunk generates commit messages by piping a templated prompt to an external command. Integrates with wt merge, wt step commit, wt step squash.
Setup
Add to ~/.config/worktrunk/config.toml:
[commit.generation]
command = "CLAUDECODE= MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --disable-slash-commands --setting-sources='' --system-prompt=''"
CLAUDECODE= unsets the nesting guard so claude -p works from within Claude Code. --no-session-persistence prevents pollution.
Custom templates
[commit.generation]
command = "llm -m claude-haiku-4.5"
template = """
Write a commit message for this diff. One line, under 50 chars.
Branch: {{ branch }}
Diff:
{{ git_diff }}
"""
squash-template = """
Combine these {{ commits | length }} commits into one message:
{% for c in commits %}
- {{ c }}
{% endfor %}
Diff:
{{ git_diff }}
"""
Branch summaries: With [list] summary = true and commit generation configured, wt list --full shows LLM-generated one-line summaries per branch.
Fallback: When no LLM is configured, worktrunk generates deterministic messages based on changed filenames.
Claude Code Integration
claude plugin marketplace add max-sixty/worktrunk
claude plugin install worktrunk@worktrunk
Provides:
- Configuration skill — Claude can help set up hooks, LLM commits, troubleshoot shell integration
- Activity tracking — 🤖 (working) and 💬 (waiting) markers in
wt list
Manual status markers: wt config state marker set "✅" --branch feature
Common Patterns (Quick Reference)
For full recipes with complete code examples, read references/tips-patterns.md.
| Pattern | Key command / config |
|---|
| New worktree + agent | alias wsc='wt switch --create -x claude' |
| Eliminate cold starts | post-start hook: wt step copy-ignored |
| Dev server per worktree | hash_port filter + [list] url |
| Database per worktree | Docker + sanitize_db + hash_port |
| Local CI gate | pre-merge hooks: lint, test, build |
| Stacked branches | wt switch --create part2 --base=@ |
| Agent handoffs | tmux/Zellij + -x claude -- 'prompt' |
| Reuse default branch | git rebase $(wt config state default-branch) |
| Bare repo layout | worktree-path = "{{ repo_path }}/../{{ branch | sanitize }}" |
Gotchas
-
Filter precedence: | binds tighter than ~. When hashing a concatenated string, you must use parentheses: {{ ('db-' ~ branch) | hash_port }}. Without them, {{ 'db-' ~ branch | hash_port }} hashes only branch and then concatenates — giving the wrong port.
-
Python venvs can't be copied between worktrees. They contain absolute paths. Don't use wt step copy-ignored for Python — use uv sync (or pip install -r requirements.txt) in a pre-start hook instead.
-
CLAUDECODE= is required for nested Claude calls. When generating LLM commit messages from within a Claude Code session, the CLAUDECODE env var blocks nested claude -p calls. The workaround is CLAUDECODE= (unset) at the start of the command string.
-
Shell integration is mandatory for wt switch. Without wt config shell install, the binary can't change the shell's working directory. Commands will run but you'll stay in the old directory. If wt switch seems broken, this is almost always the cause.
-
wt merge squashes by default. Unlike git merge, worktrunk squashes all branch commits into one. If you want to preserve individual commits, use --no-squash. Backup refs are saved to refs/wt-backup/<branch> either way.
-
Project hooks require approval on first run. If hooks seem to be silently skipped, the user hasn't approved them yet. Run wt hook show to check, or --yes to auto-approve. If a hook command changes (even whitespace), it needs re-approval.
-
wt remove deletes branches by default. It checks 5 merge-detection heuristics and deletes branches that appear merged. Use --no-delete-branch if you want to keep the branch, or -D to force-delete unmerged branches.
Troubleshooting
- Shell integration not working: Run
wt config show to diagnose. Ensure the shell rc file sources worktrunk.
wt conflicts with Windows Terminal: Use git-wt or disable the WT alias in Windows settings.
- Default branch wrong:
wt config state default-branch clear to reset cache.
- Hook not running: Check
wt hook show, verify approval with wt hook approvals, use -v for debug output.
- Background hook logs:
tail -f "$(wt config state logs get --hook=user:post-start:server)"
FAQ
- vs. branch switching: Worktrees give each agent isolated files and index — no conflicts.
- vs. plain
git worktree: Worktrunk automates the full lifecycle, adds hooks, status aggregation, and consistent naming.
- vs. git-machete / git-town: Different scope — those manage branch stacks in one directory. Worktrunk manages multi-worktree workflows. Can be used together.
- Windows: Works in Git Bash and PowerShell. Interactive picker unavailable. Git for Windows required for hooks.
- What can Worktrunk delete: Worktrees and branches, both with safeguards.
wt config state clear removes cached metadata.