Manage the per-branch git worktree checkouts of the ~/code workspace
Prebuilt binaries are attached to each release. On macOS — a universal2 binary. With Homebrew:brew tap osss/tools https://gitlab.osss.net/distribution/homebrew-tools.gitbrew install osss/tools/wt
…or with MacPorts — add the osss ports tree once, then install:sudo curl -fsSL https://macports.osss.net/osss-ports.pub -o /opt/local/share/macports/keys/ports/osss-ports.pubecho 'https://macports.osss.net/ports.tar.gz' | sudo tee -a /opt/local/etc/macports/sources.confsudo port sync && sudo port install worktree
A guided tour for someone new to wt: what problem it solves, the one idea you
need to hold in your head, the day-to-day loop, and how the hook system turns it
from a checkout manager into a real part of your workflow. For the exhaustive
flag-by-flag reference, see the README; this guide is the "why"
and the "how it fits together."
A note on command names. Each command's canonical name — what
wt --help, the man page, and completions show — is its full word:new,fetch,rename,list,remove,execute. Most also have a short abbreviation and a single letter (mk/n,get/g,rm/r, …). The forms are interchangeable; this guide uses the short ones, the way you'd actually type them. The full mapping is the alias table in the README.
A plain git checkout has one working tree. Switching branches means switching
that one directory out from under yourself: you stash (or commit) whatever you're
mid-thought on, git switch, and your editor, your running dev server, your
build cache, and your open files all churn to the new branch. Come back an hour
later and you reverse all of it. Reviewing a colleague's branch while your own
work sits half-done is the same dance, with more stashing.
Git's own answer is git worktree — multiple working trees from one repository,
each on its own branch, side by side. No stashing, no churn: the feature you're
building, the bug you're reviewing, and the hotfix you're cutting each get their
own directory, all live at once. But raw git worktree leaves you to invent and
police a directory layout by hand, and it knows nothing about the per-checkout
setup your project needs (an .env, a database, installed dependencies).
wt is the missing manager on top of git worktree. It fixes one layout —
one checkout per branch, each directory named after its branch — and runs
your project's setup/teardown automatically as checkouts come and go. You get the
parallelism without the bookkeeping.
Under each repo directory, every branch lives in its own subdirectory, named after the branch:
myproject/
main/ # the canonical clone — the repo's real .git lives here
casey/feature-x/ # branch casey/feature-x (slashes nest as dirs)
4821-hash-cache/ # branch 4821-hash-cache
Two rules follow from this, and everything wt does depends on them:
casey/feature-x/
is on casey/feature-x, always. wt infers which repo and checkout you mean
from the directory you're standing in, so this mapping has to hold.main/, or whatever the default branch is) plus one linked worktree
per branch.The practical consequence: never git switch or git checkout a checkout onto
a different branch — that silently breaks rule 1. To work on another branch, you
give it its own checkout (that's what wt mk / wt get are for). Likewise, use
wt rather than raw git worktree add / remove, so the layout stays consistent
and your hooks actually run.
wt init git@host:group/myproject.git # clone into the workspace layout → myproject/main/
cd myproject
wt mk casey/feature-x # branch off main into a new checkout, and provision it
wt init is the one workspace-level step: it clones the repo and lays down the
canonical main/ checkout. After that you live in wt mk / wt ls / wt rm.
By default wt roots the workspace at $HOME/code; set CODE_ROOT to put it
elsewhere. It is otherwise root-agnostic — nothing assumes a particular path.
Start a piece of work — branch off the up-to-date default branch into a fresh checkout:
wt mk casey/feature-x # new branch off main (fast-forwarded first)
wt mk hotfix v1.2.0 # …off a tag or commit instead
wt mk casey/part2 casey/part1 # …stacked on another branch
wt mk --cd casey/feature-x # …and cd straight into it (needs the shell wrapper, below)
Review someone else's branch without disturbing your own — fetch it into its own throwaway checkout:
wt get someones/bugfix # check out origin/someones/bugfix for review
wt get -r fork their-feature # …from a fork instead of origin
See what you have — every checkout, its branch, how stale it is, and whether it holds work you haven't pushed:
wt ls # all checkouts here
wt ls -u # only those with unpushed or uncommitted work
wt ls --merged # only branches that have landed (an autoclean preview)
Move between checkouts by name (see "Navigation" below for the one-time setup):
wt cd casey/feature-x # cd into a checkout
wt cd # …or back to the canonical main
Finish up — when a branch has landed, tear its checkout down. wt refuses to
discard unpushed or uncommitted work unless you force it, and never touches the
canonical checkout:
wt rm casey/feature-x # remove the checkout and its local branch
wt destroy casey/feature-x # …and delete the branch on the remote too
wt autoclean # list every merged, clean checkout that could go…
wt autoclean --yes # …and remove them in one sweep
Rename a checkout and its branch together (the directory moves with it, so the dir==branch rule still holds):
wt mv casey/feature-x 4821-hash-cache
The layout is half the value. The other half is hooks — the thing raw
git worktree can't do. A fresh checkout usually isn't ready to work in: it needs
an .env, a database, a node_modules, a target symlink — whatever your
project's "set up a working copy" ritual is. wt runs that ritual for you, at the
right moments:
| Hook | Runs | Use it to |
|---|---|---|
worktree-setup | after wt mk / wt get creates a checkout | provision: copy/generate .env, create a per-checkout database, install deps, link caches |
worktree-teardown | before wt rm / wt destroy / wt autoclean removes one | clean up: drop that database, remove external state the checkout owned |
worktree-rename | after wt mv renames one | migrate per-checkout state keyed on the old name to the new one |
A hook is just an executable file the repo commits at .wt/hooks/<name>. A
repo opts in simply by committing it — no flag, no registration. So your
project-specific setup travels with the project: anyone who clones it with
wt init and runs wt mk gets the same provisioned checkout, automatically.
Each hook is invoked with the checkout's identity in environment variables —
REPO_DIR, CANONICAL_DIR, WORKTREE_DIR, WORKTREE_NAME, BRANCH, and
WORKTREE_KIND (new, fetched, or renamed); the rename hook also gets the
OLD_* trio. A minimal worktree-setup that gives every checkout its own env
file:
#!/bin/sh
# .wt/hooks/worktree-setup — committed in the repo
set -e
cp "$CANONICAL_DIR/.env.example" "$WORKTREE_DIR/.env"
echo "DB_NAME=myapp_${WORKTREE_NAME}" >> "$WORKTREE_DIR/.env"
The teardown counterpart drops what setup created, so a removed checkout leaves
nothing behind. Because the hook reads from the canonical checkout's committed
copy (never the freshly-fetched branch's tree), fetching an untrusted branch with
wt get can never run code from its tree — the hook is always the reviewed one
on the branch you control.
Two commands help you work with hooks:
wt hooks # show which hook resolves for this repo, and from where
wt hooks run worktree-setup # re-run a hook by hand (e.g. after fixing it)
If a setup hook is broken and blocking you, wt mk --skip-hooks creates the
checkout unprovisioned so you can get in and repair it; wt rm --skip-hooks does
the same on the way out.
wt cd)A program can't change its parent shell's directory, so wt cd needs a tiny shell
wrapper. Add it once:
# bash / zsh — in ~/.bashrc or ~/.zshrc:
eval "$(wt shell-init bash)"
# fish — in ~/.config/fish/config.fish:
wt shell-init fish | source
With that in place, wt cd <checkout> changes directory, and wt mk --cd /
wt get --cd drop you into the new checkout as they create it. wt shell-init
covers bash, zsh, fish, elvish, powershell, and nushell; wt completions <shell>
adds tab-completion (including live checkout names).
A few commands keep a multi-checkout, multi-repo workspace tidy:
wt sync # fetch every repo here (refresh the wt ls staleness/ahead-behind counts)
wt doctor # check the invariants — flags any dir!=branch drift, orphaned worktrees, etc.
wt doctor --fix # …and auto-remediate the safe findings
wt exec -- git fetch # run a command in every checkout (a multi-worktree fan-out)
wt doctor is worth running if something feels off: it's the tool that catches a
checkout whose directory no longer matches its branch (the one way to break the
core invariant), and --fix cleans up the safe cases.
wt ls
semantics.ai_agents.md — a paste-ready block that teaches it the same
invariant and "use wt, not raw git" rules this guide opens with.