Manage the per-branch git worktree checkouts of the ~/code workspace
Prebuilt binaries are attached to each release.
Debian/Ubuntu — add the osss apt repository once, then install as root (sudo, doas, whatever you use) — provides the wt command:
apt install worktreeFedora/RHEL — add the osss RPM repository once, then install as root (sudo, doas, whatever you use) — provides the wt command:
dnf install worktreeA universal2 binary. With Homebrew:
brew tap osss/tools https://gitlab.osss.net/distribution/homebrew-tools.git
brew install osss/tools/worktree…or with MacPorts — add the osss ports tree once, then install as root (sudo, doas, whatever you use):
port install worktreeA 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)
"Up-to-date" is enforced, not assumed. Branching off a branch that tracks a
remote — wt mk 42-thing main in a repo whose canonical checkout sits on some
other branch — refreshes that branch first and refuses if it has fallen behind,
rather than quietly rooting the work at a stale base and leaving you to find out
when a merge request reports itself commits behind. Tags, commits, and purely
local branches are fixed points, so they branch as-is.
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, whether it holds work you haven't pushed, and whether it has fallen behind its upstream:
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)
A checkout whose upstream has moved on reads 3 behind (or
diverged (1 ahead, 3 behind) if both moved). That is the state wt mk refuses
to branch off, so wt sync && wt ls tells you what needs refreshing before you
start — the counts are read from the last fetch, so sync first.
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
A freshly wt mk'd checkout with no commits yet is excluded from autoclean --yes
by default — its branch hasn't diverged from default, which looks identical to
"already landed" but usually means "about to be worked on." Pass
--include-unstarted to sweep those too.
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 .worktree/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
# .worktree/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.
A fresh checkout holds only tracked files, so everything gitignored — target/,
node_modules/, .venv/ — starts empty and the first build in a new checkout is
a cold one. Copying those artifacts from the canonical checkout is far cheaper
than recreating them: for a Rust project of ~100 dependencies, a cold
cargo build --release takes tens of seconds while copying the whole accumulated
target/ takes one or two, even on a filesystem with no copy-on-write support at
all.
Seeding is a worktree-setup job, and each platform's own cp already picks the
best mechanism available — a reflink where the destination filesystem offers one,
a full copy where it doesn't:
#!/bin/sh
# .worktree/hooks/worktree-setup — seed build caches into a new checkout.
set -e
for path in target; do
src="$CANONICAL_DIR/$path"
dst="$WORKTREE_DIR/$path"
[ -e "$src" ] || continue # nothing built yet in the canonical checkout
[ -e "$dst" ] && continue # already present; never clobber
case "$(uname -s)" in
Linux) cp -a --reflink=auto "$src" "$dst" ;;
Darwin) cp -Rpc "$src" "$dst" ;;
*) cp -a "$src" "$dst" ;; # FreeBSD: copy_file_range clones transparently
esac
done
The two guards matter as much as the copy. Skipping an absent source keeps
wt mk working in a repo whose canonical checkout has never been built — a
failing setup hook rolls the whole checkout back. Skipping an existing
destination makes the hook safe to re-run with wt hooks run worktree-setup.
On the mechanism. cp --reflink=auto (Linux) and cp -c (macOS) request a
copy-on-write clone, and FreeBSD's cp gets one through copy_file_range(2)
without a flag; each falls back to a full copy when the filesystem can't oblige,
which is why the recipe needs no fallback of its own. On macOS that fallback is
silent — copying onto a non-APFS volume exits zero and lands the whole file — so
a guard around cp -c would never fire.
Whether it can is not something the filesystem's name answers — XFS clones only
when it was made with reflink=1, and ZFS only when block cloning is enabled
(vfs.zfs.bclone_enabled on FreeBSD, zfs_bclone_enabled on Linux), which
OpenZFS still ships off by default. The kernel decides per file, so the hook
needs no detection of its own; a clone saves the disk space, and where you don't
get one the copy is still far cheaper than rebuilding.
Seed only what survives the move. List the paths deliberately rather than sweeping everything gitignored: a Python virtualenv bakes in absolute paths and breaks when copied, and stale artifacts are worth less than the space they take. Build caches, dependency trees, and generated assets too large for git are the paths that pay.
Because hooks receive CODE_ROOT, several repos can share one copy of this
instead of each carrying its own — keep the body in the workspace meta-repo and
have each repo's hook source it:
#!/bin/sh
. "$CODE_ROOT/.worktree/lib/seed.sh"
seed target
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.
agent_workspace_templateEverything above manages worktrees within one repo. If you're juggling a
dozen-plus repos side by side — the common shape for an agent-driven,
multi-project workspace — wt alone doesn't give the top-level directory
holding them any structure of its own; each session still opens cold, with no
sense of what the repos are or how they relate.
agent_workspace_template
is a meta-repo skeleton that sits above your checkouts and fills that gap: a
workspace overview, one short self-contained brief per project, and a
mechanism that auto-loads only the brief a coding-agent session is actually
touching — without turning into a stale second copy of each repo's own
conventions. It uses wt as its per-project checkout manager, so every
project directory underneath already follows the one-checkout-per-branch
layout this guide describes; the template just gives the directory above
them a spine. If you've outgrown a single AGENTS.md for your whole
workspace, it's worth a look.
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.