
Worktrunk
- 3.9k installs
- 6.3k repo stars
- Updated August 5, 2026
- max-sixty/worktrunk
Guidance for setting up, configuring, and troubleshooting Worktrunk (wt CLI): user config (LLM, paths, defaults), project hooks (lifecycle automation), shell integration, template variables, aliases, approval workflows,
About
Worktrunk (wt) is a CLI tool for managing git worktrees with integrated hooks, LLM-powered commit generation, and multi-branch automation. Load this skill when configuring user config (LLM setup, worktree paths), project hooks (pre-start, pre-commit, pre-merge, post-merge, etc.), shell integration, or troubleshooting wt behavior. Supports approval workflows, template expansion, aliases, and agent handoffs via tmux/Zellij. Two-config model separates personal preferences (~/.config/worktrunk/config.toml, requires consent) from team automation (.config/wt.toml, checked into git). Key workflows: commit message generation, project hook setup, hook additions to existing configs, and parallel sub-agent spawning.
- Two-config architecture: user config (personal, consent required) vs project config (team, proactive)
- 10 hook types (5 events x pre/post): pre-start, post-start, pre-switch, post-switch, pre-commit, post-commit, pre-merge,
- LLM commit generation with Claude, Codex, llm, or aichat; template expansion and prompt building
- Hook approval workflow via wt config approvals add; security-first design prevents untrusted shell execution
- Agent handoffs: tmux/Zellij spawning, parallel sub-agents, worktree isolation patterns
Worktrunk by the numbers
- 3,920 all-time installs (skills.sh)
- +206 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #50 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/max-sixty/worktrunk --skill worktrunkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.9k |
|---|---|
| repo stars | ★ 6.3k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | max-sixty/worktrunk ↗ |
What it does
Manage git worktrees, configure hooks, and automate workflows across multiple branches with LLM-powered commit generation.
Who is it for?
Teams using git worktrees for parallel feature branches; developers wanting LLM-assisted commit messages; projects requiring automated hooks (npm install, cargo test, pytest); multi-agent workflows spawning worktrees.
Skip if: Single-branch workflows; non-git version control; users unwilling to run CLI tools; approval-free/unaudited automation (wt requires explicit consent).
When should I use this skill?
Editing .config/wt.toml or ~/.config/worktrunk/config.toml; adding/modifying hooks (post-merge, post-start, pre-commit, pre-merge, post-switch); setting up LLM commit generation; debugging wt behavior; spawning agents in
What you get
Users automate worktree lifecycle (pre-start, post-start, pre-commit, pre-merge, post-merge), generate AI-powered commit messages, enforce team quality checks, and safely spawn parallel agent tasks.
- isolated worktree per session
- wt list session status
- configured agent hooks
By the numbers
- Documents integration across 4 agent CLIs: Claude Code, Codex, OpenCode, and Gemini CLI
Files
Worktrunk
Help users work with Worktrunk, a CLI tool for managing git worktrees.
Available Documentation
Reference files are synced from worktrunk.dev documentation:
- reference/config.md: User and project configuration (LLM, hooks, command defaults)
- reference/hook.md: Hook types, timing, and execution order
- reference/switch.md, merge.md, list.md, etc.: Command documentation
- reference/extending.md: Aliases, multi-step pipelines, custom subcommands, and template-expansion gotchas (two-pass
{% raw %}deferral, for-each recipes) - reference/llm-commits.md: LLM commit message generation
- reference/tips-patterns.md: Practical recipes — aliases, per-branch variables, dev server per worktree, parallel agent patterns
- reference/shell-integration.md: Shell integration debugging
- reference/troubleshooting.md: Troubleshooting for LLM and hooks (Claude-specific)
For command-specific options, run wt <command> --help. For configuration, follow the workflows below.
Two Types of Configuration
Worktrunk uses two separate config files with different scopes and behaviors:
User Config (~/.config/worktrunk/config.toml)
- Scope: Personal preferences for the individual developer
- Location:
~/.config/worktrunk/config.toml(never checked into git) - Contains: LLM integration, worktree path templates, command settings, user hooks, approved commands
- Permission model: Always propose changes and get consent before editing
- See:
reference/config.mdfor detailed guidance
Project Config (.config/wt.toml)
- Scope: Team-wide automation shared by all developers
- Location:
<repo>/.config/wt.toml(checked into git) - Contains: Hooks for worktree lifecycle (pre-start, pre-merge, etc.)
- Permission model: Proactive (create directly, changes are reversible via git)
- See:
reference/hook.mdfor detailed guidance
Determining Which Config to Use
When a user asks for configuration help, determine which type based on:
User config indicators:
- "set up LLM" or "configure commit generation"
- "change where worktrees are created"
- "customize commit message templates"
- Affects only their environment
Project config indicators:
- "set up hooks for this project"
- "automate npm install"
- "run tests before merge"
- Affects the entire team
Both configs may be needed: For example, setting up commit message generation requires user config, but automating quality checks requires project config.
Core Workflows
Setting Up Commit Message Generation (User Config)
Most common request. See reference/llm-commits.md for supported tools and exact command syntax.
1. Detect available tools
which claude codex llm aichat 2>/dev/null2. If none installed, recommend Claude Code (already available in Claude Code sessions)
3. Propose config change — Get the exact command from reference/llm-commits.md
[commit.generation]
command = "..." # see reference/llm-commits.md for tool-specific commandsAsk: "Should I add this to your config?"
4. After approval, apply
- Check if config exists:
wt config show - If not, guide through
wt config create - Read, modify, write preserving structure
5. Suggest testing
wt step commit --show-prompt | head # verify prompt builds
wt merge # in a repo with uncommitted changesSetting Up Project Hooks (Project Config)
Common request for workflow automation. Follow discovery process:
1. Detect project type
ls package.json Cargo.toml pyproject.toml2. Identify available commands
- For npm: Read
package.jsonscripts - For Rust: Common cargo commands
- For Python: Check pyproject.toml
3. Design appropriate hooks (10 hook types: 5 events × pre/post — see reference/hook.md)
- Dependencies (fast, must complete) →
pre-start - Tests/linting (must pass) →
pre-commitorpre-merge - Long builds, dev servers →
post-start - Setup before branch resolution →
pre-switch - Terminal/IDE updates →
post-switch - After-commit triggers (CI, notifications) →
post-commit - Deployment →
post-merge - Cleanup before removal →
pre-remove - Cleanup after removal (stop servers, remove containers) →
post-remove
4. Validate commands work
npm run lint # verify exists
which cargo # verify tool exists5. Create `.config/wt.toml`
# Install dependencies when creating worktrees
pre-start = "npm install"
# Validate code quality before committing
[pre-commit]
lint = "npm run lint"
typecheck = "npm run typecheck"
# Run tests before merging
pre-merge = "npm test"6. Add comments explaining choices
7. Suggest testing
wt switch --create test-hooksSee `reference/hook.md` for complete details.
Adding Hooks to Existing Config
When users want to add automation to an existing project:
1. Read existing config: cat .config/wt.toml
2. Determine hook type - When should this run? (10 types: 5 events × pre/post)
- Creating worktree (blocking) →
pre-start - Creating worktree (background) →
post-start - Before/after a switch →
pre-switch/post-switch - Before committing →
pre-commit - After committing (CI, notifications) →
post-commit - Before merging →
pre-merge - After merging →
post-merge - Before/after removal →
pre-remove/post-remove
3. Handle format conversion if needed
Single command to a pipeline of dependent steps:
# Before
pre-start = "npm install"
# After (adding db:migrate, which needs install to finish first)
[[pre-start]]
install = "npm install"
[[pre-start]]
migrate = "npm run db:migrate"For independent commands, a named table runs them concurrently:
[pre-start]
install = "npm install"
env = "cp .env.example .env"4. Preserve existing structure and comments
Validation Before Adding Commands
Before adding hooks, validate:
# Verify command exists
which npm
which cargo
# For npm, verify script exists
npm run lint --dry-run
# For shell commands, check syntax
bash -n -c "if [ true ]; then echo ok; fi"Dangerous patterns — Warn users before creating hooks with:
- Destructive commands:
rm -rf,DROP TABLE - External dependencies:
curl http://... - Privilege escalation:
sudo
Permission Models
User Config: Conservative
- Never edit without consent - Always show proposed change and wait for approval
- Never install tools - Provide commands for users to run themselves
- Preserve structure - Keep existing comments and organization
- Validate first - Ensure TOML is valid before writing
Project Config: Proactive
- Create directly - Changes are versioned, easily reversible
- Validate commands - Check commands exist before adding
- Explain choices - Add comments documenting why hooks exist
- Warn on danger - Flag destructive operations before adding
Common Tasks Reference
User Config Tasks
- Set up commit message generation →
reference/llm-commits.md - Customize worktree paths →
reference/config.md#worktree-path-template - Custom commit templates →
reference/llm-commits.md#prompt-templates - Configure command defaults →
reference/config.md#command-config - Set up personal hooks →
reference/config.md#hooks
Project Config Tasks
- Set up hooks for new project →
reference/hook.md - Add hook to existing config →
reference/hook.md#hook-forms - Use template variables →
reference/hook.md#template-variables - Add dev server URL to list →
reference/config.md#dev-server-url
Aliases & Multi-Worktree Tasks
- Create a
wtalias →reference/extending.md#aliases - Run a command in every worktree →
reference/step.md#wt-step-for-each - Rebase every worktree (up-style) →
reference/extending.md#recipe-rebase-every-worktree-onto-its-upstream - Defer a template variable to a nested
wtcommand →reference/extending.md#deferring-expansion-to-a-nested-wt-command
Key Commands
# View all configuration
wt config show
# Create initial user config (LLM/commit setup: see reference/llm-commits.md)
wt config create
# Full config reference (subcommands, templates, env vars)
wt config --helpLoading Additional Documentation
Load reference files for detailed configuration, hook specifications, and troubleshooting.
Find specific sections with grep:
grep -A 20 "## Setup" reference/llm-commits.md
grep -A 30 "### pre-start" reference/hook.md
grep -A 20 "## Warning Messages" reference/shell-integration.mdHook Approvals in Non-Interactive Sessions
Worktrunk never runs a project's hooks or aliases until the user has explicitly approved them. The commands in .config/wt.toml are arbitrary shell code shipped in a repository the user may have just cloned, so on first run Worktrunk shows each command and waits for the user to approve it — an untrusted .config/wt.toml cannot silently execute anything. Approvals are stored per-project in ~/.config/worktrunk/approvals.toml and re-prompted whenever a command template changes, so a hook can't be swapped for a different command after it was approved.
Agents running wt merge, wt switch, or other commands that trigger hooks will hit an error like:
▲ cargo-difftest needs approval to execute 1 command:
○ post-merge install:
cargo install --path .
✗ Cannot prompt for approval in non-interactive environment
↳ To skip prompts in CI/CD, add --yes; to pre-approve commands, run wt config approvals addThe resolution is for the user to make the trust decision themselves:
- `wt config approvals add` — interactive prompt where the user reviews each command before it is stored to
~/.config/worktrunk/approvals.toml. Run once per project; the approval persists across invocations until the command template changes or the project moves. This is the path to recommend — the user reviews and consents to exactly the commands that will run.
When invoked as an agent, stop and escalate to the user. Approving a project's hooks is a security decision about whether this repository should be trusted to run arbitrary commands on the user's machine — that decision belongs to the user, not the agent. Tell the user to run wt config approvals add and let them review the commands. Do not run --yes on the user's behalf: it skips the approval gate for that invocation, so reaching for it to unblock a command defeats the protection. --yes exists for CI/CD pipelines that already control their own hook contents; it is not a shortcut for an interactive agent to silence an approval prompt.
Advanced: Agent Handoffs
When the user requests spawning a worktree with an agent in a background session ("spawn a worktree for...", "hand off to another agent"), use the appropriate pattern for their terminal multiplexer. Substitute <agent-cli> with the CLI you are running as: claude for Claude Code, 'opencode run' for OpenCode.
tmux (check $TMUX env var):
tmux new-session -d -s <branch-name> "wt switch --create <branch-name> -x <agent-cli> -- '<task description>'"Zellij (check $ZELLIJ env var):
zellij run -- wt switch --create <branch-name> -x <agent-cli> -- '<task description>'Requirements (all must be true):
- User explicitly requests spawning/handoff
- User is in a supported multiplexer (tmux or Zellij)
- The user's project instructions (
CLAUDE.mdorAGENTS.md) or an explicit prompt authorize this pattern
Do not use this pattern for normal worktree operations.
Example (tmux, Claude Code):
tmux new-session -d -s fix-auth-bug "wt switch --create fix-auth-bug -x claude -- \
'The login session expires after 5 minutes. Find the session timeout config and extend it to 24 hours.'"Example (Zellij, OpenCode):
zellij run -- wt switch --create fix-auth-bug -x 'opencode run' -- \
'The login session expires after 5 minutes. Find the session timeout config and extend it to 24 hours.'Parallel sub-Agents (single Claude Code session)
To spawn multiple sub-Agents that each work in their own worktree from one Claude Code session — no terminal multiplexer, no human in the other pane — pre-start each worktree from the parent and pass the path into the sub-Agent prompt:
wt switch --create <branch> --no-cd --no-hooksThen call the Agent tool without isolation: "worktree", naming the path in the prompt:
You are working in `/abs/path/to/worktrunk.<branch>` on branch `<branch>`.
All edits must stay in that worktree.--no-cd skips the shell-integration cd script the parent can't consume; --no-hooks is appropriate when each sub-Agent will run its own build/test step (e.g. cargo run -- hook pre-merge --yes) and you don't need post-start setup repeated per worktree.
Do not use Agent { isolation: "worktree" } for this. Claude Code passes its internal agent ID as name to the WorktreeCreate hook, so wt creates the worktree as worktrunk.agent-<id> on a throwaway branch. If the sub-Agent then creates a feature branch on top, you end up with non-canonical paths, orphan branches, and post-start hooks fired against the wrong branch. Pre-creating with wt switch --create keeps path, branch, and hook target aligned.
Agent Integration
Worktrunk ships a plugin for each supported agent CLI. What a plugin provides depends on the hooks that CLI exposes:
| Capability | Claude Code | Codex | OpenCode | Gemini CLI |
|---|---|---|---|---|
| Configuration skill | ✓ | ✓ | ✓ | |
Activity tracking (🤖/💬 in wt list) | ✓ | ✓ | ✓ | |
| Worktree isolation | ✓ | |||
/wt-switch-create command | ✓ |
The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and /wt-switch-create needs session working-directory switching — both Claude Code-only, so Codex, OpenCode, and Gemini users invoke wt switch --create and wt remove directly. Codex omits activity tracking because its hooks have no turn-end event, so a 🤖 marker could never clear back to 💬.
Installation
Claude Code
wt config plugins claude installManual equivalent:
claude plugin marketplace add max-sixty/worktrunk
claude plugin install worktrunk@worktrunkCodex
wt config plugins codex installThis configures the Worktrunk marketplace in Codex. Then run /plugins in Codex and install Worktrunk from the marketplace. Manual equivalent:
codex plugin marketplace add max-sixty/worktrunkTo remove the marketplace entry, run wt config plugins codex uninstall. Already-installed plugins are left unchanged.
OpenCode
wt config plugins opencode installThis writes the activity-tracking plugin to OpenCode's global plugins directory, ~/.config/opencode/plugins/worktrunk.ts (honoring $OPENCODE_CONFIG_DIR and $XDG_CONFIG_HOME). wt config plugins opencode uninstall removes it.
Gemini CLI
gemini extensions install https://github.com/max-sixty/worktrunkGemini loads the extension natively from the repository, so there is no wt wrapper. gemini extensions uninstall worktrunk removes it.
Configuration skill
With the /worktrunk skill, the agent can help with:
- Setting up LLM-generated commit messages
- Adding project hooks (pre-start, pre-merge, pre-commit)
- Configuring worktree path templates
- Fixing shell integration issues
Claude Code is designed to load the skill automatically when it detects worktrunk-related questions.
Activity tracking
The Claude Code, OpenCode, and Gemini plugins track agent sessions with status markers in wt list:
$ wt list
<b>Branch</b> <b>Status</b> <b>HEAD±</b> <b>main↕</b> <b>Remote⇅</b> <b>Path</b> <b>Commit</b> <b>Age</b> <b>Message</b>
@ main <span class=d>^</span><span class=d>⇡</span> <span class=g>⇡1</span> . <span class=d>33323bc1</span> <span class=d>1d</span> <span class=d>Initial commit</span>
+ feature-api <span class=d>↑</span> 🤖 <span class=g>↑1</span> ../repo.feature-api <span class=d>70343f03</span> <span class=d>1d</span> <span class=d>Add REST API endpoints</span>
+ review-ui <span class=c>?</span> <span class=d>↑</span> 💬 <span class=g>↑1</span> ../repo.review-ui <span class=d>a585d6ed</span> <span class=d>1d</span> <span class=d>Add dashboard component</span>
+ wip-docs <span class=c>?</span> <span class=d>–</span> ../repo.wip-docs <span class=d>33323bc1</span> <span class=d>1d</span> <span class=d>Initial commit</span>
<span class=d>○</span> <span class=d>Showing 4 worktrees, 2 with changes, 2 ahead</span>- 🤖 — agent is working
- 💬 — agent is waiting or idle
The plugin clears the marker when a session ends. A stale marker can remain if the agent process is killed before its session-end hook runs; wt config state marker clear removes a marker manually.
Manual status markers
Set status markers manually for any workflow:
$ wt config state marker set "🚧" # Current branch
$ wt config state marker set "✅" --branch feature # Specific branch
$ git config worktrunk.state.feature.marker '{"marker":"💬","set_at":0}' # DirectWorktree isolation (Claude Code only)
Claude Code agents can run in isolated worktrees (isolation: "worktree"). By default, Claude Code creates these with git worktree add. The plugin's WorktreeCreate and WorktreeRemove hooks route this through wt switch --create and wt remove instead, so worktrees created by agents get worktrunk's naming conventions, hooks, and lifecycle management.
/wt-switch-create command (Claude Code only)
/wt-switch-create [<branch>] [<repo>] [-- <task>] starts a task in a fresh worktree without leaving the session: it creates the worktree, switches into it, and runs the task (all arguments optional). The worktree persists like any other; merge or remove it with wt merge / wt remove.
Statusline (Claude Code only)
wt list statusline --format=claude-code outputs a single-line status for the Claude Code statusline. When the CI status cache is stale, this fetches from the network — typically 1–2 seconds — making it suitable for async statuslines but too slow for synchronous shell prompts. If a faster version would be helpful, please open an issue.
<code>~/w/myproject.feature-auth !🤖 @<span style='color:#0a0'>+42</span> <span style='color:#a00'>-8</span> <span style='color:#0a0'>↑3</span> <span style='color:#0a0'>⇡1</span> <span style='color:#0a0'>#3035</span> Opus 🌔 65% <span style='color:#a70'>1.4×(10am–3pm)</span></code>
When Claude Code provides context window usage via stdin JSON, a moon phase gauge appears (🌕→🌑 as context fills). A yellow <n>×(<window>) segment appears when Claude's 5-hour or weekly rate limit is on track to be hit before reset — 1.4×(10am–3pm) reads as 1.4× the pace that would exactly fill that window. Above 90% used it shows usage instead of pace — 93%(10am–3pm) — near the cap, how much is left matters more than how fast it's going.
Add to ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "wt list statusline --format=claude-code"
}
}wt config
Manage user & project configs. Includes shell integration, hooks, and saved state.
Examples
Install shell integration (required for directory switching):
$ wt config shell installCreate user config file with documented examples:
$ wt config createCreate project config file (.config/wt.toml) for hooks:
$ wt config create --projectShow current configuration and file locations:
$ wt config showConfiguration files
| File | Location | Contains | Committed & shared |
|---|---|---|---|
| User config | ~/.config/worktrunk/config.toml | Worktree path template, LLM commit configs, etc | ✗ |
| Project config | .config/wt.toml | Project hooks, dev server URL | ✓ |
Organizations can deploy a system-wide config file for shared defaults — run wt config show for the platform-specific location.
User config — personal preferences:
# ~/.config/worktrunk/config.toml
worktree-path = ".worktrees/{{ branch | sanitize }}"
[commit.generation]
command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''"Project config — shared team settings:
# .config/wt.toml
[pre-start]
deps = "npm ci"
[pre-merge]
test = "npm test"<!-- USER_CONFIG_START -->
User Configuration
Create with wt config create. Values shown are defaults unless noted otherwise.
Location:
- macOS/Linux:
~/.config/worktrunk/config.toml(or$XDG_CONFIG_HOMEif set) - Windows:
%APPDATA%\worktrunk\config.toml
Worktree path template
Controls where new worktrees are created.
Available template variables:
{{ repo_path }}— absolute path to the repository root (e.g.,/Users/me/code/myproject. Or for bare repos, the bare directory itself){{ repo }}— repository directory name (e.g.,myproject){{ owner }}— primary remote owner path (may include subgroups likegroup/subgroup){{ branch }}— raw branch name (e.g.,feature/auth){{ branch | sanitize }}— filesystem-safe:/and\become-(e.g.,feature-auth){{ branch | sanitize_db }}— database-safe: lowercase, underscores, hash suffix (e.g.,feature_auth_x7k){{ branch | codename(2) }}— deterministic friendly name from a ~1.26M-combo pool (e.g.,malleable-opah)
This is a smaller set than the variables hooks and aliases get.
Examples for repo at ~/code/myproject, branch feature/auth:
Default — sibling directory (~/code/myproject.feature-auth):
worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}"Inside the repository (~/code/myproject/.worktrees/feature-auth):
worktree-path = "{{ repo_path }}/.worktrees/{{ branch | sanitize }}"Friendly branch-derived names (~/code/myproject.malleable-opah):
worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | codename(2) }}"Friendly names with branch identity in a parent directory (~/code/worktrees/feature-auth/malleable-opah):
worktree-path = "{{ repo_path }}/../worktrees/{{ branch | sanitize }}/{{ branch | codename(2) }}"Centralized worktrees directory (~/worktrees/myproject/feature-auth):
worktree-path = "~/worktrees/{{ repo }}/{{ branch | sanitize }}"By remote owner path (~/development/max-sixty/myproject/feature/auth):
worktree-path = "~/development/{{ owner }}/{{ repo }}/{{ branch }}"Bare repository (~/code/myproject/feature-auth):
worktree-path = "{{ repo_path }}/../{{ branch | sanitize }}"~ expands to the home directory. Relative paths resolve from repo_path.
LLM commit messages
Generate commit messages automatically during merge. Requires an external CLI tool.
Claude Code
[commit.generation]
command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''"Codex
[commit.generation]
command = "codex exec -m gpt-5.4-mini -c model_reasoning_effort='low' -c system_prompt='' --sandbox=read-only --json - | jq -sr '[.[] | select(.item.type? == \"agent_message\")] | last.item.text'"OpenCode
[commit.generation]
command = "opencode run -m anthropic/claude-haiku-4.5 --variant fast"llm
[commit.generation]
command = "llm -m claude-haiku-4.5"aichat
[commit.generation]
command = "aichat -m claude:claude-haiku-4.5"See LLM commits docs for setup and Custom prompt templates for template customization.
Command config
List
Persistent flag values for wt list. Override on command line as needed.
[list]
summary = false # Enable LLM branch summaries (requires [commit.generation])
full = false # Show CI, main…± diffstat, and LLM summaries (--full)
branches = false # Include branches without worktrees (--branches)
remotes = false # Include remote-only branches (--remotes)
columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)
task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables
timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disablescolumns selects and orders the columns to render; omit it for the default set. It is designed to be driven by an alias that sets it per invocation — a body like wt --config-set 'list.columns=[…]' list gives a named view (run as wt <alias>) without disturbing the default wt list. Setting it statically in the config file uses the same key and works, but is not the intended use: it pins one layout over a table that otherwise adapts to --full and terminal width.
Valid built-in names are branch, status, working-diff, ahead-behind, branch-diff, summary, upstream, ci, path, url, commit, age, and message. A custom column is named by its [list.custom-columns] header, so a selection mixes built-ins and custom columns in one ordered list (columns = ["branch", "Ticket", "ci"]). When columns is set it is exhaustive — only the listed columns render, so a custom column omitted from a non-empty list is hidden (omit columns entirely to keep the default set, where custom columns append automatically). A built-in name wins over a custom header that collides with it. The gutter type indicator always shows.
Listing a column requests it but does not force it on: a column gated off elsewhere stays hidden — ci needs --full, summary needs [commit.generation] — so columns only narrows which columns may appear.
The selection drives the rendered table and the wt switch picker. wt list --format json ignores it, always emitting every field, built-in and custom.
Custom columns [experimental]
Custom columns add per-branch context to the wt list table. Each [list.custom-columns] entry is a column: the key is the header, the template renders each row's cell.
[list.custom-columns.Ticket]
template = "{{ vars.ticket }}" # Required; the result is the cell text
width = 20 # Optional max display width (default: 40)
priority = 9 # Optional drop order when the terminal narrows;
# lower = kept longer (default: 9, the URL band)Templates may reference {{ branch }}, {{ worktree_path }}, {{ worktree_name }} (empty for branch-only rows), and {{ vars.* }} — per-branch values stored with `wt config state vars set`. All standard filters work (sanitize, hash_port, codename, …). A row where the template renders empty (e.g. a branch without the vars key) shows an empty cell; a column that is empty for every row is dropped from the table. wt list --format json includes the rendered values under columns.
A Note column showing free-form descriptions, set per branch with wt config state vars set note "Bug fix for production fire":
[list.custom-columns.Note]
template = "{{ vars.note }}"Commit
Shared by wt step commit, wt step squash, and wt merge.
[commit]
stage = "all" # What to stage before commit: "all", "tracked", or "none"Merge
Most flags are on by default. Set to false to change default behavior.
[merge]
squash = true # Squash commits into one (--no-squash to preserve history)
commit = true # Commit uncommitted changes first (--no-commit to skip)
rebase = true # Rebase onto target before merge (--no-rebase to skip)
remove = true # Remove worktree after merge (--no-remove to keep)
verify = true # Run project hooks (--no-hooks to skip)
ff = true # Fast-forward merge (--no-ff to create a merge commit instead)Remove
Persistent flag values for wt remove. Override on command line as needed.
[remove]
delete-branch = true # Delete branch after removal (--no-delete-branch to keep)Switch
[switch]
cd = true # Change directory after switching (--no-cd to skip)
[switch.picker]
pager = "delta --paging=never" # Example: override git's core.pager for diff previewStep
[step.copy-ignored]
exclude = [] # Additional excludes (e.g., [".cache/", ".turbo/"])Built-in excludes always apply: VCS metadata directories (.bzr/, .hg/, .jj/, .pijul/, .sl/, .svn/) and tool-state directories (.conductor/, .entire/, .worktrees/). User config and project config exclusions are combined.
Aliases
Command templates that run as wt <name>. See the Extending Worktrunk guide for usage and flags.
[aliases]
greet = "echo Hello from {{ branch }}"
url = "echo http://localhost:{{ branch | hash_port }}"Aliases defined here apply to all projects. For project-specific aliases, use the project config [aliases] section instead.
User project-specific settings
User config can include a [projects] table for project-specific settings — worktree layout, setting overrides, anything else — separate from the project config shared with teammates.
Entries are keyed by project identifier — <host>/<owner>/<repo> derived from the primary remote URL (no .git suffix), or the canonical repo path when there is no remote. Run wt config show inside the repo to see the identifier for the current project; it appears in the PROJECT CONFIG section as Identifier: ….
Scalar values (like worktree-path) replace the global value; everything else (hooks, aliases, etc.) appends, global first.
[projects."github.com/user/repo"]
worktree-path = ".worktrees/{{ branch | sanitize }}"
list.full = true
merge.squash = false
remove.delete-branch = false
pre-start.env = "cp .env.example .env"
step.copy-ignored.exclude = [".repo-local-cache/"]
aliases.deploy = "make deploy BRANCH={{ branch }}"Hooks support all three hook forms. A table runs multiple commands concurrently; an array-of-tables pipeline runs steps in sequence. The dotted-key examples below are equivalent to the table forms — TOML treats projects."github.com/user/repo".post-start.server = "..." and a [projects."github.com/user/repo".post-start] table the same way:
# Single command
[projects."github.com/user/repo"]
post-start = "mise trust"
# Multiple commands, running concurrently
[projects."github.com/user/repo".post-start]
mise = "mise trust"
server = "npm run dev"
# Pipeline: steps run in sequence
[[projects."github.com/user/repo".post-start]]
install = "npm ci"
[[projects."github.com/user/repo".post-start]]
build = "npm run build"
server = "npm run dev"Custom prompt templates
Templates use minijinja syntax.
Commit template
Available variables:
{{ git_diff }},{{ git_diff_stat }}— diff content{{ branch }},{{ repo }}— context{{ recent_commits }}— recent commit messages{{ user_guidance }},{{ project_guidance }}— rendered append fragments (see Appending to the prompt)
Default template:
<!-- DEFAULT_TEMPLATE_START -->
[commit.generation]
template = """
<task>Write a commit message for the staged changes below.</task>
<format>
- Subject line under 50 chars
- For material changes, add a blank line then a body paragraph explaining the change
- Output only the commit message, no quotes or code blocks
</format>
<style>
- Imperative mood: "Add feature" not "Added feature"
- Match recent commit style (conventional commits if used)
- Describe the change, not the intent or benefit
</style>
{% if user_guidance %}
<user-guidance>
{{ user_guidance }}
</user-guidance>
{% endif %}{% if project_guidance %}
<project-guidance>
{{ project_guidance }}
</project-guidance>
{% endif %}
<diffstat>
{{ git_diff_stat }}
</diffstat>
<diff>
{{ git_diff }}
</diff>
<context>
Branch: {{ branch }}
{% if recent_commits %}<recent_commits>
{% for commit in recent_commits %}- {{ commit }}
{% endfor %}</recent_commits>{% endif %}
</context>
"""<!-- DEFAULT_TEMPLATE_END -->
Squash template
Available variables (in addition to commit template variables):
{{ commit_details }}— list of commits being squashed; each renders as its subject and exposes.subject/.body{{ target_branch }}— merge target branch
Default template:
<!-- DEFAULT_SQUASH_TEMPLATE_START -->
[commit.generation]
squash-template = """
<task>Write a commit message for the combined effect of these commits.</task>
<format>
- Subject line under 50 chars
- For material changes, add a blank line then a body paragraph explaining the change
- Output only the commit message, no quotes or code blocks
</format>
<style>
- Imperative mood: "Add feature" not "Added feature"
- Match the style of commits being squashed (conventional commits if used)
- Describe the change, not the intent or benefit
</style>
{% if user_guidance %}
<user-guidance>
{{ user_guidance }}
</user-guidance>
{% endif %}{% if project_guidance %}
<project-guidance>
{{ project_guidance }}
</project-guidance>
{% endif %}
<commits branch="{{ branch }}" target="{{ target_branch }}">
{% for detail in commit_details %}- {{ detail.subject }}
{% endfor %}</commits>
<diffstat>
{{ git_diff_stat }}
</diffstat>
<diff>
{{ git_diff }}
</diff>
"""<!-- DEFAULT_SQUASH_TEMPLATE_END -->
Appending to the prompt [experimental]
template-append adds to the prompt instead of replacing it. The value is rendered as its own minijinja template (same variables) and injected into the default templates' {{ user_guidance }} slot — a <user-guidance> block right after <style>. It applies to both commit and squash. Use it for personal preferences without restating the whole template:
[commit.generation]
template-append = """
- Explain the rationale in the body, not just the change
"""The project config has a template-append of its own; it renders into a separate <project-guidance> block right after <user-guidance>.
Hooks
See `wt hook` for hook types, execution order, template variables, and examples. User hooks apply to all projects; project hooks apply only to that repository. <!-- USER_CONFIG_END --> <!-- PROJECT_CONFIG_START -->
Project Configuration
Project configuration lets teams share repository-specific settings — hooks, dev server URLs, and other defaults. The file lives in .config/wt.toml and is typically checked into version control.
To create a starter file with commented-out examples, run wt config create --project.
Hooks
Project hooks apply to this repository only. See `wt hook` for hook types, execution order, and examples.
pre-start = "npm ci"
post-start = "npm run dev"
pre-merge = "npm test"Dev server URL
URL column in wt list (dimmed when port not listening):
[list]
url = "http://localhost:{{ branch | hash_port }}"Forge platform
Name the forge explicitly for SSH aliases or self-hosted instances, where it can't be detected from the remote URL:
[forge]
platform = "github" # or "gitlab", "gitea" (experimental), "azure-devops" (experimental)
hostname = "github.example.com" # Example: API host (GHE / self-hosted GitLab)Commit-message append [experimental]
Project-wide commit-message conventions appended to the LLM commit and squash prompts inside a <project-guidance> block, after the main template's <style> section (and after any user <user-guidance>). Rendered as a minijinja template with the same variables as the main commit template ({{ branch }}, {{ git_diff }}, etc.), so it can reference them directly. The first time the fragment changes, wt prompts the user to approve it — the same one-shot gate as project-defined hooks.
[commit.generation]
template-append = """
- Use conventional commits (feat:, fix:, docs:, …)
- Reference the relevant issue ID in the body
"""Only template-append is honored from the project file. The LLM command and the main prompt template stay in user config — they describe per-developer environment (which CLI is installed, which agent the developer prefers). User config has a [commit.generation] template-append of its own; it renders into a separate <user-guidance> block immediately before this one.
Copy-ignored excludes
Additional excludes for wt step copy-ignored:
[step.copy-ignored]
exclude = [".cache/", ".turbo/"]Built-in excludes always apply: VCS metadata directories (.bzr/, .hg/, .jj/, .pijul/, .sl/, .svn/) and tool-state directories (.conductor/, .entire/, .worktrees/). User config and project config exclusions are combined.
Aliases
Command templates that run as wt <name>. See the Extending Worktrunk guide for usage and flags.
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
url = "echo http://localhost:{{ branch | hash_port }}"Aliases defined here are shared with teammates. For personal aliases, use the user config [aliases] section instead. <!-- PROJECT_CONFIG_END -->
Shell Integration
Worktrunk needs shell integration to change directories when switching worktrees. Install with:
$ wt config shell installFor manual setup, see wt config shell init --help.
Without shell integration, wt switch prints the target directory but cannot cd into it.
First-run prompts
On first run without shell integration, Worktrunk offers to install it. On first commit without LLM configuration, it offers to configure a detected tool (claude, codex). Declining sets skip-shell-integration-prompt or skip-commit-generation-prompt automatically.
Other
Environment variables
All user config options can be overridden with environment variables using the WORKTRUNK_ prefix.
Naming convention
Config keys use kebab-case (worktree-path), while env vars use SCREAMING_SNAKE_CASE (WORKTRUNK_WORKTREE_PATH). The conversion happens automatically.
For nested config sections, use double underscores to separate levels:
| Config | Environment Variable |
|---|---|
worktree-path | WORKTRUNK_WORKTREE_PATH |
commit.generation.command | WORKTRUNK_COMMIT__GENERATION__COMMAND |
commit.stage | WORKTRUNK_COMMIT__STAGE |
Example: CI/testing override
Override the LLM command in CI to use a mock:
$ WORKTRUNK_COMMIT__GENERATION__COMMAND="echo 'test: automated commit'" wt mergeOther environment variables
| Variable | Purpose |
|---|---|
WORKTRUNK_BIN | Override binary path for shell wrappers; useful for testing dev builds |
WORKTRUNK_CONFIG_PATH | Override user config file location |
WORKTRUNK_SYSTEM_CONFIG_PATH | Override system config file location |
WORKTRUNK_PROJECT_CONFIG_PATH | Override project config file location (defaults to .config/wt.toml) |
XDG_CONFIG_DIRS | Colon-separated system config directories (default: /etc/xdg) |
WORKTRUNK_DIRECTIVE_CD_FILE | Internal: set by shell wrappers. wt writes a raw path; the wrapper cds to it |
WORKTRUNK_DIRECTIVE_EXEC_FILE | Internal: set by shell wrappers. wt writes shell commands; the wrapper sources the file |
WORKTRUNK_SHELL | Internal: set by shell wrappers to indicate shell type (e.g., powershell) |
WORKTRUNK_MAX_CONCURRENT_COMMANDS | Max parallel git commands (default: 32). Lower if hitting file descriptor limits. |
WORKTRUNK_VERBOSE | Verbosity level (0/1/2), like -v/-vv but applied everywhere — including shell completion, which no flag can reach |
RUST_LOG | Logging directive (e.g. worktrunk=debug); overrides the verbosity baseline for what reaches stderr |
NO_COLOR | Disable colored output (standard) |
CLICOLOR_FORCE | Force colored output even when not a TTY |
Inline config overrides (--config-set)
--config-set <toml> overrides any user config key for a single invocation, with higher priority than both config files and WORKTRUNK_ env vars. The value is a TOML fragment, so arrays and tables work directly; the flag is global (works before or after the subcommand), repeatable, and a later --config-set replaces an earlier one for the same key.
$ wt --config-set list.full=true list
$ wt step copy-ignored --config-set 'step.copy-ignored.exclude=["target", "dist"]'This composes with aliases — an alias body can invoke wt --config-set … <command> to render a named view without changing the saved config.
Command reference
wt config - Manage user & project configs
Includes shell integration, hooks, and saved state.
Usage: wt config [OPTIONS] <COMMAND>
Commands:
shell Shell integration setup
create Create configuration file
show Show configuration files & locations
update Update deprecated config settings
approvals Manage command approvals
alias Inspect and preview aliases
plugins Plugin management
state Manage internal data and cache
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptsSubcommands
wt config show
Show configuration files & locations.
Shows location and contents of user config (~/.config/worktrunk/config.toml) and project config (.config/wt.toml). Also shows system config if present.
If a config file doesn't exist, shows defaults that would be used.
Full diagnostics
Use --full to run diagnostic checks:
$ wt config show --fullThis tests:
- CI tool status — Whether
gh(GitHub) orglab(GitLab) is installed and authenticated - Commit generation — Whether the LLM command can generate commit messages
- Version check — Whether a newer version is available on GitHub
Command reference
wt config show - Show configuration files & locations
Usage: wt config show [OPTIONS]
Options:
--full
Run diagnostic checks (CI tools, commit generation, version)
-h, --help
Print help (see a summary with '-h')
Output:
--format <FORMAT>
Output format
[default: text]
[possible values: text, json]
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config approvals
Manage command approvals.
Project hooks and project aliases prompt for approval on first run to prevent untrusted projects from running arbitrary commands. Approvals from both flows are stored together.
Examples
Pre-approve all hook and alias commands for current project:
$ wt config approvals addClear approvals for current project:
$ wt config approvals clearClear global approvals:
$ wt config approvals clear --globalHow approvals work
Approved commands are saved to ~/.config/worktrunk/approvals.toml. Re-approval is required when the command template changes or the project moves. Use --yes to bypass prompts in CI.
Command reference
wt config approvals - Manage command approvals
Usage: wt config approvals [OPTIONS] <COMMAND>
Commands:
add Store approvals in approvals.toml
clear Clear approved commands from approvals.toml
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config alias
Inspect and preview aliases.
Aliases are command templates configured in user (~/.config/worktrunk/config.toml) or project (.config/wt.toml) config and run as wt <name>. See the Extending Worktrunk guide for the configuration format.
Examples
Show every configured alias's template:
$ wt config alias showShow the template for deploy:
$ wt config alias show deployPreview an invocation without running it:
$ wt config alias dry-run deploy
$ wt config alias dry-run deploy -- --env=stagingCommand reference
wt config alias - Inspect and preview aliases
Usage: wt config alias [OPTIONS] <COMMAND>
Commands:
show Show an alias's template, or all aliases' templates
dry-run Preview an alias invocation with template expansion
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state
Manage internal data and cache.
State is stored in .git/ (config entries and log files), separate from configuration files.
Keys
- cache: Regenerable caches — CI status, summaries, git commands, hints, and the `wt switch -` target
- default-branch: The repository's default branch (`main`, `master`, etc.)
- marker: Custom status marker for a branch (shown in `wt list`)
- vars: [experimental] Custom variables per branch
- logs: Operation and debug logs
Examples
Get the default branch:
$ wt config state default-branchSet the default branch manually:
$ wt config state default-branch set mainSet a marker for current branch:
$ wt config state marker set 🚧Store arbitrary data:
$ wt config state vars set env=stagingDrop the regenerable caches:
$ wt config state cache clearShow all stored state:
$ wt config state getClear all stored state:
$ wt config state clearCommand reference
wt config state - Manage internal data and cache
Usage: wt config state [OPTIONS] <COMMAND>
Commands:
get Get all stored state
clear Clear all stored state
cache Regenerable caches
default-branch Default branch detection and override
logs Operation and debug logs
marker Branch markers
vars [experimental] Custom variables per branch
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state cache
Regenerable caches.
View or drop worktrunk's regenerable caches in one place. Everything here is rebuilt on demand — clearing only forces recomputation, never data loss.
What's cached
- CI status — GitHub/GitLab CI per branch (30–60s TTL), shown in `wt list`, plus the largest PR/MR number seen (sizes the CI column)
- Summaries — LLM-generated branch summaries (
wt list --full,wt switchpreview) - Git commands — SHA-keyed disk caches: merge-tree, ancestry, diff-stats, and
wt switchpreview renders - Hints — one-time hints already shown in this repo
- Previous branch — the
wt switch -target, re-recorded on the next switch
cache clear drops all of the above with no prompt. It re-shows one-time hints and forgets the wt switch - target until the next switch — both repopulate on their own.
Without a subcommand, runs get.
Examples
Show cache contents:
$ wt config state cacheDrop all caches:
$ wt config state cache clearCommand reference
wt config state cache - Regenerable caches
Usage: wt config state cache [OPTIONS] [COMMAND]
Commands:
get Show cache contents
clear Drop all caches
Options:
-h, --help
Print help (see a summary with '-h')
Output:
--format <FORMAT>
Output format (text, json) [default: text]
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state default-branch
Default branch detection and override.
Useful in scripts to avoid hardcoding main or master:
$ git rebase $(wt config state default-branch)In a hook or alias template, prefer the {{ default_branch }} template variable; $(wt config state default-branch) is for plain shell scripts.
Without a subcommand, runs get. Use set to override, or clear then get to re-detect.
default-branch get resolves the value and caches it on a miss; the aggregate wt config state get only reports the cache (read-only), so it can show (none) until something populates it.
Detection
Worktrunk detects the default branch automatically:
1. Worktrunk cache — Checks git config worktrunk.default-branch 2. Git cache — Detects primary remote and checks its HEAD (e.g., origin/HEAD) 3. Remote query — If not cached, queries git ls-remote — typically 100ms–2s 4. Local inference — If no remote, infers from local branches
Once detected, the result is cached in worktrunk.default-branch for fast access.
The local inference fallback uses these heuristics in order:
- If only one local branch exists, uses it
- For bare repos or empty repos, checks
symbolic-ref HEAD - Checks
git config init.defaultBranch - Looks for common names:
main,master,develop,trunk
If none of these match, detection fails; set it explicitly with wt config state default-branch set BRANCH.
Command reference
wt config state default-branch - Default branch detection and override
Usage: wt config state default-branch [OPTIONS] [COMMAND]
Commands:
get Get the default branch
set Set the default branch
clear Clear the default branch cache
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state logs
Operation and debug logs.
View and manage log files — hook output, command audit trail, and debug diagnostics.
What's logged
Three kinds of logs live in .git/wt/logs/:
Command log (commands.jsonl)
All hook executions and LLM commands are recorded automatically — one JSON object per line. Rotates to commands.jsonl.old at 1MB (~2MB total). Fields:
| Field | Description |
|---|---|
ts | ISO 8601 timestamp |
wt | The wt command that triggered this (e.g., wt hook pre-merge --yes) |
label | What ran (e.g., pre-merge user:lint, commit.generation) |
cmd | Shell command executed |
exit | Exit code (null for background commands) |
dur_ms | Duration in milliseconds (null for background commands) |
The command log appends entries and is not branch-specific — it records all activity across all worktrees.
Hook output logs
Hook output lives in per-branch subtrees under .git/wt/logs/{branch}/:
| Operation | Log path |
|---|---|
| Background hooks | {branch}/{source}/{hook-type}/{name}.log |
| Background removal | {branch}/internal/remove.log |
All post-* hooks (post-start, post-switch, post-commit, post-merge) run in the background and produce log files. Source is user or project. Branch and hook names are sanitized for filesystem safety (invalid characters → -; short collision-avoidance hash appended). Same operation on same branch overwrites the previous log. Removing a branch clears its subtree; orphans from deleted branches can be swept with wt config state logs clear.
Diagnostic files
| File | Created when |
|---|---|
trace.log | Running with -vv |
subprocess.log | Running with -vv |
diagnostic.md | Running with -vv |
trace.log captures debug-level records at -vv — commands, [wt-trace] records, bounded subprocess previews. subprocess.log holds the raw uncapped subprocess stdout/stderr bodies. diagnostic.md is a markdown bug-report bundle that inlines trace.log; wt prints a gh gist create command pointing at it. All three are overwritten on each -vv run.
Location
All logs are stored in .git/wt/logs/ (in the main worktree's git directory). All worktrees write to the same directory. Top-level files are shared logs (command audit + diagnostics); top-level directories are per-branch log trees.
Structured output
wt config state logs --format=json emits three arrays — command_log, hook_output, diagnostic. Each entry carries a file (relative), path (absolute), size, and modified_at (unix seconds). Hook-output entries additionally expose branch, source (user / project / internal), hook_type (the post-* kind, or null for internal ops), and name. Filter with jq to pick out a specific entry.
Examples
List all log files:
$ wt config state logsQuery the command log:
$ tail -5 .git/wt/logs/commands.jsonl | jq .Path to one hook log (e.g. the post-start server hook for the current branch):
$ wt config state logs --format=json | jq -r '.hook_output[] | select(.source == "user" and .hook_type == "post-start" and (.name | startswith("server"))) | .path'Logs for a specific branch:
$ wt config state logs --format=json | jq '.hook_output[] | select(.branch | startswith("feature"))'Clear all logs:
$ wt config state logs clearCommand reference
wt config state logs - Operation and debug logs
Usage: wt config state logs [OPTIONS] [COMMAND]
Commands:
get List all log file paths
clear Clear all log files
Options:
-h, --help
Print help (see a summary with '-h')
Output:
--format <FORMAT>
Output format (text, json) [default: text]
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state ci-status
CI status cache.
Deprecated — the CI status cache is now part of `wt config state cache`. This subcommand still works but prints a deprecation notice.
Caches GitHub/GitLab CI status for display in `wt list`.
Requires gh (GitHub) or glab (GitLab) CLI, authenticated. Platform auto-detects from the remote URL; set forge.platform = "github" (or "gitlab") in .config/wt.toml for SSH host aliases or self-hosted instances. For GitHub Enterprise or self-hosted GitLab, also set forge.hostname.
Checks open PRs/MRs first, then branch pipelines for branches with upstream. Local-only branches (no remote tracking) show blank.
Results cache for 30-60 seconds. Indicators dim when local changes haven't been pushed.
Status values
| Status | Meaning |
|---|---|
passed | All checks passed |
running | Checks in progress |
failed | Checks failed |
conflicts | PR has merge conflicts |
no-ci | No checks configured |
error | Fetch error (rate limit, network, auth) |
See `wt list` CI status for display symbols and colors.
Without a subcommand, runs get for the current branch. Use clear to reset cache for a branch or clear --all to reset all.
Command reference
wt config state ci-status - CI status cache
Usage: wt config state ci-status [OPTIONS] [COMMAND]
Commands:
get Get CI status for a branch
clear Clear CI status cache
Options:
-h, --help
Print help (see a summary with '-h')
Output:
--format <FORMAT>
Output format (text, json) [default: text]
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state marker
Branch markers.
Custom status text or emoji shown in the wt list Status column.
Display
Markers appear at the end of the Status column, after git symbols:
$ wt list
Branch Status HEAD± main↕ Remote⇅ Commit Age Message
@ main ^⇡ ⇡1 33323bc1 1d Initial commit
+ feature-api ↑ 🤖 ↑1 70343f03 1d Add REST API endpoints
+ review-ui ? ↑ 💬 ↑1 a585d6ed 1d Add dashboard component
+ wip-docs ? – 33323bc1 1d Initial commit
○ Showing 4 worktrees, 2 with changes, 2 ahead, 1 column hiddenUse cases
- Work status —
🚧WIP,✅ready for review,🔥urgent - Agent tracking — The Claude Code plugin sets markers automatically
- Notes — Any short text:
"blocked","needs tests"
Storage
Stored in git config as worktrunk.state.<branch>.marker. Set directly with:
$ git config worktrunk.state.feature.marker '{"marker":"🚧","set_at":0}'Without a subcommand, runs get for the current branch. For --branch, use get --branch=NAME.
Command reference
wt config state marker - Branch markers
Usage: wt config state marker [OPTIONS] [COMMAND]
Commands:
get Get marker for a branch
set Set marker for a branch
clear Clear marker for a branch
Options:
-h, --help
Print help (see a summary with '-h')
Output:
--format <FORMAT>
Output format (text, json) [default: text]
Global Options:
-C <path>
Working directory for this command
--config <path>
User config file path
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptswt config state vars
[experimental]
Custom variables per branch.
Store custom variables per branch. Values are stored as-is — plain strings or JSON.
Examples
Set and get values:
$ wt config state vars set env=staging
$ wt config state vars get envStore JSON:
$ wt config state vars set config='{"port": 3000, "debug": true}'List all keys:
$ wt config state vars listOperate on a different branch:
$ wt config state vars set env=production --branch=mainTemplate access
Variables are available in hook templates as {{ vars.<key> }}. Use the default filter for keys that may not be set:
[post-start]
dev = "ENV={{ vars.env | default('development') }} npm start -- --port {{ vars.port | default('3000') }}"JSON object and array values support dot access:
$ wt config state vars set config='{"port": 3000, "debug": true}'[post-start]
dev = "npm start -- --port {{ vars.config.port }}"Storage format
Stored in git config as worktrunk.state.<branch>.vars.<key>. Keys must contain only letters, digits and hyphens — dots conflict with git config's section separator, underscores with its variable name format.
Command reference
wt config state vars - [experimental] Custom variables per branch
Usage: wt config state vars [OPTIONS] <COMMAND>
Commands:
get Get a value
list List all keys
set Set a value
clear Clear a key or all keys
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptsExtending Worktrunk
Worktrunk has three extension mechanisms.
[Hooks](#hooks) are shell commands that run automatically at lifecycle events (switching, starting, committing, merging, removing). Defined in TOML.
[Aliases](#aliases) are reusable shell commands invoked as wt <name>. Defined in TOML.
[Custom subcommands](#custom-subcommands) are standalone executables invoked as wt <name>. Drop wt-foo on PATH and it becomes wt foo.
| Hooks | Aliases | Custom subcommands | |
|---|---|---|---|
| Trigger | Automatic (lifecycle events) | Manual (wt <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 and aliases live in the same TOML config and share the template engine. User config is trusted; project config requires approval on first run. When both define the same name, both run (user first).
Hooks
Ten hooks cover five lifecycle events:
| Event | pre- (blocking) | post- (background) |
|---|---|---|
| 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.
[pre-start]
deps = "npm ci"
[post-start]
server = "npm run dev -- --port {{ branch | hash_port }}"
[pre-merge]
test = "npm test"See `wt hook` for the full reference and built-in recipes (dev server per worktree, database per worktree, progressive validation). Tips & Patterns has more.
Aliases
Aliases are configured under [aliases]:
[aliases]
deploy = "fly deploy --config=fly.{{ env }}.toml --app=myapp-{{ branch }}"
open = "open http://localhost:{{ branch | hash_port }}"
since-main = "git log --oneline {{ default_branch }}..HEAD"wt deploy --env=staging
wt openwt <name> resolves to a built-in first, then an alias, then a custom subcommand.
Templates
Aliases use the same template engine as hooks: variables, filters, functions, and `--KEY=VALUE` smart routing (bind if the template references KEY, else forward to {{ args }}). For example, wt deploy --env=staging sets {{ env }}.
Alias templates add {{ args }} for positional CLI arguments. Operation-context variables (target, base, pr_number) aren't auto-populated, but can still be bound with --KEY=VALUE.
Positional arguments
{{ args }} renders as a space-joined, shell-escaped string, ready to splice into a command:
[aliases]
s = "wt switch {{ args }}"wt s some-branch
wt s feature/api
wt s 'has a space'For indexing ({{ args[0] }}), looping, and counting, see Passing values.
Tokens after -- forward unconditionally, bypassing any binding. Writing wt deploy -- --branch=foo forwards the literal --branch=foo to {{ args }} even though the template references {{ branch }}.
An alias that forwards {{ args }} to a wt command — like co = "wt switch {{ args }}" or cm = "wt step commit {{ args }}" — inherits that command's argument and flag completion, so wt co <Tab> completes branches the same way wt switch <Tab> does.
Inspecting and previewing
wt config alias show <name>prints the template.wt config alias dry-run <name> [-- args...]prints the rendered command.
wt config alias show deploy
wt config alias dry-run deploy
wt config alias dry-run deploy -- --env=stagingMulti-step pipelines
[[aliases.NAME]] defines a pipeline using the [same [[block]] semantics as hooks](https://worktrunk.dev/hook/#hook-forms): blocks run in order, keys within a block run concurrently, and a step failure aborts the remainder.
[[aliases.release]]
test = "cargo test"
[[aliases.release]]
build = "cargo build --release"
package = "cargo package --no-verify"
[[aliases.release]]
publish = "cargo publish {{ args }}"Every step sees the same {{ args }} and bound variables. wt release -- --dry-run forwards --dry-run to publish without affecting earlier steps.
Changing directory
wt switch, wt merge (when it leaves the removed source), and wt remove of the current worktree change the parent shell's directory even when invoked from an alias; the Worktrunk shell integration propagates the change through. Other shell state doesn't persist: the alias runs in a subshell, so cd, export, and similar commands only affect that subshell.
Deferring expansion to a nested wt command
A wt step for-each alias that prints the same branch in every worktree is rendering {{ branch }} too early. An alias body renders once at dispatch, in the invoking worktree, so a bare {{ branch }} is baked to that worktree's branch before for-each iterates. (wt config alias dry-run <name> shows the rendered body, with the value already baked in.)
{% raw %}…{% endraw %} defers the variable: it survives the dispatch render as a literal {{ branch }}, and for-each expands it per worktree. One catch for for-each: the deferred {{ branch }} has spaces, so the alias body's sh -c splits it into {{, branch, }} before for-each sees it (Failed to expand for-each argument: syntax error). Give for-each its own sh -c '…' to keep the value one token:
[aliases]
show-branches = "wt step for-each -- sh -c 'echo {% raw %}{{ branch }}{% endraw %}'"wt show-branches prints each worktree's own branch.
wt switch --execute defers the same way, without the extra wrapper: its --execute '…' argument is already a single quoted string, so only {% raw %} is needed. Here {{ worktree_path }} expands against the worktree being created, not the one the alias ran from:
[aliases]
echo-target = "wt switch {{ args }} --no-cd --execute 'echo {% raw %}{{ worktree_path }}{% endraw %}'"A repo-level variable like {{ default_branch }} needs no deferral: it is identical in every worktree, so a bare {{ default_branch }} is already correct everywhere.
Recipe: rebase every worktree onto its upstream
[aliases]
up = '''
git fetch --all --prune && wt step for-each -- sh -c '
git rev-parse --verify -q @{u} >/dev/null || exit 0
g=$(git rev-parse --git-dir)
test -d "$g/rebase-merge" -o -d "$g/rebase-apply" && exit 0
git update-index --refresh -q >/dev/null || true
git rebase @{u} --no-autostash || git rebase --abort
''''wt up fetches all remotes, then iterates every worktree: skip if no upstream, skip if mid-rebase, refresh the index to drop stale stat entries, then rebase and auto-abort on conflict. It rebases onto git-native @{u} rather than a {{ … }} template, so git resolves each worktree's own upstream and there is nothing to defer.
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, pair it with git stash:
# .config/wt.toml
[aliases]
move-changes = '''
if git diff --quiet HEAD && test -z "$(git ls-files --others --exclude-standard)"; then
wt switch --create {{ to }} --execute="{{ args }}"
else
git stash push --include-untracked --quiet
wt switch --create {{ to }} --execute="git stash pop --index; {{ args }}"
fi
'''Run with wt move-changes --to=feature-xyz. The guard skips the stash when nothing is in flight; otherwise git stash push captures everything and --execute pops it in the new worktree with the staged/unstaged split intact. Anything after -- runs in the new worktree after pop. For example, wt move-changes --to=feature-xyz -- claude opens Claude there.
To copy instead of move, add git stash apply --index --quiet right after the push.
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:
[aliases]
hook-log = '''
tail -f "$(wt config state logs --format=json | jq -r --arg name "{{ name | sanitize_hash }}" --arg kind "{{ kind }}" '
.hook_output[]
| select(.branch == "{{ branch | sanitize_hash }}" and .hook_type == $kind and .name == $name)
| .path
' | head -1)"
'''Run with wt hook-log --kind=post-start --name=server to tail the log for the server hook on the current branch. --kind picks the hook type; the branch is pulled from the current worktree via {{ branch }}. sanitize_hash rewrites branch and name to filesystem-safe forms with a hash suffix that keeps distinct originals unique (the same transformation Worktrunk applies on disk), so the alias resolves the right log even when either contains characters like /.
Custom subcommands
[experimental]
Any executable named wt-<name> on PATH becomes available as wt <name>, the same pattern git uses for git-foo. Built-in commands and aliases take precedence.
wt sync origin # runs: wt-sync origin
wt -C /tmp/repo sync # -C is forwarded as the child's working directoryArguments pass through verbatim, stdio is inherited, and the child's exit code propagates unchanged.
Examples
- `worktrunk-sync`: rebases stacked worktree branches in the dependency order inferred from git history. Install with
cargo install worktrunk-sync, then run aswt sync.
Reference: hooks vs. aliases
Aside from the differences below, hooks and aliases behave the same.
<details> <summary>Interface differences</summary>
| Axis | Hooks | Aliases |
|---|---|---|
| Invocation | wt hook <type> [args...] (nested under the hook built-in) | wt <name> [args...] (top-level) |
| Bare positionals | Filter names (wt hook pre-merge test build runs only test and build) | Forwarded to {{ args }} |
Reach {{ args }} from positionals | Must use -- (wt hook pre-merge -- extra) | Any bare positional lands there |
| Approval skip flag | Post-subcommand --yes / -y supported (wt hook pre-merge --yes) | Only the global form (wt -y <alias>); post-alias --yes falls through to {{ args }} |
| Source discrimination | user: / project: / user:name / project:name filter syntax | Run user first, then project; no filter syntax |
| Force-bind escape | --var KEY=VALUE (deprecated in favor of --KEY=VALUE, but still force-binds) | None; smart routing is the only path |
--help | wt hook --help lists hook types; wt hook <type> --help shows flags and arguments for that type | The template body is the documentation: wt <alias> --help redirects to wt config alias show / dry-run. wt --help and wt step --help list configured aliases alongside built-in commands |
| Inspection | wt hook show [type] [--expanded] | wt config alias show <name> / wt config alias dry-run <name> |
| Stdin | All template variables as JSON (parse with json.load(sys.stdin)) | Inherits parent stdin (pipes pass through; interactive TUIs like wt switch keep the tty) |
| Template-context extras | hook_type, hook_name, per-type operation vars (base, target, pr_number, …) | args on top of the shared base variables |
</details>
FAQ
How does Worktrunk compare to alternatives?
vs. branch switching
Branch switching uses one directory: uncommitted changes from one agent get mixed with the next agent's work, or block switching entirely. Worktrees give each agent its own directory with independent files and index.
vs. Plain git worktree
Git's built-in worktree commands work but require manual lifecycle management:
# Plain git worktree workflow
$ git worktree add -b feature-branch ../myapp-feature main
$ cd ../myapp-feature
# ...work, commit, push...
$ cd ../myapp
$ git merge feature-branch
$ git worktree remove ../myapp-feature
$ git branch -d feature-branchWorktrunk automates the full lifecycle:
$ wt switch --create feature-branch # Creates worktree, runs setup hooks
# ...work...
$ wt merge # Merges into default branch, cleans upNo cd back to main — wt merge runs from the feature worktree and merges into the target, like GitHub's merge button.
What git worktree doesn't provide:
- Consistent directory naming and cleanup validation
- Project-specific automation (install dependencies, start services)
- Unified status across all worktrees (commits, CI, conflicts, changes)
vs. git-machete / git-town
Different scopes:
- git-machete: Branch stack management in a single directory
- git-town: Git workflow automation in a single directory
- worktrunk: Multi-worktree management with hooks and status aggregation
These tools can be used together—run git-machete or git-town inside individual worktrees.
vs. Git TUIs (lazygit, gh-dash, etc.)
Git TUIs operate on a single repository. Worktrunk manages multiple worktrees, runs automation hooks, and aggregates status across branches. TUIs work inside each worktree directory.
Does Worktrunk support stacked branches?
Not natively — stacked-branch workflows are a large design space, so Worktrunk treats them as an extension rather than a built-in. `worktrunk-sync` is a community tool that auto-detects the branch dependency tree from git history and rebases each branch onto its parent in topological order. Install with cargo install worktrunk-sync and run as wt sync (via custom subcommands).
How do I move uncommitted changes to a new worktree?
Stash the changes, create the worktree, then pop:
$ git stash push -u # -u also stashes untracked files
$ wt switch --create feature # new branch off the default branch
$ git stash pop # changes reappear in the new worktreeThe stash lives in the shared .git directory, so it's reachable from the new worktree. The original branch is left clean.
wt switch --create bases the new branch on the default branch. To base it on the current commit instead, pass --base=@ (needed when the current branch has commits beyond the default branch).
There's an issue with my shell setup
If shell integration isn't working (auto-cd not happening, completions missing, wt not found as a function), the fastest path to a fix is using Claude Code with the Worktrunk plugin:
1. Install the Worktrunk plugin in Claude Code 2. Ask Claude to debug the Worktrunk shell integration
Claude will run wt config show, inspect the shell config files, and identify the issue.
If Claude can't fix it, please open an issue with the output of wt config show, the shell (bash/zsh/fish), and OS. (And even if it fixes the problem, feel free to open an issue: non-standard success cases are useful for ensuring Worktrunk is easy to set up for others.)
What does -v / -vv do?
Three verbosity levels. Each is a superset of the previous one.
| Level | Stderr | Files (.git/wt/logs/) | Use case |
|---|---|---|---|
| (none) | Warnings only | — | Normal use |
-v | + Info: hook output, alias template variable resolution | — | Debugging hooks/aliases |
-vv | Same as -v | + trace.log, subprocess.log, diagnostic.md | Filing a bug |
At -vv, debug-level records ($ cmd headers, [wt-trace] timing, bounded subprocess preview) route to trace.log instead of stderr — so the terminal stays readable while the deep trace lands on disk. A one-line pointer on stderr shows where the files went.
The three -vv files have distinct audiences:
- `trace.log` — bounded preview (~1K lines),
[wt-trace]records, gistable. - `subprocess.log` — raw uncapped stdout/stderr of every subprocess
wtspawns (multi-MB possible, e.g. fullgit log -poutput). The deep-dive escape hatch. - `diagnostic.md` — markdown bug-report bundle that inlines
trace.log.wtprints agh gist createcommand pointing at it.
RUST_LOG overrides the flag baseline when set (RUST_LOG=debug wt -v lifts -v to debug-on-stderr).
The flags only reach a command you type; shell completion runs as its own process with nowhere to pass one. Set WORKTRUNK_VERBOSE=0|1|2 to apply the level to every invocation, completion included — it's the env-var equivalent of -v/-vv, so level 2 writes the same trace.log/subprocess.log/diagnostic.md files. An explicit -v/-vv on a command raises the level further but never lowers this baseline. To profile a slow tab-completion, run it the way your shell does — e.g. WORKTRUNK_VERBOSE=2 COMPLETE=fish wt -- wt switch '' — then read trace.log.
What files does Worktrunk create?
1. Worktree directories
Created by wt switch <branch> when switching to a branch that doesn't have a worktree. Use wt switch --create <branch> to create a new branch. Default location is ../<repo>.<branch> (sibling to main repo), configurable via worktree-path in user config.
To remove: wt remove <branch> removes the worktree directory and deletes the branch.
2. Config files
| File | Created by | Purpose |
|---|---|---|
~/.config/worktrunk/config.toml | wt config create | User preferences |
~/.config/worktrunk/approvals.toml | Approving project commands | Approved hook and alias commands |
.config/wt.toml | wt config create --project | Project hooks (checked into repo) |
User config location: $XDG_CONFIG_HOME/worktrunk/ (or ~/.config/worktrunk/) on Linux/macOS, %APPDATA%\worktrunk\ on Windows.
To remove: Delete directly. User config: rm ~/.config/worktrunk/config.toml. Project config: rm .config/wt.toml (and commit).
3. Shell integration
Created by wt config shell install:
- Bash: adds line to
~/.bashrc - Zsh: adds line to
~/.zshrc(or$ZDOTDIR/.zshrc) - Fish: creates
~/.config/fish/functions/wt.fishand~/.config/fish/completions/wt.fish - Nushell [experimental]: creates
wt.nuin Nushell's user vendor-autoload directory — the last entry of$nu.vendor-autoload-dirs, under$nu.data-dir(typically~/.local/share/nushell/vendor/autoloadon Linux,~/Library/Application Support/nushell/vendor/autoloadon macOS) - PowerShell (Windows): creates both profile files if they don't exist:
Documents/PowerShell/Microsoft.PowerShell_profile.ps1(PowerShell 7+)Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1(Windows PowerShell 5.1)
PowerShell detection on Windows: When running from cmd.exe or PowerShell, both PowerShell profile files are created automatically. When running from Git Bash or MSYS2, PowerShell is skipped (use wt config shell install powershell to create the profiles explicitly).
To remove: wt config shell uninstall.
4. Metadata in .git/ (automatic)
Worktrunk stores small amounts of cache and log data in the repository's .git/ directory:
| Location | Purpose | Created by |
|---|---|---|
git config worktrunk.* | Cached default branch, switch history, branch markers, custom variables | Various commands |
.git/wt/cache/{kind}/*.json | Cached CI status, the largest PR/MR number seen (sizes the wt list CI column), and git command results (merge-tree, integration probes, diff stats, ancestry checks, ahead/behind counts) | wt list, wt merge, wt remove |
.git/wt/cache/summary/{branch}/{hash}.json | Cached LLM branch summaries, content-addressed by diff hash | wt list --full, wt switch (when [list] summary = true) |
.git/wt/logs/{branch}/**/*.log | Background hook output (nested per branch) | Hooks, background wt remove |
.git/wt/logs/commands.jsonl | Command audit log (~2MB max) | Hooks, LLM commands |
.git/wt/logs/trace.log | Debug log for issue reporting | Running with -vv |
.git/wt/logs/subprocess.log | Raw uncapped subprocess stdout/stderr (may be multi-MB) | Running with -vv |
.git/wt/logs/diagnostic.md | Diagnostic report for issue reporting | Running with -vv |
.git/wt/trash/<name>-<timestamp> | Staged worktree contents pending background deletion | wt remove |
None of this is tracked by git or pushed to remotes.
To remove: wt config state clear removes all worktrunk data — config keys, caches, markers, hints, variables, logs, and stale trash.
What Worktrunk does NOT create
- No files outside
.git/, config directories, or worktree directories - No global git hooks
- No modifications to
~/.gitconfig - No long-running background processes or daemons
What can Worktrunk delete?
Worktrunk can delete worktrees and branches. Both have safeguards.
Worktree removal
wt remove mirrors git worktree remove: it refuses to remove worktrees with uncommitted changes (staged, modified, or untracked files). The --force flag removes the worktree anyway, discarding all of those changes.
For worktrees containing precious ignored data (databases, caches, large assets), use git worktree lock:
git worktree lock ../myproject.feature --reason "Contains local database"Locked worktrees show ⊞ in wt list. Neither git worktree remove nor wt remove (even with --force) will delete them. Unlock with git worktree unlock.
Branch deletion
By default, wt remove only deletes branches whose content is already in the default branch. Branches showing _ (same commit) or ⊂ (integrated) in wt list are safe to delete.
For the full algorithm, see Branch cleanup — it handles squash-merge and rebase workflows where commit history differs but file changes match.
Use -D to force-delete branches with unmerged changes. Use --no-delete-branch to keep the branch regardless of status.
Other cleanup
wt remove— in addition to the target worktree, runs a background internal sweep: deletes.git/wt/trash/entries older than 24 hours (eventual cleanup for directories orphaned when a previous background removal was interrupted), and terminates (SIGTERMthenSIGKILL)git fsmonitor--daemonprocesses whose worktree no longer existswt remove— terminates the removed worktree'sgit fsmonitor--daemonprocess. Git starts this per-worktree filesystem-watch daemon whencore.fsmonitor=true; once its worktree is gone it would leak. Removal sendsgit fsmonitor--daemon stop, then resolves that daemon's PID from its IPC socket and force-terminates it (SIGTERM, then SIGKILL) if it didn't exit. The signal only ever targets the daemon whose socket resolves to the worktree being removed.wt config state clear— removes all worktrunk data from.git/(config keys, caches, markers, hints, variables, logs, stale trash)wt config shell install— when migrating an integration to a new location, removes the worktrunk-managed file left at the old one: fishconf.d/wt.fish(nowfunctions/wt.fish) and nushell wrappers stranded under<config-dir>/vendor/autoload(now<data-dir>/vendor/autoload)wt config shell uninstall— removes shell integration from rc files
See What files does Worktrunk create? for details.
What commands does Worktrunk execute?
Worktrunk runs git commands internally and optionally runs gh (GitHub) or glab (GitLab) for CI status. Beyond that, user-defined commands execute in four contexts:
1. User hooks (~/.config/worktrunk/config.toml) — Personal automation for all repositories 2. Project hooks (.config/wt.toml) — Repository-specific automation 3. LLM commands (~/.config/worktrunk/config.toml) — Commit message generation and branch summaries 4. --execute flag — Explicitly provided commands
User hooks and user aliases don't require approval (you defined them). Commands from project hooks and project aliases require approval on first run. Approved commands are saved to the approvals file (approvals.toml). If a command changes, Worktrunk requires new approval.
Example approval prompt
▲ repo needs approval to execute 3 commands:
○ pre-start install: npm ci ○ pre-start build: cargo build --release ○ pre-start env: echo 'PORT={{ branch | hash_port }}' > .env.local
❯ Allow and remember? [y/N]
Use --yes to bypass prompts (useful for CI/automation).
Command log
All hook executions and LLM commands are recorded in .git/wt/logs/commands.jsonl — one JSON object per line. Fields: ts (timestamp), wt (the wt command that triggered it), label (what ran, e.g., pre-merge user:lint), cmd (shell command), exit (exit code, null for background), dur_ms (duration, null for background). The file rotates to commands.jsonl.old at 1MB, bounding storage to ~2MB.
View the log with wt config state logs get, or query directly:
# Recent commands
$ tail -5 .git/wt/logs/commands.jsonl | jq .
# Failed commands
$ jq 'select(.exit != 0 and .exit != null)' .git/wt/logs/commands.jsonlClear with wt config state logs clear.
Does Worktrunk work on Windows?
Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See installation for setup details, including avoiding the Windows Terminal wt conflict.
Git for Windows required — Hooks use bash syntax and execute via Git Bash, so Git for Windows must be installed even when PowerShell is the interactive shell.
`wt switch` interactive picker unavailable — Uses skim, which doesn't support Windows. Use wt list and wt switch <branch> instead.
How does Worktrunk determine the default branch?
Worktrunk checks the local git cache first, queries the remote if needed, and falls back to local inference when no remote exists.
If the remote's default branch has changed (e.g., renamed from master to main), clear the cache with wt config state default-branch clear.
For full details on the detection mechanism, see wt config state default-branch --help.
My for-each or --execute alias prints the same value in every worktree
An alias body renders once at dispatch, in the invoking worktree's context, so a per-worktree variable like {{ branch }} is baked to that one worktree's value before the nested wt command iterates. Every worktree then sees the same value.
Confirm it with wt config alias dry-run <name>: if the value is already substituted (e.g. … echo branch=main), it was baked at dispatch.
To defer a variable to the nested command, wrap it as {% raw %}{{ branch }}{% endraw %}; for wt step for-each, also keep it inside a quoted sh -c '…' so the alias's shell doesn't word-split it. See deferring expansion in an alias. A repo-level variable like {{ default_branch }} is unaffected — it is identical in every worktree.
Installation fails with C compilation errors
Errors related to tree-sitter or C compilation (C99 mode, le16toh undefined) can be avoided by installing without syntax highlighting:
cargo install worktrunk --no-default-features --features cliThis disables bash syntax highlighting in command output but keeps all core functionality. The syntax highlighting feature requires C99 compiler support and can fail on older systems or minimal Docker images.
Running tests (for contributors)
Quick tests
cargo testFull integration tests
Shell integration tests require bash, zsh, fish, and nushell:
cargo test --test integration --features shell-integration-testsHow can I contribute?
- Star the repo
- Try it out and open an issue with feedback — even small annoyances
- What worktree friction does Worktrunk not yet solve? Tell us
- Send to a friend
- Post about it on X, Reddit, or LinkedIn
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 and project hooks are supported.
Hook Types
| Event | pre- — blocking | post- — background |
|---|---|---|
| switch | pre-switch | post-switch |
| create | 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 with output logged (use `wt config state logs` to find and manage log files). Use -v to see the template variables for background hooks; wt hook <type> --dry-run previews the commands.
The most common creation hook is post-start — it runs background tasks (dev servers, file copying, builds) without blocking worktree creation. Prefer post-start over pre-start unless a later step needs the work completed first.
| Hook | Purpose |
|---|---|
pre-switch | Runs before branch resolution or worktree creation. {{ branch }} is the destination as typed (before resolution) |
post-switch | Triggers on all switch results: creating, switching to existing, or staying on current |
pre-start | Runs once when a new worktree is created, blocking post-start/--execute until complete: dependency install, env file generation |
post-start | Runs once when a new worktree is created, in the background: dev servers, long builds, file watchers, copying caches |
pre-commit | Formatters, linters, type checking — runs during wt merge before the squash commit |
post-commit | CI triggers, notifications, background linting |
pre-merge | Tests, security scans, build verification — runs after rebase, before merge to target |
post-merge | Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree |
pre-remove | Cleanup before worktree deletion: saving test artifacts, backing up state. Runs in the worktree being removed |
post-remove | Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree |
During wt merge, hooks run in this order: pre-commit → post-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:
○ pre-start install:
npm ci
○ pre-start build:
cargo build --release
○ pre-start env:
echo 'PORT={{ branch | hash_port }}' > .env.local
❯ Allow and remember? [y/N]- Approvals are saved to
~/.config/worktrunk/approvals.toml - If a command changes, new approval is required
- Declining skips every project command for that operation — including any already approved — and continues without them; saved approvals are unaffected
- Use
--yesto bypass prompts — useful for CI and automation - Use
--no-hooksto skip hooks
Manage approvals with wt config approvals add and wt config approvals clear.
Configuration
Hooks can be defined in project config (.config/wt.toml) or user config (~/.config/worktrunk/config.toml). Both use the same format. The project config is read from the worktree the command ran in.
Hook forms
Hooks take one of three forms, determined by their TOML shape.
A string is a single command:
pre-start = "npm install"A table is multiple commands that run concurrently:
[post-start]
server = "npm run dev"
watch = "npm run watch"A pipeline is a sequence of [[hook]] blocks run in order. Each block is one step; multiple keys within a block run concurrently. A failing step aborts the rest of the pipeline:
[[post-start]]
install = "npm ci"
[[post-start]]
build = "npm run build"
server = "npm run dev"Here install runs first, then build and server run together.
Templates are syntax-checked before the pipeline starts and rendered as each step runs, so a step can store per-branch vars that later steps read via {{ vars.<key> }}.
Most hooks don't need [[hook]] blocks. Reach for them when there's a dependency chain — typically setup that must complete before later steps, like installing dependencies before running a build and dev server concurrently.
Project vs user 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 | First |
Skip all hooks with --no-hooks. To run a specific hook when user and project both define the same name, use user:name or project:name syntax.
Template variables
Hooks can use template variables that expand at runtime:
| Kind | Variable | Description |
|---|---|---|
| active | {{ branch }} | Branch name |
{{ worktree_path }} | Worktree path | |
{{ worktree_name }} | Worktree directory name | |
{{ commit }} | Branch HEAD SHA | |
{{ short_commit }} | Branch HEAD SHA, abbreviated per core.abbrev | |
{{ upstream }} | Branch upstream (if tracking a remote) | |
| operation | {{ base }} | Base branch name (switch/create only) |
{{ base_worktree_path }} | Base worktree path | |
{{ target }} | Target branch name | |
{{ target_worktree_path }} | Target worktree path (when target has a worktree) | |
{{ pr_number }} | PR/MR number (post-switch, pre-start, post-start; when creating via pr:N / mr:N) | |
{{ pr_url }} | PR/MR web URL (post-switch, pre-start, post-start; when creating via pr:N / mr:N) | |
| repo | {{ repo }} | Repository directory name |
{{ repo_path }} | Absolute path to repository root | |
{{ owner }} | Primary remote owner path (may include subgroups) | |
{{ primary_worktree_path }} | Primary worktree path | |
{{ default_branch }} | Default branch name | |
{{ remote }} | Primary remote name | |
{{ remote_url }} | Remote URL | |
| exec | {{ cwd }} | Directory where the hook command runs |
{{ hook_type }} | Hook type being run (e.g. pre-start, pre-merge) | |
{{ hook_name }} | Hook command name (if named) | |
{{ args }} | Tokens forwarded from the CLI — see Running Hooks Manually | |
| user | {{ vars.<key> }} | Per-branch variables from `wt config state vars` |
The repo variables (repo, repo_path, owner, primary_worktree_path, default_branch, remote, remote_url) are constant across the whole repository — default_branch is the same in every worktree. The active variables (branch, worktree_path, worktree_name, commit, short_commit, upstream) vary per worktree.
Bare variables (branch, worktree_path, commit) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. base and target give the other side:
| Operation | Bare vars | base | target |
|---|---|---|---|
| switch/create | destination | where you came from | = bare vars |
| commit (during merge/squash) | worktree being squashed | = bare vars | integration target |
| merge | feature being merged | = bare vars | merge target |
| remove | branch being removed | = bare vars | where you end up |
All hooks share the same perspective — {{ branch | hash_port }} produces the same port in post-start and post-remove.
cwd is the worktree root where the hook command runs. It equals worktree_path except in three cases:
pre-switch: hook runs in the source worktree;worktree_pathis the destinationpost-removeandpost-mergewith removal: the active worktree is gone, so the hook runs in primary or target, respectively
Undefined variables error — use conditionals or defaults for optional behavior:
[pre-start]
# Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature)
sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}"Run any hook-firing command with -v to see the resolved variables for the actual invocation — each hook prints a template variables: block showing every in-scope variable and its value ((unset) for conditional vars that didn't populate, like target_worktree_path during wt switch -). Aliases do the same under -v: wt -v <alias> prints the alias's in-scope variables before the pipeline runs.
Variables use dot access and the default filter for missing keys. JSON object/array values are parsed automatically, so {{ vars.config.port }} works when the value is {"port": 3000}:
[post-start]
dev = "ENV={{ vars.env | default('development') }} npm start -- --port {{ vars.config.port | default('3000') }}"Worktrunk filters
Templates support Jinja2 filters for transforming values:
| Filter | Example | Description |
|---|---|---|
sanitize | `{{ branch \ | sanitize }}` |
sanitize_db | `{{ branch \ | sanitize_db }}` |
sanitize_hash | `{{ branch \ | sanitize_hash }}` |
hash | `{{ branch \ | hash }}` |
hash_port | `{{ branch \ | hash_port }}` |
dirname | `{{ repo_path \ | dirname }}` |
basename | `{{ repo_path \ | basename }}` |
codename(n) | `{{ branch \ | codename(2) }}` |
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 sanitize_hash filter produces a filesystem-safe name and appends a 3-character hash suffix when sanitization changed the input, so distinct originals never collide — already-safe names pass through unchanged. The codename(n) filter produces deterministic friendly names from an input string: codename(1) returns a noun, codename(2) returns adjective-noun, and higher counts add more adjectives. The pool is large (~1.26M combinations for codename(2)), so it usually stands alone as a worktree leaf:
# Friendly branch-derived worktree names, e.g. myproject.malleable-opah
worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | codename(2) }}"When you want both a friendly name and the original branch identity in the path, put the branch name in a parent directory:
worktree-path = "{{ repo_path }}/../worktrees/{{ branch | sanitize }}/{{ branch | codename(2) }}"The hash filter is the bare 3-character base36 digest, useful for composing your own truncate-with-collision-avoidance recipes when an output budget is tight (e.g., Unix socket paths capped at 107 bytes):
# Truncated branch slug + hash: collisions remain disambiguated even when prefixes match
worktree-path = "/tmp/{{ (branch | sanitize)[:20] }}_{{ branch | sanitize | hash }}"The dirname and basename filters traverse paths. They're useful for bare repos in a hidden directory like myproject/.git, where {{ repo }} resolves to .git:
# Place worktrees as siblings of the bare repo, named `<wrapper>.<branch>`
worktree-path = "{{ repo_path }}/../{{ repo_path | dirname | basename }}.{{ branch | sanitize }}"The hash_port filter is useful for running dev servers on unique ports per worktree:
[post-start]
dev = "npm run dev -- --host {{ branch }}.localhost --port {{ branch | hash_port }}"Hash any string, including concatenations:
# Unique port per repo+branch combination
dev = "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:
[pre-start]
# Copy config from main worktree
setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}"JSON context
Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express:
[pre-start]
setup = "python3 scripts/pre-start-setup.py"import json, sys, subprocess
ctx = json.load(sys.stdin)
if ctx['branch'].startswith('feature/') and 'backend' in ctx['repo']:
subprocess.run(['make', 'seed-db'])Copying untracked files
One specific command worth calling out: `wt step copy-ignored`. Git worktrees share the repository but not untracked files, and this copies gitignored files between worktrees:
[post-start]
copy = "wt step copy-ignored"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.
$ wt hook pre-merge # Run all pre-merge hooks
$ wt hook pre-merge test # Run hooks named "test" from both sources
$ wt hook pre-merge test build # Run hooks named "test" and "build"
$ wt hook pre-merge user: # Run all user hooks
$ wt hook pre-merge project: # Run all project hooks
$ wt hook pre-merge user:test # Run only user's "test" hook
$ wt hook pre-merge --yes # Skip approval prompts (for CI)
$ wt hook pre-start --branch=feature/test # Override a template variable
$ wt hook pre-merge -- --extra args # Forward tokens into {{ args }}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.
$ wt hook pre-merge
◎ Running pre-merge project:test
cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.12s
Running unittests src/lib.rs (target/debug/deps/worktrunk-abc123)
running 18 tests
test auth::tests::test_jwt_decode ... ok
test auth::tests::test_jwt_encode ... ok
test auth::tests::test_token_refresh ... ok
test auth::tests::test_token_validation ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
◎ Running pre-merge project:lint
cargo clippy
Checking worktrunk v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 1.23s$ wt hook post-start
◎ Running post-start: project @ ~/acmePassing values
--KEY=VALUE binds KEY whenever {{ KEY }} appears in any command of the hook — the same smart-routing rule wt <alias> uses. Built-in variables can be overridden: --branch=foo sets {{ branch }} inside hook templates (the worktree's actual branch doesn't move). Hyphens in keys become underscores: --my-var=x sets {{ my_var }}.
Any --KEY=VALUE whose key isn't referenced by a hook template forwards into {{ args }} as a literal --KEY=VALUE token. Tokens after -- also forward into {{ args }} verbatim. {{ args }} renders as a space-joined, shell-escaped string; index with {{ args[0] }}, loop with {% for a in args %}…{% endfor %}, count with {{ args | length }}.
The long form --var KEY=VALUE is deprecated but still supported. It force-binds regardless of whether any hook template references KEY — useful when a template only references the key conditionally (e.g. {% if override %}…{% endif %}).
Recipes
- Eliminate cold starts:
wt step copy-ignoredinpost-startshares build caches and dependencies; use a[[post-start]]pipeline when a later hook depends on the copy - Dev server per worktree:
wt step tetherinpost-startruns the dev server and kills its whole process group when the worktree is removed, with optional subdomain routing - Database per worktree: a
post-startpipeline stores container name, port, and connection string as per-branch vars that later hooks reference - Progressive validation: quick lint/typecheck in
pre-commit, expensive tests and builds inpre-merge - Target-specific hooks: branch on
{{ target }}inpost-mergefor per-environment deploys
Command reference
wt hook - Run configured hooks
Usage: wt hook [OPTIONS] <COMMAND>
Commands:
show Show configured hooks
pre-switch Run pre-switch hooks
post-switch Run post-switch hooks
pre-start Run pre-start hooks
post-start Run post-start hooks
pre-commit Run pre-commit hooks
post-commit Run post-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
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
--config-set <toml>
Override config with inline TOML, e.g. --config-set list.full=true (repeatable)
-v, --verbose...
Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug
logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to
apply the same level everywhere — including shell completion, which no flag can reach
-y, --yes
Skip approval promptsLLM Commit Messages
Worktrunk generates commit messages by building a templated prompt and piping it to an external command. This integrates with wt merge, wt step commit, and wt step squash.
Setup
Any command that reads a prompt from stdin and outputs a commit message works. Add to ~/.config/worktrunk/config.toml:
Claude Code
[commit.generation]
command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''"--no-session-persistence prevents the commit conversation from polluting claude --continue. --safe-mode keeps the run hermetic — no hooks, plugins, MCP, skills, or CLAUDE.md — while leaving authentication working normally, so setups that authenticate via apiKeyHelper (not just OAuth or ANTHROPIC_API_KEY) still get a key. --setting-sources='user' scopes settings to your user config so a project .claude/settings.json can't override auth. The remaining flags disable tools, system prompt, and thinking for fast text-only output. --safe-mode requires Claude Code ≥ 2.1.169. See Claude Code docs for installation.
Codex
[commit.generation]
command = "codex exec -m gpt-5.4-mini -c model_reasoning_effort='low' -c system_prompt='' --sandbox=read-only --json - | jq -sr '[.[] | select(.item.type? == \"agent_message\")] | last.item.text'"Uses the fast mini model with low reasoning effort and an empty system prompt for faster output. Requires jq for JSON parsing. See Codex CLI docs.
Other tools
# opencode — use a fast model variant
command = "opencode run -m anthropic/claude-haiku-4.5 --variant fast"
# llm
command = "llm -m claude-haiku-4.5"
# aichat
command = "aichat -m claude:claude-haiku-4.5"Usage
These examples assume a feature worktree with changes to commit.
wt merge
Squashes all changes (uncommitted + existing commits) into one commit with an LLM-generated message, then merges to the default branch:
$ wt merge
<span class=c>◎</span> <span class=c>Squashing 3 commits into a single commit <span style='color:var(--bright-black,#555)'>(5 files, <span class=g>+16</span></span></span><span style='color:var(--bright-black,#555)'>)</span>...
<span class=c>◎</span> <span class=c>Generating squash commit message...</span>
<span style='background:var(--bright-white,#fff)'> </span> <b>feat(auth): Implement JWT authentication system</b>
<span style='background:var(--bright-white,#fff)'> </span>
<span style='background:var(--bright-white,#fff)'> </span> Add comprehensive JWT token handling including validation, refresh
<span style='background:var(--bright-white,#fff)'> </span> logic, and authentication tests.
<span class=g>✓</span> <span class=g>Squashed @ a1b2c3d</span>
<span class=c>◎</span> <span class=c>Merging 1 commit to <b>main</b> @ <span class=d>a1b2c3d</span> (no rebase needed)</span>
<span style='background:var(--bright-white,#fff)'> </span> * <span style='color:var(--yellow,#a60)'>a1b2c3d</span> feat(auth): Implement JWT authentication system
<span style='background:var(--bright-white,#fff)'> </span> auth.rs | 2 <span class=g>++</span>
<span style='background:var(--bright-white,#fff)'> </span> auth_test.rs | 2 <span class=g>++</span>
<span style='background:var(--bright-white,#fff)'> </span> integration_test.rs | 6 <span class=g>++++++</span>
<span style='background:var(--bright-white,#fff)'> </span> jwt.rs | 3 <span class=g>+++</span>
<span style='background:var(--bright-white,#fff)'> </span> jwt_test.rs | 3 <span class=g>+++</span>
<span style='background:var(--bright-white,#fff)'> </span> 5 files changed, 16 insertions(+)
<span class=g>✓</span> <span class=g>Merged to <b>main</b> <span style='color:var(--bright-black,#555)'>(1 commit, 5 files, <span class=g>+16</span></span></span><span style='color:var(--bright-black,#555)'>)</span>
<span class=c>◎</span> <span class=c>Removing <b>feature</b> worktree & branch in background (same commit as <b>main</b>,</span> <span class=d>_</span><span class=c>)</span>
<span class=d>○</span> Switched to worktree for <b>main</b> @ <b>~/repo</b>wt step commit
Stages and commits with LLM-generated message:
$ wt step commit
<span class=c>◎</span> <span class=c>Generating commit message and committing changes... <span style='color:var(--bright-black,#555)'>(2 files, <span class=g>+26</span></span></span><span style='color:var(--bright-black,#555)'>)</span>
<span style='background:var(--bright-white,#fff)'> </span> <b>feat(validation): add input validation utilities</b>
<span class=g>✓</span> <span class=g>Committed changes @ <span class=d>a1b2c3d</span></span>wt step squash
Squashes branch commits into one with LLM-generated message:
$ wt step squash
<span class=c>◎</span> <span class=c>Squashing 3 commits into a single commit <span style='color:var(--bright-black,#555)'>(5 files, <span class=g>+16</span></span></span><span style='color:var(--bright-black,#555)'>)</span>...
<span class=c>◎</span> <span class=c>Generating squash commit message...</span>
<span style='background:var(--bright-white,#fff)'> </span> <b>feat(auth): Implement JWT authentication system</b>
<span style='background:var(--bright-white,#fff)'> </span>
<span style='background:var(--bright-white,#fff)'> </span> Add comprehensive JWT token handling including validation, refresh
<span style='background:var(--bright-white,#fff)'> </span> logic, and authentication tests.
<span class=g>✓</span> <span class=g>Squashed @ a1b2c3d</span>See `wt merge` and `wt step` for full documentation.
Branch summaries
[experimental]
With summary = true and a [commit.generation] command configured, Worktrunk generates LLM branch summaries — one-line descriptions of each branch's changes since the default branch.
Summaries appear in:
- `wt switch` interactive picker — preview tab 5
- `wt list --full` — the Summary column (see `wt list`)
Enable in user config:
[list]
summary = trueSummaries are cached and regenerated only when the diff changes.
Prompt templates
Worktrunk uses minijinja templates (Jinja2-like syntax) to build prompts.
Custom templates
Override the defaults with inline 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 {{ commit_details | length }} commits into one message:
{% for c in commit_details %}
- {{ c.subject }}
{% endfor %}
Diff:
{{ git_diff }}
"""Template variables
| Variable | Description |
|---|---|
{{ git_diff }} | The diff (staged changes or combined diff for squash) |
{{ git_diff_stat }} | Diff statistics (files changed, insertions, deletions) |
{{ branch }} | Current branch name |
{{ repo }} | Repository name |
{{ recent_commits }} | Recent commit subjects (for style reference) |
{{ commit_details }} | Commits being squashed (squash template only); each renders as its subject and exposes .subject / .body |
{{ target_branch }} | Merge target branch (squash template only) |
{{ user_guidance }} | Rendered user template-append fragment (see below) |
{{ project_guidance }} | Rendered project template-append fragment (see below) |
Template syntax
Templates use minijinja, which supports:
- Variables:
{{ branch }},{{ repo | upper }} - Filters:
{{ commit_details | length }},{{ repo | upper }} - Conditionals:
{% if recent_commits %}...{% endif %} - Loops:
{% for c in commit_details %}{{ c.subject }}{% endfor %} - Loop variables:
{{ loop.index }},{{ loop.length }} - Whitespace control:
{%- ... -%}strips surrounding whitespace
See wt config create --help for the full default templates.
Appending to the prompt
[experimental]
template-append adds to the commit and squash prompts instead of replacing them. It lives in both user config (personal preferences) and project config (.config/wt.toml, shared so every teammate's LLM sees the same style guide). Each fragment is itself a minijinja template — Worktrunk renders it with the same variables as the main template ({{ branch }}, {{ git_diff }}, …), then appends the result after <style>. The user fragment renders into a <user-guidance> block and the project fragment into a <project-guidance> block, so the LLM can tell personal preference from shared convention:
# .config/wt.toml
[commit.generation]
template-append = """
- Use conventional commits (feat:, fix:, docs:, …)
- Reference the related issue ID in the body
"""When both the user and project set template-append, the <user-guidance> block comes first, then <project-guidance>.
The user fragment needs no approval — it's the developer's own config. For the project fragment, the first time the rendered text is sent to the LLM, Worktrunk shows the raw fragment in an approval prompt — the same one-shot gate as project-defined hooks. Subsequent commits don't re-prompt unless the fragment changes. Declining is non-fatal: the LLM runs with just the user fragment (if any).
Custom user templates that don't reference {{ user_guidance }} / {{ project_guidance }} opt out of the appended blocks — the rendered values are injected only where the template places them.
Fallback behavior
When no LLM is configured, worktrunk generates deterministic messages based on changed filenames (e.g., "Changes to auth.rs & config.rs").
Related skills
How it compares
Pick worktrunk over manual `git worktree add` when you need session activity markers and agent-specific hook setup across multiple CLIs.
FAQ
Which agent CLIs does worktrunk support?
worktrunk ships plugins for Claude Code, Codex, OpenCode, and Gemini CLI. All four include a configuration skill; activity tracking in `wt list` works on Claude Code, OpenCode, and Gemini CLI.
Why is worktree isolation limited to Claude Code?
worktrunk worktree isolation and `/wt-switch-create` depend on worktree-lifecycle hooks that only Claude Code exposes today. Codex and OpenCode receive the configuration skill and activity tracking where their hook APIs allow.
Is Worktrunk safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.