wt
Git worktree manager with GitHub/GitLab integration.
Why wt
Git worktrees let you work on multiple branches simultaneously without stashing or switching—great for juggling a feature branch and a hotfix, or running multiple AI agent sessions in parallel.
But worktrees can pile up fast. You end up with a dozen directories, can't remember which ones are already merged, and need custom scripts to open your editor, create terminal tabs, or clean up stale checkouts.
wt solves this:
- Hooks auto-run commands when creating/opening worktrees (open editor, spawn terminal tab)
- Prune removes merged worktrees and shows PR/MR status so you know what's safe to delete
- PR checkout opens pull requests in worktrees for easier code review
⚠️ Pre-1.0 Notice
This project may include breaking command & configuration changes until v1.0 is released. Once v1 is released, backwards compatibility will be maintained.
If something breaks:
- Delete
~/.wt/prs.json (PR cache)
- Compare your config with
wt config init -s and update to match newer config format
Install
# Homebrew (macOS/Linux)
brew install raphi011/tap/wt
# Go
go install github.com/raphi011/wt/cmd/wt@latest
Requires git in PATH. For GitHub repos: gh CLI. For GitLab repos: glab CLI.
Getting Started
1. Create Config
wt config init # Create ~/.wt/config.toml
wt config init -s # Print default config to stdout (for review)
2. Register Repos
# Register a repo you already have cloned
wt repo add ~/path/to/myrepo
# Or clone and register a new repo
wt repo clone git@github.com:org/repo.git
Repos are also auto-registered the first time you run wt checkout inside one.
3. Create a Worktree
wt checkout -b new-branch # Create worktree with new branch (from default branch)
4. List Worktrees
wt list -g
You're ready to go! See Scenarios below for common workflows.
Interactive Mode
For guided worktree creation, use the -i flag:
wt checkout -i
This launches a step-by-step wizard that guides you through the checkout process.
Scenarios
Starting a New Feature
# Create worktree with new branch (from origin/main)
wt checkout -b feature-login
# Create from a different base branch
wt checkout -b feature-login --base develop
# Fetch base branch before creating (ensures up-to-date base)
wt checkout -b feature-login -f
# Fetch target branch from origin before checkout
wt checkout feature-login -f
# Stash local changes and apply them to the new worktree
wt checkout -b feature-login -s
# Add a note to remember what you're working on
wt checkout -b feature-login --note "Implementing OAuth flow"
# Target a specific repo from any directory (repo:branch syntax)
wt checkout -b myrepo:feature-login
# Combine with other flags
wt checkout -b myrepo:feature-login --base develop -f
With hooks configured, your editor opens automatically:
# ~/.wt/config.toml
[hooks.vscode]
command = "code '{worktree-dir}'"
on = ["checkout"]
Reviewing a Pull Request
# Checkout PR from current repo
wt pr checkout 123
# Checkout PR from a different local repo (by name)
wt pr checkout backend-api 123
# Clone repo you don't have locally and checkout PR
wt pr checkout org/new-repo 456
# Specify forge type when auto-detection fails
wt pr checkout 123 --forge gitlab
View PR details or open in browser:
wt pr view # Show PR details
wt pr view -w # Open PR in browser
wt pr view myrepo # View PR for specific repo
After review, merge and clean up in one command:
wt pr merge # Uses squash by default
wt pr merge -s rebase # Or specify strategy
wt pr merge --keep # Merge but keep worktree
Creating a Pull Request
# Create PR for current branch
wt pr create --title "Add login feature"
# With description
wt pr create --title "Fix bug" --body "Fixes issue #123"
# Read body from file (great for templates)
wt pr create --title "Add feature" --body-file=pr.md
# Create as draft
wt pr create --title "WIP: Refactor auth" --draft
# Create and open in browser
wt pr create --title "Ready for review" -w
# By repo name (when outside worktree)
wt pr create --title "Add feature" myrepo
Cleaning Up
# See what worktrees exist
wt list
# Remove merged worktrees (uses cached PR status)
wt prune
# Refresh PR status from GitHub/GitLab first
wt prune -R
# Preview what would be removed
wt prune -d
# Verbose dry-run: see what's skipped and why
wt prune -d -v
# Clear cached PR data and re-fetch
wt prune --reset-cache
# Also delete local branches after removal
wt prune --delete-branches
# Keep local branches even if config says delete
wt prune --no-delete-branches
# Remove specific branch worktree
wt prune feature-login -f
# Remove worktree from specific repo
wt prune myrepo:feature-login -f
Working Across Multiple Repos
Register and label repos for batch operations:
# Register a repo (from inside the repo)
wt repo add .
# Register a repo by path
wt repo add ~/path/to/myrepo
# Clone and register a new repo
wt repo clone git@github.com:org/repo.git
# Convert an existing repo to bare structure (for faster worktrees)
wt repo convert --clone-mode bare ./myrepo
# Convert a bare repo back to regular structure
wt repo convert --clone-mode regular ./myrepo
# Unregister a repo
wt repo remove myrepo
# List all repos
wt repo list
wt repo list backend # Filter by label
Label your repos for batch operations:
# Add labels to repos
cd ~/Git/backend-api && wt label add backend
cd ~/Git/auth-service && wt label add backend
cd ~/Git/web-app && wt label add frontend
# List labels
wt label list # Labels for current repo
wt label list -g # All labels across repos
# Clear labels from a repo
wt label clear
# Create same branch across all backend repos (using label prefix)
wt checkout -b backend:feature-auth
# Or target specific repo by name
wt checkout -b backend-api:feature-auth
# Run command across worktrees
wt exec main -- git status # In all repos' main worktree
wt exec backend-api:main -- make test # In specific repo's worktree
Quick Navigation
Note: wt cd prints the path but can't change your shell directory. Add the shell wrapper from Shell Integration to use wt cd directly.
# Jump to most recently accessed worktree
wt cd
# Jump to worktree by branch name
wt cd feature-auth
# Jump to worktree in specific repo (if branch exists in multiple repos)
wt cd backend-api:feature-auth
# Interactive fuzzy search
wt cd -i
# Run command in worktree
wt exec -- git status # In current worktree
wt exec myrepo:main -- code .
Running Hooks Manually
# Run a hook on current worktree
wt hook vscode
# Run multiple hooks
wt hook vscode kitty
# Run on specific worktree (repo:branch format)
wt hook vscode -- myrepo:feature
# Run across worktrees by label
wt hook build -- backend:main
# Pass custom variables
wt hook claude --arg prompt="implement feature X"
# Preview command without executing
wt hook vscode -d
Branch Notes
# Set a note (visible in list/prune output)
wt note set "WIP: fixing auth timeout issue"
# Get current note
wt note get
# Clear note
wt note clear
# Set note on specific worktree (repo:branch format)
wt note set "Ready for review" myrepo:feature
Configuration
Global config: ~/.wt/config.toml
Local config: .wt.toml (in bare repo root)
wt config init # Create default global config
wt config init --local # Create per-repo .wt.toml
wt config init -s # Print config to stdout
wt config show # Show effective config (merged if in a repo)
wt config show --repo myrepo # Show effective config for specific repo
wt config hooks # List hooks with source annotations
Basic Settings
# Default sort order for list: "date", "repo", "branch"
default_sort = "date"
# Labels applied to newly auto-registered repos
# default_labels = ["work"]
[checkout]
# Folder naming: {repo}, {branch}, {origin}
worktree_format = "{repo}-{branch}"
# Base ref for new branches: "remote" (default) or "local"
# - "remote": branches from origin/<base> (ensures latest remote state)
# - "local": branches from local <base> (useful for offline work)
base_ref = "remote"
# Auto-fetch from origin before checkout (default: false)
# Note: with base_ref="local" and an explicit --base, --fetch is skipped (warns) since fetch doesn't affect local refs
auto_fetch = true
# Auto-set upstream tracking (default: false)
# set_upstream = false
[prune]
# Delete local branches after worktree removal (default: false)
# delete_local_branches = false
[list]
# Days before a worktree's commit age is highlighted as stale (default: 14, 0 = disabled)
# stale_days = 14
Base branch resolution (--base flag):
--base value |
base_ref config |
Branch created from |
| (none) |
remote |
origin/<default> (main/master) |
| (none) |
local |
local default branch |
develop |
remote |
origin/develop |
develop |
local |
local develop |
origin/develop |
(overridden) |
origin/develop |
upstream/main |
(overridden) |
upstream/main |
Explicit remote refs (origin/branch, upstream/branch) always override base_ref config.
Fetch behavior (--fetch / auto_fetch):
| Scenario |
Fetch behavior |
--base origin/develop |
Fetches develop from origin |
--base upstream/main |
Fetches main from upstream |
--base develop + base_ref=remote |
Fetches develop from origin |
--base develop + base_ref=local |
Skipped with warning |
Hooks
[hooks.vscode]
command = "code '{worktree-dir}'"
description = "Open VS Code"
on = ["checkout", "pr"] # Auto-run for these commands
[hooks.kitty]
command = "kitty @ launch --type=tab --cwd='{worktree-dir}'"
description = "Open new kitty tab"
on = ["checkout"]
[hooks.cleanup]
command = "echo 'Removed {branch}'"
on = ["prune"]
[hooks.claude]
command = "kitty @ launch --cwd='{worktree-dir}' -- claude '{prompt:-help me}'"
description = "Open Claude with prompt"
# No "on" = only runs via: wt hook claude --arg prompt="..."
Hook triggers: checkout, pr, prune, merge, all (see on field in config)
Placeholders: {worktree-dir}, {repo-dir}, {branch}, {repo}, {origin}, {trigger}, {key}, {key:-default}, {key:+text}
Args: Pass --arg key=value or --arg key (bare boolean, sets to "true")
Forge Settings
Configure forge detection and multi-account auth for PR operations:
[forge]
default = "github" # Default forge
default_org = "my-company" # Default org (allows: wt pr checkout repo 123)
[[forge.rules]]
pattern = "company/*"
type = "gitlab"
[[forge.rules]]
pattern = "work-org/*"
type = "github"
user = "work-account" # Use specific gh account for matching repos
Merge Settings
[merge]
strategy = "squash" # squash, rebase, or merge
Preserve Settings
Automatically copy git-ignored files (.env, .envrc, etc.) from an existing worktree when checking out a new one with wt checkout. Useful for keeping local configuration in sync across worktrees.
[preserve]
patterns = [".env", ".env.*", ".envrc", "docker-compose.override.yml"]
exclude = ["node_modules", ".cache", "vendor"]
- patterns — glob patterns matched against file basenames; only git-ignored files are considered
- exclude — path segments to skip (any component match excludes the file)
Files are copied from the worktree on the default branch, e.g. main (or the first available worktree). Existing files are never overwritten. Symlinks are skipped. Use --no-preserve on wt checkout to skip.
Self-Hosted Instances
[hosts]
"github.mycompany.com" = "github"
"gitlab.internal.corp" = "gitlab"
Theming
Customize the interactive UI with preset themes or custom colors:
[theme]
# Use a preset theme
name = "dracula" # none, default, dracula, nord, gruvbox, catppuccin
# Theme mode: "auto" (detect terminal), "light", or "dark"
mode = "auto"
# Use nerd font symbols (requires a nerd font installed)
nerdfont = true
Override individual colors with hex codes or ANSI color numbers:
[theme]
name = "nord" # Start with a preset
primary = "#88c0d0" # Override specific colors
accent = "#b48ead"
Available color keys: primary, accent, success, error, muted, normal, info, warning.
Per-Repo Config
Place a .wt.toml file in your bare repo root to override global settings for that repo:
wt config init --local # Creates .wt.toml in current repo root
Local settings merge with global config — unset fields inherit from global. Available overrides:
# .wt.toml — per-repo overrides
[checkout]
worktree_format = "{branch}" # replaces global
base_ref = "local" # replaces global
auto_fetch = true # replaces global
set_upstream = true # replaces global
[merge]
strategy = "rebase" # replaces global
[prune]
delete_local_branches = true # replaces global
[forge]
default = "gitlab" # replaces global
[preserve]
patterns = [".env.local"] # appended to global (deduplicated)
exclude = ["dist"] # appended to global (deduplicated)
# Hooks merge by name — add new hooks or override global ones
[hooks.setup]
command = "go mod download"
on = ["checkout"]
# Disable a global hook for this repo
[hooks.npm-install]
enabled = false
Not overridable (global-only): default_sort, default_labels, forge.default_org, forge.rules, hosts, theme.
Writing Hooks
Hooks are shell commands executed via sh -c. Placeholders like {worktree-dir} are replaced with raw text before the command runs — no automatic escaping or quoting is applied.
Quoting Placeholders
Since values are substituted as-is, paths with spaces or special characters will break unquoted placeholders:
# Breaks if path contains spaces
[hooks.unsafe]
command = "code {worktree-dir}"
# Safe — single quotes protect the value
[hooks.safe]
command = "code '{worktree-dir}'"
Note: Single quotes protect against spaces and most special characters, but not against values containing literal single quotes. This is a limitation of raw text substitution.
The same applies to all placeholders ({repo-dir}, {branch}, {repo}, {origin}, {trigger}) and custom --arg variables:
[hooks.claude]
command = "claude '{prompt:-help me}'"
Conditional Placeholders
Use {key:+text} to include text only when an arg is set (and non-empty). This is useful for optional flags:
[hooks.claude]
command = "cd '{worktree-dir}' && claude {skip:+--dangerously-skip-permissions} -p '{prompt:-help}'"
# Without skip — flag omitted
wt hook claude -a prompt="implement auth"
# → cd '/path' && claude -p 'implement auth'
# With skip — flag included (bare -a key sets value to "true")
wt hook claude -a skip -a prompt="implement auth"
# → cd '/path' && claude --dangerously-skip-permissions -p 'implement auth'
Multiline Hooks
Use TOML triple-quoted strings for multi-step hooks:
[hooks.setup]
command = '''
cd '{worktree-dir}'
npm install
npm run build
'''
on = ["checkout"]
Important: Without set -e, intermediate failures are silent — only the exit code of the last command determines whether the hook succeeds or fails. Use set -e to fail fast:
[hooks.setup]
command = '''
set -e
cd '{worktree-dir}'
npm install
npm run build
'''
on = ["checkout"]
Piping Content via Stdin
Use --arg key=- to pipe stdin into a variable:
echo "implement auth" | wt hook claude --arg prompt=-
Multiple keys can read from the same stdin (all keys receive identical content):
cat spec.md | wt hook claude --arg prompt=- --arg context=-
Integration with gh-dash
wt works great with gh-dash. Add a keybinding to checkout PRs as worktrees:
# ~/.config/gh-dash/config.yml
keybindings:
prs:
- key: O
command: wt pr checkout {{.RepoName}} {{.PrNumber}}
Press O to checkout PR → hooks auto-open your editor.
Shell Integration
wt cd prints the worktree path to stdout but can't change your shell's directory on its own. wt init outputs a shell wrapper that intercepts wt cd and performs the actual cd.
# Bash - add to ~/.bashrc
eval "$(wt init bash)"
# Zsh - add to ~/.zshrc
eval "$(wt init zsh)"
# Fish - add to ~/.config/fish/config.fish
wt init fish | source
Shell Completions
Completions are installed automatically when using Homebrew. For manual installs:
# Fish
wt completion fish > ~/.config/fish/completions/wt.fish
# Bash
wt completion bash > ~/.local/share/bash-completion/completions/wt
# Zsh (add ~/.zfunc to fpath in .zshrc)
wt completion zsh > ~/.zfunc/_wt
Development
just build # Build ./wt binary
just test # Run tests
just install # Install to ~/go/bin (+ shell completions)