A bare repo as the worktree root
I stopped treating branches as states of one directory.
The usual workflow asks one checkout to be everything at once: switch branches, stash unfinished work, rebuild dependencies, and remember what was running before the switch. Git worktrees fix the underlying problem by giving each branch its own directory while sharing one object store.
A normal clone still leaves one checkout in a special position: the repository
lives inside its .git directory while every additional worktree lives
somewhere else. I prefer a symmetrical layout instead. The repository itself is
bare, and every checkout hangs from it:
~/repos/sageveil.git/
├── main/
├── feat/pi-port/
└── docs/screenshot-backgrounds/
No checkout owns the repository. They are all disposable views over the same history.
I previously wrapped this setup in a shared justfile and a large helper named
git-worktree-tms. It worked, but the interface leaked the implementation:
every repository needed an import, and I had separate recipes for opening,
listing, picking, refreshing, and removing worktrees.
The replacement is one command: wt.
Initialize once
wt init git@github.com:sageveil/sageveil.git
That command:
- clones the repository bare into
~/repos/sageveil.git - restores the remote fetch refspec that bare clones omit
- fetches
origin - wires the default local branch to
origin/HEAD - runs the post-init hook
The Git setup it hides is still worth knowing:
git clone --bare <repo-url> ~/repos/<name>.git
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git fetch --prune origin
git branch --set-upstream-to=origin/main main
Without that refspec, remote branches do not appear under origin/*, which
makes worktree creation feel mysteriously broken.
My post-init hook opens the new repository through tmux-sessionizer. TMS notices that the repository is bare and creates the default branch worktree as the first tmux window.
One sharp edge: TMS matches excluded_dirs against the full path as a
substring. Excluding .git also excludes a bare repository named
sageveil.git. I removed .git from that list; normal repositories are found
before TMS ever needs to descend into their metadata directory.
One interface for daily work
Create or open a worktree by branch name:
wt feat/pi-port
wt fetches first, then does the least surprising thing:
- an existing local branch is checked out
- an
originbranch becomes a local tracking branch - an unknown branch is created from
origin/HEAD - a branch already checked out simply returns its existing path
Run it without arguments to pick from all available local and remote branches:
wt
The fzf list is deduplicated and excludes branches that already have a
worktree.
The rest of the interface is deliberately small:
wt list # list worktrees
wt rm feat/pi-port # remove a clean worktree and its local branch
wt rm # pick multiple worktrees with fzf
wt rm topic --force # remove a dirty worktree deliberately
wt rm --force # force-remove selected worktrees
Removal never deletes the remote branch. A dirty checkout requires --force,
so the short command is not a shortcut around data-loss protection.
Hooks instead of hard-coded integrations
The worktree lifecycle and my tmux setup are separate concerns. wt sources
$XDG_CONFIG_HOME/wt/hooks and recognizes:
wt_pre_init wt_post_init
wt_pre_add wt_post_add
wt_pre_rm wt_post_rm
Each hook receives the repository root, repository name, branch, and worktree path through environment variables. A failing pre-hook aborts the operation; a failing post-hook reports failure.
My defaults are small:
wt_post_initopens the new TMS sessionwt_post_addandwt_post_rmruntms refreshwt_pre_rmcloses tmux windows rooted in the worktree being removed
The Git tool remains useful without tmux, and changing session managers no longer means rewriting worktree logic.
Worktrees make agent handoffs boring
The same layout is useful for coding agents. My handoff skill creates a new
branch from the current branch, asks wt for a fresh worktree, and places an
untracked handoff.md there with the goal, current state, decisions, next
steps, validation, and relevant files.
The next agent starts in an isolated checkout with explicit context instead of inheriting a long conversation and a dirty directory. The only important rule is to hand off committed state: a new branch cannot contain changes that exist only in another worktree’s index or working directory.
The result
The old version automated Git commands. The new version gives the workflow one small interface.
Repositories are initialized consistently. Branch discovery is interactive. Destructive removal is guarded. Tmux integration lives in hooks. Agent sessions can move between worktrees with a plain Markdown handoff.
No per-repository justfile, no special main checkout, and no pile of commands to remember.
The implementation lives in my dotfiles:
wt.bash— worktree lifecycle and hook runnerhandoffskill — fresh-worktree agent handoffs