Worktrunk is a CLI for managing Git worktrees in workflows with parallel AI coding agents. It encapsulates the worktree lifecycle, project setup hooks, and status tracking into a small set of explicit commands.
It's designed for developers who use terminal-based agents such as Claude Code, Amp, aider, Codex CLI, and Gemini CLI, with each agent in its own short‑lived branch and directory.
Agentic coding makes it cheap to spin up a new "developer" for each feature or bugfix. The coordination problems:
- Keeping multiple tasks in flight without re-explaining the codebase
- Preventing agents from stepping on each other
- Merging, testing, and cleaning up all those branches
Git worktrees solve this: multiple isolated working directories backed by a
single .git directory. But raw git worktree requires picking directory
names, tracking paths, remembering cleanup steps. Worktrunk wraps that with:
- Consistent directory layout (
../repo.feature-x/) - Lifecycle hooks for setup, tests, cleanup
- Unified status across all worktrees
One repository, many short-lived worktrees. main is the trunk. Each
feature/bugfix/agent gets a branch + worktree directory with a 1:1 mapping.
Three core commands:
| Command | Purpose |
|---|---|
wt switch |
Create or switch worktrees |
wt merge |
Commit, squash, rebase, test, merge, cleanup |
wt list |
Status across all worktrees |
See Commands for the full list.
cargo install worktrunk
wt config shell install # Bash, Zsh, FishShell integration lets wt switch and wt merge change directories.
$ wt switch --create fix-auth
✅ Created new worktree for fix-auth from main at ../repo.fix-authThis creates ../repo.fix-auth on branch fix-auth.
$ wt merge
🔄 Merging 1 commit to main @ a1b2c3d (no commit/squash/rebase needed)
* a1b2c3d Implement JWT validation
auth.rs | 13 +++++++++++++
1 file changed, 13 insertions(+)
✅ Merged to main (1 commit, 1 file, +13)
🔄 Removing fix-auth worktree & branch in backgroundwt merge handles the full workflow: stage, commit, squash, rebase, run hooks,
merge, cleanup.
$ wt list
Branch Status HEAD± main↕ Path Remote⇅ Commit Age Message
@ main ^ ./test-repo ↑0 ↓0 b834638e 10mo Initial commit
+ bugfix-y ↑ ↑1 ./bugfix-y 412a27c8 10mo Fix bug
+ feature-x + ↑ +5 ↑3 ./feature-x 7fd821aa 10mo Add file 3
⚪ Showing 3 worktrees, 1 with changes, 2 ahead--full adds CI status and conflicts. --branches includes all branches.
Worktrunk is opinionated:
- Trunk-based — Short-lived branches, linear histories.
- Local-first — Terminal-based agents, local dev loops.
- Git underneath — Git remains the source of truth; Worktrunk orchestrates.
- Minimal surface — Three core commands. Extras are escape hatches.
- 1:1 branch/worktree — Worktrees addressed by branch name.
- Squash by default — Clean history. Opt out with
--no-squash. - Progressive UI —
wt listshows local data first, slow data as it arrives. - Pluggable —
git worktreecommands still work.
Worktrunk can invoke external commands to generate commit messages. llm is recommended.
~/.config/worktrunk/config.toml:
[commit-generation]
command = "llm"
args = ["-m", "claude-haiku-4-5-20251001"]wt merge generates commit messages automatically:
$ wt merge
🔄 Squashing 3 commits into a single commit (3 files, +33)...
🔄 Generating squash commit message...
feat(auth): Implement JWT authentication system
Add comprehensive JWT token handling including validation, refresh logic,
and authentication tests. This establishes the foundation for secure
API authentication.
- Implement token refresh mechanism with expiry handling
- Add JWT encoding/decoding with signature verification
- Create test suite covering all authentication flows
✅ Squashed @ a1b2c3d
🔄 Running pre-merge test:
cargo test
🔄 Running pre-merge lint:
cargo clippy
🔄 Merging 1 commit to main @ a1b2c3d (no rebase needed)
* a1b2c3d feat(auth): Implement JWT authentication system
auth.rs | 8 ++++++++
auth_test.rs | 17 +++++++++++++++++
jwt.rs | 8 ++++++++
3 files changed, 33 insertions(+)
✅ Merged to main (1 commit, 3 files, +33)
🔄 Removing feature-auth worktree & branch in background
🔄 Running post-merge install:
cargo install --path .wt step commit runs just the commit step. Custom prompt templates: wt config --help.
Configure hooks in .config/wt.toml:
| Hook | When | Example |
|---|---|---|
| post-create | After worktree created | cp -r .cache, ln -s |
| post-start | After worktree created (background) | npm install, cargo build |
| pre-commit | Before squash commit created | pre-commit run |
| pre-merge | After squash, before push | cargo test, pytest |
| post-merge | After successful merge | cargo install --path . |
# Install dependencies, build setup
[post-create]
"install" = "uv sync"
# Dev servers, file watchers (runs in background)
[post-start]
"dev" = "uv run dev"
# Tests and lints before merging (blocks on failure)
[pre-merge]
"test" = "uv run pytest"
"lint" = "uv run ruff check"$ wt switch --create feature-x
🔄 Running post-create install:
uv sync
Resolved 24 packages in 145ms
Installed 24 packages in 1.2s
✅ Created new worktree for feature-x from main at ../repo.feature-x
🔄 Running post-start dev:
uv run devMerging with pre-merge hooks
$ wt merge
🔄 Squashing 3 commits into a single commit (2 files, +45)...
🔄 Generating squash commit message...
feat(api): Add user authentication endpoints
Implement login and token refresh endpoints with JWT validation.
Includes comprehensive test coverage and input validation.
✅ Squashed @ a1b2c3d
🔄 Running pre-merge test:
uv run pytest
============================= test session starts ==============================
collected 3 items
tests/test_auth.py::test_login_success PASSED [ 33%]
tests/test_auth.py::test_login_invalid_password PASSED [ 66%]
tests/test_auth.py::test_token_validation PASSED [100%]
============================== 3 passed in 0.8s ===============================
🔄 Running pre-merge lint:
uv run ruff check
All checks passed!
🔄 Merging 1 commit to main @ a1b2c3d (no rebase needed)
* a1b2c3d feat(api): Add user authentication endpoints
api/auth.py | 31 +++++++++++++++++++++++++++++++
tests/test_auth.py | 14 ++++++++++++++
2 files changed, 45 insertions(+)
✅ Merged to main (1 commit, 2 files, +45)
🔄 Removing feature-auth worktree & branch in backgroundSee wt switch --help and wt merge --help for skipping hooks, template variables, security details.
Shell integration lets wt switch, wt merge, and wt remove change
directories:
wt config shell install # Bash, Zsh, FishManual setup: wt config shell --help.
Alias for new worktree + agent:
alias wsl='wt switch --create --execute=claude'
wsl new-feature # Creates worktree, runs hooks, launches ClaudeEliminate cold starts — post-create hooks install deps and copy caches.
See .config/wt.toml for an example using copy-on-write.
Local CI gate — pre-merge hooks run before merging. Failures abort the
merge.
Track agent status — Custom emoji markers show agent state in wt list.
Claude Code hooks can set these automatically. See Custom Worktree
Status.
Monitor CI across branches — wt list --full --branches shows PR/CI status
for all branches, including those without worktrees. CI column links to PR pages
in terminals with hyperlink support.
JSON API — wt list --format=json for dashboards, statuslines, scripts.
Task runners — Reference Taskfile/Justfile/Makefile in hooks:
[post-create]
"setup" = "task install"
[pre-merge]
"validate" = "just test lint"Shortcuts — ^ = default branch, @ = current branch, - = previous
worktree. Example: wt switch --create hotfix --base=@ branches from current
HEAD.
wt switch [branch] - Switch to existing worktree or create a new one
wt switch — Switch to a worktree
Usage: switch [OPTIONS] <BRANCH>
Arguments:
<BRANCH>
Branch, path, '@' (HEAD), '-' (previous), or '^' (main)
Options:
-c, --create
Create a new branch
-b, --base <BASE>
Base branch
Defaults to default branch.
-x, --execute <EXECUTE>
Command to run after switch
-f, --force
Skip approval prompts
--no-verify
Skip all project hooks
-h, --help
Print help (see a summary with '-h')
- If worktree exists for branch, changes directory via shell integration
- No hooks run
- No branch creation
- Creates new branch (defaults to current default branch as base)
- Creates worktree in configured location (default:
../{{ main_worktree }}.{{ branch }}) - Runs post-create hooks sequentially (blocking)
- Shows success message
- Spawns post-start hooks in background (non-blocking)
- Changes directory to new worktree via shell integration
- Run after worktree creation, before success message
- Typically:
npm install,cargo build, setup tasks - Failures block the operation
- Skip with
--no-verify
- Spawned after success message shown
- Typically: dev servers, file watchers, editors
- Run in background, failures logged but don't block
- Logs:
.git/wt-logs/{branch}-post-start-{name}.log - Skip with
--no-verify
Template variables: {{ repo }}, {{ branch }}, {{ worktree }}, {{ repo_root }}
Security: Commands from project hooks require approval on first run.
Approvals are saved to user config. Use --force to bypass prompts.
See wt config approvals --help.
Switch to existing worktree:
wt switch feature-branchCreate new worktree from main:
wt switch --create new-featureSwitch to previous worktree:
wt switch -Create from specific base:
wt switch --create hotfix --base productionCreate and run command:
wt switch --create docs --execute "code ."Skip hooks during creation:
wt switch --create temp --no-verifyUse @ for current HEAD, - for previous, ^ for main:
wt switch @ # Switch to current branch's worktree
wt switch - # Switch to previous worktree
wt switch --create new-feature --base=^ # Branch from main (default)
wt switch --create bugfix --base=@ # Branch from current HEAD
wt remove @ # Remove current worktreewt merge [target] - Merge, push, and cleanup
wt merge — Merge worktree into target branch
Usage: merge [OPTIONS] [TARGET]
Arguments:
[TARGET]
Target branch
Defaults to default branch.
Options:
--no-squash
Skip commit squashing
--no-commit
Skip commit, squash, and rebase
--no-remove
Keep worktree after merge
--no-verify
Skip all project hooks
-f, --force
Skip approval prompts
--stage <STAGE>
What to stage before committing [default: all]
Possible values:
- all: Stage everything: untracked files + unstaged tracked changes
- tracked: Stage tracked changes only (like git add -u)
- none: Stage nothing, commit only what's already in the index
-h, --help
Print help (see a summary with '-h')
Commit → Squash → Rebase → Pre-merge hooks → Push → Cleanup → Post-merge hooks
Uncommitted changes are staged and committed with LLM commit message.
Use --stage=tracked to stage only tracked files, or --stage=none to commit only what's already staged.
Multiple commits are squashed into one (like GitHub's "Squash and merge") with LLM commit message.
Skip with --no-squash. Safety backup: git reflog show refs/wt-backup/<branch>
Branch is rebased onto target. Conflicts abort the merge immediately.
Pre-merge commands run after rebase (failures abort). Post-merge commands
run after cleanup (failures logged). Skip all with --no-verify.
Fast-forward push to local target branch. Non-fast-forward pushes are rejected.
Worktree and branch are removed. Skip with --no-remove.
Template variables: {{ repo }}, {{ branch }}, {{ worktree }}, {{ repo_root }}, {{ target }}
Security: Commands from project hooks require approval on first run.
Approvals are saved to user config. Use --force to bypass prompts.
See wt config approvals --help.
Basic merge to main:
wt mergeMerge without squashing:
wt merge --no-squashKeep worktree after merging:
wt merge --no-removeSkip all hooks:
wt merge --no-verifywt remove [worktree] - Remove worktree and branch
wt remove — Remove worktree and branch
Usage: remove [OPTIONS] [WORKTREES]...
Arguments:
[WORKTREES]...
Worktree or branch (@ for current)
Options:
--no-delete-branch
Keep branch after removal
-D, --force-delete
Delete unmerged branches
--no-background
Run removal in foreground
-h, --help
Print help (see a summary with '-h')
Removes worktree directory, git metadata, and branch. Requires clean working tree.
- Removes current worktree and switches to main worktree
- In main worktree: switches to default branch
- Removes specified worktree(s) and branches
- Current worktree removed last (switches to main first)
- Returns immediately so you can continue working
- Logs:
.git/wt-logs/{branch}-remove.log - Use
--no-backgroundfor foreground (blocking)
Stops any git fsmonitor daemon for the worktree before removal. This prevents orphaned processes when using builtin fsmonitor (core.fsmonitor=true). No effect on Watchman users.
Remove current worktree and branch:
wt removeRemove specific worktree and branch:
wt remove feature-branchRemove worktree but keep branch:
wt remove --no-delete-branch feature-branchRemove multiple worktrees:
wt remove old-feature another-branchRemove in foreground (blocking):
wt remove --no-background feature-branchSwitch to default in main:
wt remove # (when already in main worktree)wt list - Show all worktrees and branches
wt list — List worktrees and optionally branches
Usage: list [OPTIONS]
Options:
--format <FORMAT>
Output format (table, json)
[default: table]
--branches
Include branches without worktrees
--remotes
Include remote branches
--full
Show CI, conflicts, diffs
--progressive
Show fast info immediately, update with slow info
Displays local data (branches, paths, status) first, then updates with remote data (CI, upstream) as it arrives. Auto-enabled for TTY.
-h, --help
Print help (see a summary with '-h')
- Branch: Branch name
- Status: Quick status symbols (see Status Symbols below)
- HEAD±: Uncommitted changes vs HEAD (+added -deleted lines, staged + unstaged)
- main↕: Commit count ahead↑/behind↓ relative to main (commits in HEAD vs main)
- main…± (
--full): Line diffs in commits ahead of main (+added -deleted) - Path: Worktree directory location
- Remote⇅: Commits ahead↑/behind↓ relative to tracking branch (e.g.
origin/branch) - CI (
--full): CI pipeline status (tries PR/MR checks first, falls back to branch workflows)●passed (green) - All checks passed●running (blue) - Checks in progress●failed (red) - Checks failed●conflicts (yellow) - Merge conflicts with base●no-ci (gray) - PR/MR or workflow found but no checks configured- (blank) - No PR/MR or workflow found, or
gh/glabCLI unavailable - (dimmed) - Stale: unpushed local changes differ from PR/MR head
- Commit: Short commit hash (8 chars)
- Age: Time since last commit (relative)
- Message: Last commit message (truncated)
Order: +!? ✖⚠≡∅ ↻⋈ ↑↓↕ ⇡⇣⇅ ⎇⌫⊠
+Staged files (ready to commit)!Modified files (unstaged changes)?Untracked files present✖Merge conflicts - unresolved conflicts in working tree (fix before continuing)⚠Would conflict - merging into main would fail≡Working tree matches main (identical contents, regardless of commit history)∅No commits (no commits ahead AND no uncommitted changes)↻Rebase in progress⋈Merge in progress↑Ahead of main branch↓Behind main branch↕Diverged (both ahead and behind main)⇡Ahead of remote tracking branch⇣Behind remote tracking branch⇅Diverged (both ahead and behind remote)⎇Branch indicator (shown for branches without worktrees)⌫Prunable worktree (directory missing, can be pruned)⊠Locked worktree (protected from auto-removal)
Rows are dimmed when there's no marginal contribution (≡ matches main OR ∅ no commits).
Use --format=json for structured data. Each object contains two status maps
with the same fields in the same order as Status Symbols above:
status - variant names for querying:
working_tree:{untracked, modified, staged, renamed, deleted}booleansbranch_state:""|"Conflicts"|"MergeTreeConflicts"|"MatchesMain"|"NoCommits"git_operation:""|"Rebase"|"Merge"main_divergence:""|"Ahead"|"Behind"|"Diverged"upstream_divergence:""|"Ahead"|"Behind"|"Diverged"user_status: string (optional)
status_symbols - Unicode symbols for display (same fields, plus worktree_attrs: ⎇/⌫/⊠)
Note: locked and prunable are top-level fields on worktree objects, not in status.
Worktree position fields (for identifying special worktrees):
is_main: boolean - is the main/default worktreeis_current: boolean - is the current working directory (present when true)is_previous: boolean - is the previous worktree fromwt switch(present when true)
Query examples:
# Find worktrees with conflicts
jq '.[] | select(.status.branch_state == "Conflicts")'
# Find worktrees with untracked files
jq '.[] | select(.status.working_tree.untracked)'
# Find worktrees in rebase or merge
jq '.[] | select(.status.git_operation != "")'
# Get branches ahead of main
jq '.[] | select(.status.main_divergence == "Ahead")'
# Find locked worktrees
jq '.[] | select(.locked != null)'
# Get current worktree info (useful for statusline tools)
jq '.[] | select(.is_current == true)'wt config - Manage configuration
wt config — Manage configuration and shell integration
Usage: config <COMMAND>
Commands:
shell Shell integration setup
create Create global configuration file
show Show configuration files & locations
refresh-cache Refresh default branch from remote
status Manage branch status markers
approvals Manage command approvals
help Print this message or the help of the given subcommand(s)
Options:
-h, --help
Print help (see a summary with '-h')
-
Set up shell integration
wt config shell installOr manually add to your shell config:
eval "$(wt config shell init bash)" -
(Optional) Create config file
wt config createThis creates ~/.config/worktrunk/config.toml with examples.
-
(Optional) Enable LLM commit messages
Install:
uv tool install -U llmConfigure:llm keys set anthropicAdd to config.toml:[commit-generation] command = "llm"
For Claude:
llm install llm-anthropic
llm keys set anthropic
llm models default claude-haiku-4-5-20251001For OpenAI:
llm keys set openaiUse wt config show to view your current configuration.
Docs: https://llm.datasette.io/ | https://github.com/sigoden/aichat
Global config (user settings):
- Location:
~/.config/worktrunk/config.toml(orWORKTRUNK_CONFIG_PATH) - Run
wt config create --helpto view documented examples
Project config (repository hooks):
- Location:
.config/wt.tomlin repository root - Contains: post-create, post-start, pre-commit, pre-merge, post-merge hooks
wt step - Building blocks for workflows
wt step — Workflow building blocks
Usage: step <COMMAND>
Commands:
commit Commit changes with LLM commit message
squash Squash commits with LLM commit message
push Push changes to local target branch
rebase Rebase onto target
post-create Run post-create hook
post-start Run post-start hook
pre-commit Run pre-commit hook
pre-merge Run pre-merge hook
post-merge Run post-merge hook
help Print this message or the help of the given subcommand(s)
Options:
-h, --help
Print help
Add emoji status markers to branches that appear in wt list.
# Set status for current branch
wt config status set "🤖"
# Or use git config directly
git config worktrunk.status.feature-x "💬"Status appears in the Status column:
$ wt list
Branch Status HEAD± main↕ Path Remote⇅ Commit Age Message
@ main ^ ./test-repo b834638e 10mo Initial commit
+ clean-no-status ∅ ./clean-no-status b834638e 10mo Initial commit
+ clean-with-status ∅ 💬 ./clean-with-status b834638e 10mo Initial commit
+ dirty-no-status ! +1 -1 ./dirty-no-status b834638e 10mo Initial commit
+ dirty-with-status ?∅ 🤖 ./dirty-with-status b834638e 10mo Initial commit
⚪ Showing 5 worktrees, 1 with changesThe custom emoji appears directly after the git status symbols.
Automation with Claude Code Hooks
Claude Code can automatically set/clear emoji status when coding sessions start and end.
When using Claude:
- Sets status to
🤖for the current branch when submitting a prompt (working) - Changes to
💬when Claude needs input (waiting for permission or idle) - Clears the status completely when the session ends
$ wt list
Branch Status HEAD± main↕ Path Remote⇅ Commit Age Message
@ main ^ ./test-repo b834638e 10mo Initial commit
+ clean-no-status ∅ ./clean-no-status b834638e 10mo Initial commit
+ clean-with-status ∅ 💬 ./clean-with-status b834638e 10mo Initial commit
+ dirty-no-status ! +1 -1 ./dirty-no-status b834638e 10mo Initial commit
+ dirty-with-status ?∅ 🤖 ./dirty-with-status b834638e 10mo Initial commit
⚪ Showing 5 worktrees, 1 with changesHow it works:
- Status is stored as
worktrunk.status.<branch>in.git/config - Each branch can have its own status emoji
- The hooks automatically detect the current branch and set/clear its status
- Works with any git repository, no special configuration needed
Experimental commands under wt beta. Interface may change.
Interactive worktree selector with fuzzy search and diff preview. Unix only.
Preview modes (toggle with 1/2/3):
- Mode 1: Working tree changes (uncommitted)
- Mode 2: History (commits not on main highlighted)
- Mode 3: Branch diff (changes ahead of main)
Single-line status for shell prompts, starship, or editor integrations.
branch status ±working commits upstream ci
Claude Code integration (--claude-code): Reads workspace context from
stdin, outputs directory, branch status, and model name. Can be used by Claude
Code hooks to show worktree state in the status line.
Worktrunk is in active development. The core features are stable and ready for use. There may be backward-incompatible changes.
The most helpful way to contribute:
- Use it!
- Star the repo / tell friends / post about it
- Find bugs, file reproducible bug reports
What commands does Worktrunk execute?
Worktrunk executes commands in three contexts:
- Project hooks (
.config/wt.toml) - Automation for worktree lifecycle - LLM commands (
~/.config/worktrunk/config.toml) - Commit message generation - --execute flag - Commands provided explicitly
Commands from project hooks and LLM configuration require approval on first run. Approved commands are saved to ~/.config/worktrunk/config.toml under the project's configuration. If a command changes, Worktrunk requires new approval.
Example approval prompt:
🟡 test-repo needs approval to execute 3 commands:
⚪ post-create install:
echo 'Installing dependencies...'
⚪ post-create build:
echo 'Building project...'
⚪ post-create test:
echo 'Running tests...'
💡 Allow and remember? [y/N]
Use --force to bypass prompts (useful for CI/automation).
How does Worktrunk compare to alternatives?
Branch switching uses one directory, so only one agent can work at a time. Worktrees give each agent its own directory.
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 # Squashes, merges, removes worktreeWhat 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)
Worktrunk adds path management, lifecycle hooks, and wt list --full for viewing all worktrees—branches, uncommitted changes, commits ahead/behind, CI status, and conflicts—in a single view.
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.
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.
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-featuresThis 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.
Any notes for developing this crate?
Quick tests (no external dependencies):
cargo test --lib --bins # Unit tests (~200 tests)
cargo test --test integration # Integration tests without shell tests (~300 tests)Full integration tests (requires bash, zsh, fish):
cargo test --test integration --features shell-integration-testsDependencies for shell integration tests:
- bash, zsh, fish shells
- Quick setup:
./dev/setup-claude-code-web.sh(installs shells on Linux)
Use cargo-release to publish new versions:
cargo install cargo-release
# Bump version, update Cargo.lock, commit, tag, and push
cargo release patch --execute # 0.1.0 -> 0.1.1
cargo release minor --execute # 0.1.0 -> 0.2.0
cargo release major --execute # 0.1.0 -> 1.0.0This updates Cargo.toml and Cargo.lock, creates a commit and tag, then pushes to GitHub. The tag push triggers GitHub Actions to build binaries, create the release, and publish to crates.io.
Run without --execute to preview changes first.
