Because git worktree add should be smarter than you are at 3am.
A CLI tool that makes git worktrees actually usable for parallel agent workflows. It copies or renders your .envrc when you spawn a worktree and cleans up your mess when you delete one.
git wt-add → "PORT [8080] = ? SECRET [default] = ?"
git worktree add → "here's your .envrc, I already direnv-allow'd it, you're welcome"
git wt-remove → "let me run your cleanup script before I nuke this"
No more forgetting to copy .envrc. No more orphaned docker containers named dev-feature-branch-42 haunting your machine like digital ghosts. git wt-add creates worktrees with .envrc auto-installed and direnv allow run — and for template repos, it prompts for vars interactively before creating the worktree.
- direnv — for loading
.envrc(optional but recommended, like seatbelts) - jq — only if you're doing the juju thing below
- A git repository — presumably you have one of these
go install github.com/gfouillet/wt-helper@latestOr if you like to live dangerously:
git clone https://github.com/gfouillet/wt-helper.git
cd wt-helper
go build -o wt-helper .
mv wt-helper ~/.local/bin/Point it at a repo and tell it what to copy and what to run on cleanup:
# The full monty (template + wrapup)
wt-helper setup --envrc-file .envrc.template --vars-file .wt-helper.vars.json --wrapup-cmd ./scripts/cleanup.sh
# Plain envrc (no template, just copy)
wt-helper setup --envrc-file .envrc
# Just wrapup (I'm a chaos agent who doesn't use direnv)
wt-helper setup --wrapup-cmd ./scripts/cleanup.sh
# Point at a different repo (because you have many repos, you overachiever)
wt-helper setup --target /path/to/repo --envrc-file .envrc --wrapup-cmd ./cleanup.sh
# "I know what I'm doing, overwrite my hooks"
wt-helper setup --envrc-file .envrc --wrapup-cmd ./cleanup.sh --forceIf your .envrc contains {{, wt-helper treats it as a Go template. Each worktree gets its own rendered values — unique ports, unique DB names, unique everything.
# With a vars file (JSON, TOML, or YAML)
wt-helper setup --envrc-file .envrc.template --vars-file .wt-helper.vars.json
# Auto-detected vars file in repo root (.wt-helper.vars.{json,toml,yaml})
wt-helper setup --envrc-file .envrc.template
# Agent swarm mode: fail if any var is missing (no interactive prompts)
wt-helper setup --envrc-file .envrc.template --vars-file agent-1.vars.json --auto
# Prompt for missing vars interactively
wt-helper setup --envrc-file .envrc.template
# → PORT = 8080
# → DB_DRIVER = sqliteBuilt-in variables (always available):
| Variable | Example |
|---|---|
{{.WorktreeName}} |
feature-auth |
{{.WorktreePath}} |
/home/you/repos/../feature-auth |
{{.BranchName}} |
feature-auth |
Vars file formats (auto-detected by extension):
// .wt-helper.vars.json
{"PORT": "8080", "DB_DRIVER": "sqlite"}# .wt-helper.vars.toml
PORT = "8080"
DB_DRIVER = "sqlite"# .wt-helper.vars.yaml
PORT: "8080"
DB_DRIVER: "sqlite"Some directories are .gitignored but needed in every worktree — build caches (.go, .npm), dependencies (node_modules, vendor), etc. wt-helper can symlink or deep-copy them automatically.
# Symlink .go and node_modules into every new worktree
wt-helper setup --envrc-file .envrc --dep-dir .go --dep-dir node_modules
# Deep copy instead of symlink (for dirs that break with symlinks)
wt-helper setup --envrc-file .envrc --dep-dir .go --dep-dir node_modules --dep-copy
# Interactively pick which dirs to link (tab-completion available)
wt-helper setup --envrc-file .envrc --dep-interactiveAt worktree creation, the hook creates the symlinks/copies AFTER installing .envrc and BEFORE direnv allow. If the directory already exists in the worktree, it is overwritten.
Completion for --dep-dir lists directories in the target repo:
wt-helper setup --dep-dir <TAB>
# → .go node_modules vendor .cache# Template repos — interactive creation, prompts for vars
git wt-add ../my-feature -b feature-branch
# PORT [8080] = ← press Enter to keep default
# DB_DRIVER [sqlite] = ← press Enter to keep default
# → prepares vars, creates worktree, renders .envrc, direnv allow
# Override vars via env — skipped from prompts
SECRET=agent-42 git wt-add ../my-auth -b auth-work
# PORT [8080] = ← only PORT prompts, SECRET skipped
# Plain repos — .envrc copied verbatim, no prompts
git wt-add ../my-plain -b plain-branch
# → wt-helper: .envrc installed and allowed
# Work on your feature...
# Clean up — wrapup runs, worktree goes away
git wt-remove ../my-feature
# → wt-helper: running wrapup for .../my-feature...
# → (docker stops, databases drop, angels weep)
# → wt-helper: worktree removed
# Force delete when git complains about untracked files
# (we've all been there)
git wt-remove ../my-feature --forceBecause tab-completing is the closest most of us will get to telekinesis:
# bash
wt-helper completion bash > /etc/bash_completion.d/wt-helper
# zsh
wt-helper completion zsh > "${fpath[1]}/_wt-helper"
# fish
wt-helper completion fish > ~/.config/fish/completions/wt-helper.fishwt-helper setup makes the repo self-contained:
- Copies your
.envrc(or template) into.git/envrc-source - If template: copies your vars file into
.git/worktree-helper-vars.{json,toml,yaml} - If template: renders a default
.envrcin the repo root (git-ignored) - Writes config to
.git/worktree-helper.conf - Installs a
post-checkouthook - Installs a
git wt-removealias - Installs a
git wt-addalias - If
--dep-dirspecified: stores dependency dirs in config for linking/copying into new worktrees
git wt-add is the recommended way to create worktrees. For template repos, it handles vars BEFORE spawning the worktree:
- Reads the default vars file from
.git/worktree-helper-vars.{format} - Prompts for ALL custom vars, showing defaults in brackets
- Any env var already set (
PORT=9090) skips the prompt for that var - Writes merged vars to a temp file, sets
WT_VARS_FILEenv var - Runs
git worktree add→ triggers the hook below - Cleans up the temp file
Triggered by ANY git worktree add (or git wt-add). The hook:
- Detects new worktrees (null SHA trick — don't ask)
- Reads
.git/worktree-helper.confto decide what to do - Checks
$WT_VARS_FILE— if set bygit wt-add, uses it instead of the default vars file - If
is-template = true: callswt-helper render-templatewith the vars file - If
is-template = false: copies.git/envrc-sourceverbatim - Creates symlinks (or deep copies) for configured dependency dirs
- Runs
direnv allowso you don't have to type it like an animal - If the worktree already has an
.envrc, asks before overwriting (it's polite like that)
The hook uses marker comments (# >>> wt-helper start / # <<< wt-helper end) so re-running setup doesn't clobber your other hooks. It's surgical. It's precise. It's better than you at managing hooks.
Git has no hook for worktree deletion, which is rude. So wt-helper installs a git wt-remove alias that:
- Reads your wrapup command from
.git/worktree-helper.conf - Runs it through
bash -c(sources your~/.profileso shell functions work) - If the wrapup fails, aborts — your cleanup had one job
- Runs
git worktree removeand reports back - Forwards extra flags (
--force,-f) togit worktree remove
| What you pass | What happens | Example |
|---|---|---|
| A file path | Validated at setup (must exist + be executable) | ./scripts/cleanup.sh |
| A command name | Stored as-is, resolved at runtime via PATH | docker |
| A shell function | Stored as-is, resolved via ~/.profile at runtime |
juju-kill-all |
The wrapup command receives the worktree absolute path as $1. Use it however you want. We don't judge.
Setup writes .git/worktree-helper.conf:
# Generated by wt-helper — do not edit manually
main-repo = /home/you/repos/my-project
envrc-source = /home/you/repos/my-project/.git/envrc-source
wrapup-cmd = ./scripts/cleanup.sh
vars-file = /home/you/repos/my-project/.git/worktree-helper-vars.json
helper-bin = /home/you/.local/bin/wt-helper
is-template = trueYou can peek at it. You can even edit it. We said "do not edit" but we're not your parents.
my-repo/
├── .envrc # rendered default (git-ignored)
├── .gitignore # updated to include .envrc
├── .git/
│ ├── envrc-source # your template or plain .envrc
│ ├── worktree-helper.conf # config
│ └── worktree-helper-vars.json # your vars (if template)
└── ...
# Each worktree gets the same .envrc, shell variables resolve at direnv load time
export PORT=${PORT:-8080}
export DATABASE_URL="sqlite:///tmp/$(basename $PWD).db"
export GOPATH="$PWD/.go"
export PROJECT_NAME="$(basename $PWD)"# .envrc.template — rendered per worktree with Go templates
export PORT={{.PORT}}
export DATABASE_URL="{{.DB_DRIVER}}:///tmp/{{.WorktreeName}}.db"
export WORKTREE={{.WorktreeName}}
export BRANCH={{.BranchName}}
export GOPATH="{{.WorktreePath}}/.go"With this vars file:
{"PORT": "8080", "DB_DRIVER": "sqlite"}Agent 1 creates a worktree:
git wt-add ../agent-1 -b feature-auth
# PORT [8080] = press Enter
# DB_DRIVER [sqlite] = postgres type overrideAgent 1's rendered .envrc:
export PORT=8080
export DATABASE_URL="postgres:///tmp/agent-1.db"
export WORKTREE=agent-1
export BRANCH=feature-authAgent 2 passes vars via env:
PORT=9090 git wt-add ../agent-2 -b fix-bug
# DB_DRIVER [sqlite] = press Enter (PORT skipped — in env)Agent 2's rendered .envrc:
export PORT=9090
export DATABASE_URL="sqlite:///tmp/agent-2.db"
export WORKTREE=agent-2
export BRANCH=fix-bugSame template, different worktrees. No port conflicts, no shared state, no therapist bills.
#!/bin/sh
# scripts/cleanup.sh — called with worktree path as $1
WORKTREE="$1"
NAME=$(basename "$WORKTREE")
echo "Cleaning up $NAME..."
docker stop "dev-$NAME" 2>/dev/null
docker rm "dev-$NAME" 2>/dev/null
rm -f "/tmp/$NAME.db"
echo "$NAME is gone. Like it never existed."# In ~/.profile — because sometimes your cleanup IS a shell function
function juju-kill-all() {
juju controllers --format=json | jq -r '.controllers | keys[]' | xargs -I{} juju kill-controller --no-prompt {}
}wt-helper setup --wrapup-cmd juju-kill-all
# Now: git wt-remove tears down all juju controllers before removing the worktree
# Use responsibly. Or don't. We're not your conscience.Q: Why not just use git worktree add and copy .envrc manually?
A: Why wash dishes manually when you have a dishwasher?
Q: What's the difference between git wt-add and git worktree add?
A: git wt-add is the recommended way to create worktrees. In template mode, it prompts for vars interactively before creating the worktree. In plain mode, it works like git worktree add but you get a consistent interface either way. Both trigger the same post-checkout hook that installs .envrc and runs direnv allow. You can still use git worktree add directly if you prefer — the hook runs either way.
Q: What happens if I run setup twice?
A: It updates the hook in place (markers, remember?). No duplicates. It's idempotent, like a well-behaved function should be.
Q: Can I use this with my existing post-checkout hook?
A: Yes. wt-helper appends its section between markers. Your existing hook content is preserved. Two hooks, one file, no drama.
Q: git wt-remove says "wrapup failed" — what gives?
A: Your wrapup command exited non-zero. Fix your script, or the worktree stays. wt-helper has trust issues, and that's a feature.
Q: Does this work on macOS? A: Probably. We haven't tested it. If it doesn't, open an issue and we'll pretend to be surprised.
Q: Can I use templates with shell variables like $(basename $PWD) instead?
A: Yes, that's the "plain copy" mode. If your .envrc doesn't contain {{, it's copied verbatim and shell variables are evaluated by direnv at load time. Templates are for when you need values resolved at worktree creation time (e.g., reading from a config file).
Q: What if I have both a .wt-helper.vars.json and a .wt-helper.vars.yaml in my repo?
A: JSON wins. The detection order is: .json > .toml > .yaml > .yml. Or just use --vars-file explicitly and stop confusing the tool.
Q: How do env var overrides work?
A: Any env var matching a template variable name skips the interactive prompt in git wt-add. The env var value is used automatically. For plain git worktree add, the post-checkout hook overlays env vars when calling render-template.