Git worktree manager with GitHub/GitLab integration.
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
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 -sand update to match newer config format
# Homebrew (macOS/Linux)
brew install raphi011/tap/wt
# Go
go install github.com/raphi011/wt/cmd/wt@latestRequires git in PATH. For GitHub repos: gh CLI. For GitLab repos: glab CLI.
Set up shell completions and the shell wrapper so that wt cd can change your directory. See Shell Integration for instructions.
wt config init # Create ~/.wt/config.toml
wt config init -s # Print default config to stdout (for review)The most important setting is checkout.worktree_format — it controls where worktrees are placed. The format supports {repo} and {branch} placeholders, and the path prefix determines placement:
[checkout]
# Nested with subfolder (default): ~/Git/myrepo/.worktrees/feature-branch
worktree_format = ".worktrees/{branch}"
# Nested inside repo: ~/Git/myrepo/myrepo-feature-branch
worktree_format = "{repo}-{branch}"
# Sibling to repo dir: ~/Git/myrepo-feature-branch
worktree_format = "../{repo}-{branch}"
# Centralized folder: ~/worktrees/myrepo-feature-branch
worktree_format = "~/worktrees/{repo}-{branch}"
# Absolute path: /tmp/worktrees/myrepo-feature-branch
worktree_format = "/tmp/worktrees/{repo}-{branch}"# Register a repo you already have cloned
wt repo add ~/path/to/myrepo
# Or clone and register a new repo (clones into current directory)
wt repo clone git@github.com:org/repo.gitRepos are also auto-registered the first time you run wt checkout inside one.
# From inside a registered repo
wt checkout -b new-branch # Create worktree with new branch (from default branch)
# From anywhere — target a repo by name (as shown in wt repo list)
wt checkout -b myrepo:new-branchHooks run automatically when creating, opening, merging, or removing worktrees. Add them to ~/.wt/config.toml:
# Open VS Code after every checkout
[hooks.vscode]
command = "code '{worktree-dir}'"
on = ["checkout"]
# Open a new terminal tab (kitty example)
[hooks.kitty]
command = "kitty @ launch --type=tab --cwd='{worktree-dir}'"
on = ["checkout"]
# Run Claude to review PR when checking out a PR
[hooks.claude-review]
command = "claude -p 'review this PR'"
on = ["checkout:pr"]
# Manual-only hook — only runs via: wt hook claude --arg prompt="..."
[hooks.claude]
command = "claude '{prompt:-help me}'"See Hooks and Writing Hooks for the full placeholder reference and advanced patterns.
wt list -gYou're ready to go! Most commands also support -i for an interactive wizard mode (e.g. wt checkout -i).
# 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# 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 gitlabView 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 repoAfter 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# 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# See what worktrees exist
wt list
# Remove merged worktrees (detects PR merges and local merges via ancestry)
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 -fLabel 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 worktreeNote:
wt cdprints the path but can't change your shell directory. Add the shell wrapper from Shell Integration to usewt cddirectly.
# 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 .# Run a hook on current worktree
wt hook vscode
# Run on specific worktree ([scope:]branch format)
wt hook myrepo:feature vscode
# Run across worktrees by label
wt hook backend:main build
# Pass custom variables
wt hook claude --arg prompt="implement feature X"
# Preview command without executing
wt hook vscode -d# 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:featureGlobal 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# 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}
worktree_format = ".worktrees/{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
# Days before a worktree's commit age is highlighted as stale (default: 14, 0 = disabled)
# stale_days = 14Base 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 |
See Getting Started > Configure Hooks for examples. Each hook has a command, optional description, and optional on triggers.
Triggers — syntax for the on field: [before:|after:]trigger[:subtype]
| Trigger | Subtypes | Description |
|---|---|---|
checkout |
create, open, pr |
Worktree checkout |
prune |
— | Worktree removal |
merge |
— | PR merge |
all |
— | Matches all triggers |
Timing prefix:
| Prefix | Default | Description |
|---|---|---|
| (none) | after |
Runs after the operation |
after: |
— | Explicit after (same as no prefix) |
before: |
— | Runs before the operation; non-zero exit aborts |
Examples:
on = ["checkout"] # All checkouts (after)
on = ["checkout:pr"] # PR checkouts only
on = ["before:prune"] # Pre-prune guard (can abort)
on = ["before:checkout:pr"] # Before PR checkout only
on = ["checkout", "merge"] # Multiple triggersHooks without on only run when invoked explicitly via wt hook <name> or --hook <name>.
Placeholders — substituted in the hook command before execution:
| Placeholder | Description |
|---|---|
{worktree-dir} |
Absolute path to the worktree |
{repo-dir} |
Absolute path to the main repo (bare root or .git parent) |
{branch} |
Branch name |
{repo} |
Repo name (as registered in wt repo list) |
{trigger} |
Command that triggered the hook (checkout, prune, merge, run) |
{action} |
Checkout subtype: create, open, pr, or manual (for wt hook) |
{phase} |
Hook timing: before or after |
{config-dir} |
Absolute path to the wt config directory (~/.wt/) |
{pr-number} |
PR/MR number (empty for non-PR checkouts) |
{pr-repo} |
Forge repo path, e.g. owner/repo (empty for non-PR checkouts) |
{key} |
Custom variable from --arg key=value (empty if unset) |
{key:-default} |
Custom variable with fallback value if unset |
{key:+text} |
Expands to text if key is set and non-empty, otherwise empty |
Args: Pass --arg key=value or --arg key (bare boolean, sets to "true")
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]
strategy = "squash" # squash, rebase, or mergeSymlink files from the repo root into new worktrees created with wt checkout. Useful for keeping local configuration (.env, .envrc, etc.) in sync across worktrees — edits in any worktree are instantly visible in all others.
[preserve]
paths = [".env", ".envrc"]- paths — relative paths from the repo root to symlink (e.g.,
".env","config/.env")
Paths that don't exist in the repo root are silently skipped. Existing files in the target worktree are never overwritten. Use --no-preserve on wt checkout to skip.
[hosts]
"github.mycompany.com" = "github"
"gitlab.internal.corp" = "gitlab"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 = trueOverride 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.
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 rootLocal 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]
paths = [".env.local"] # 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 = falseNot overridable (global-only): default_sort, default_labels, forge.default_org, forge.rules, hosts, theme.
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.
Hooks run with a working directory that depends on the command and phase:
| Command | before CWD |
after CWD |
|---|---|---|
checkout |
Worktree directory | Worktree directory |
checkout:pr (via wt pr checkout) |
Repo root | Repo root |
prune |
Worktree directory (still exists) | Repo root (worktree deleted) |
merge |
Repo root | Repo root |
For checkout hooks, the worktree already exists when before hooks run. A failing before hook aborts the command but does not roll back the worktree.
Since the working directory is already set, cd '{worktree-dir}' is unnecessary in checkout hooks. For other commands, use {worktree-dir} or {repo-dir} placeholders if you need a specific directory.
Hooks run in alphabetical order by name. Use naming prefixes to control ordering:
[hooks.01-install]
command = "npm install"
on = ["checkout"]
[hooks.02-lint]
command = "npm run lint"
on = ["checkout"]
[hooks.99-open-editor]
command = "code '{worktree-dir}'"
on = ["checkout"]TUI programs (editors, claude, interactive CLIs) work as hooks because they inherit the terminal's stdin/stdout/stderr. Place them last alphabetically so non-interactive hooks complete first.
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}, {trigger}) and custom --arg variables:
[hooks.claude]
command = "claude '{prompt:-help me}'"Use {key:+text} to include text only when an arg is set (and non-empty). This is useful for optional flags:
[hooks.claude]
command = "claude {skip:+--dangerously-skip-permissions} -p '{prompt:-help}'"# Without skip — flag omitted
wt hook claude -a prompt="implement auth"
# → claude -p 'implement auth'
# With skip — flag included (bare -a key sets value to "true")
wt hook claude -a skip -a prompt="implement auth"
# → claude --dangerously-skip-permissions -p 'implement auth'Use TOML triple-quoted strings for multi-step hooks:
[hooks.setup]
command = '''
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
npm install
npm run build
'''
on = ["checkout"]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=-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.
# Fish - add to ~/.config/fish/config.fish
wt init fish | source
# Bash - add to ~/.bashrc
eval "$(wt init bash)"
# Zsh - add to ~/.zshrc
eval "$(wt init zsh)"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 — ensure ~/.zfunc exists and is on fpath, then generate
mkdir -p ~/.zfunc
echo 'fpath=(~/.zfunc $fpath)' >> ~/.zshrc # add once, before compinit
wt completion zsh > ~/.zfunc/_wtwt 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.
just build # Build ./wt binary
just test # Run tests
just install # Install to ~/go/bin (+ shell completions)