Spin up multiple local sandboxes of the same repo, without port collisions, without copy-pasting env vars, and without accidentally committing your "agent #7" Supabase changes.
I like running multiple branches at once. Humans do it, agents do it, and modern dev stacks absolutely hate it.
The pain is always the same:
- you create a second
git worktree - you run
dev - everything collides on ports, containers, project ids, and "helpful" config files
The goal: a tiny tool that turns "parallel work" into a one-liner.
The vibe: keep it boring and deterministic. When it touches your repo, it should be obvious what happened.
| Worktree orchestration | Creates feature worktrees as siblings of main inside a managed root |
| Managed roots | Can clone/bootstrap <root>/.git as a bare common repo with <root>/main and <root>/<feature> checkouts |
| Port block reservation | Allocates a unique WRT_PORT_BLOCK per worktree (offset = block * 100) |
| Shell-friendly env | Writes .wrt.env into each worktree and can print export ... lines for your shell |
| Supabase sharing + isolation | Starts one main stack, lets features share it or create isolated stacks, and supports nested config paths |
| Run inside worktree | wrt run <name> -- ... runs a command in that worktree with WRT_* set |
| State tracking | Tracks worktrees in <git-common-dir>/.wrt/state.json and can prune missing entries |
| Repo discovery (optional) | wrt init can call the Codex CLI to generate managed-root .wrt.json conventions |
# install locally
cargo install --path .
# clone into a managed root
wrt clone git@github.com:org/app.git --install false
cd app
# optional: generates shared .wrt.json via Codex at the managed root
cd main
wrt init
cd ..
wrt new a/gpt/login-timeout
# jump into it
cd "$(wrt path a-gpt-login-timeout)"
# load the worktree's reserved port block
eval "$(wrt env)"
echo "$WRT_PORT_OFFSET"Managed-root layout:
wrt clone git@github.com:org/app.git --install false
cd app
wrt new a/gpt/login-timeout
cd "$(wrt path a-gpt-login-timeout)"- Zsh completions (manual
fpath)
mkdir -p ~/.zsh/completions
wrt completions zsh > ~/.zsh/completions/_wrt
echo 'fpath=(~/.zsh/completions $fpath)\nautoload -Uz compinit && compinit' >> ~/.zshrcRestart your shell or exec zsh.
- Oh My Zsh plugin
mkdir -p ~/.oh-my-zsh/custom/plugins/wrt
wrt completions zsh > ~/.oh-my-zsh/custom/plugins/wrt/_wrt
cat > ~/.oh-my-zsh/custom/plugins/wrt/wrt.plugin.zsh <<'EOF'
fpath=(${0:A:h} $fpath)
EOFAdd wrt to plugins=(...) in ~/.zshrc, then restart your shell.
Zsh convenience wrapper (auto-cd on wrt new):
wrt() {
if [[ "$1" == "new" ]]; then
eval "$(command wrt "$@" --cd)"
else
command wrt "$@"
fi
}Run a command inside the worktree (without cd):
wrt run a-gpt-login-timeout -- sh -lc 'echo $WRT_NAME && env | rg ^WRT_'wrt init [--force] [--print] [--model <codex-model>]
wrt clone <git-repo-url> [--root <dir>] [--main <branch>] [--install auto|true|false] [--supabase auto|true|false] [--supabase-config <path>] [--db auto|true|false]
wrt root init <source> --root <dir> [--main <branch>] [--install auto|true|false] [--supabase auto|true|false] [--supabase-config <path>] [--db auto|true|false]
wrt root status
wrt new <name> [--from <ref>] [--branch <branch>] [--install auto|true|false] [--supabase auto|shared|isolated|none] [--supabase-config <path>] [--db auto|true|false] [--cd]
wrt add <name> [--from <ref>] [--branch <branch>] [--install auto|true|false] [--supabase auto|shared|isolated|none] [--supabase-config <path>] [--db auto|true|false] [--cd]
wrt db [<name>] reset|seed|migrate [--print]
wrt ls
wrt path <name>
wrt env [<name>]
wrt rm <name> [--force] [--delete-branch]
wrt remove <name> [--force] [--delete-branch]
wrt prune
wrt housekeeping [--apply]
wrt run <name> -- <command> [args...]
wrt completions zsh
Examples:
# create from a ref (default is HEAD)
wrt new perf/agent-01 --from origin/main
wrt add perf/agent-02 --from origin/main
# create a managed root with sibling worktrees
wrt clone git@github.com:org/app.git --install false
cd app
wrt new perf/agent-01
# create a managed root from an existing local checkout
wrt root init . --root ../my-repo-wrt --install false
cd ../my-repo-wrt
wrt root status
wrt new perf/agent-01
# create and jump into it (shell integration)
eval "$(wrt new a/gpt/login-timeout --cd --install false)"
# keep the directory slugged but force a branch name
wrt new "Agent 02: API cleanup" --branch agent/api-cleanup
# explicitly create an isolated feature stack
wrt new x --supabase isolated
# explicitly skip Supabase for this worktree
wrt new docs-only --supabase none
# persist a nested Supabase project path for main and future worktrees
wrt clone git@github.com:org/monorepo.git --supabase-config apps/api/supabase/config.toml
# remove worktree (and optionally the branch ref)
wrt rm x --force
wrt remove x --force
wrt rm x --force --delete-branch
# prune stale state entries after manual deletions
wrt prune
# dry-run branch cleanup; add --apply to delete candidates
wrt housekeeping- Worktree paths
wrt new <name>creates the slug as a sibling ofmain(example:<root>/a-gpt-fix-login-timeout)wrt clone <source>derives<root>from the repo URL, then creates<root>/.gitand<root>/mainwrt root init <source> --root <dir>creates<dir>/.gitas the bare common repo and<dir>/mainas block0- branch names keep slashes, but spaces are normalized to
-
- State
- tracked in
<git-common-dir>/.wrt/state.json(usually.git/.wrt/state.json) - block
0is reserved for the main workdir; first worktree usually gets block1=> offset100 - Supabase ownership and the repo-relative config path are persisted; credentials are not
- tracked in
- Environment
.wrt.env,wrt env, andwrt runshare one environment resolver- generated vars include
WRT_ROOT,WRT_WORKTREE_PATH,WRT_MAIN_PATH,COMPOSE_PROJECT_NAME, and discovered service port/url vars from checkout-local.wrt.jsonor the managed-root.wrt.json - Supabase values are read from
supabase status -o jsonand synchronized into generated blocks in.envand the Supabase workdir's.env - if an
.envis tracked, wrt leaves it unchanged and writes the generated block to.env.local
- Git excludes
wrtappends these to.git/info/excludeto reduce accidental commits:.env.env.local.wrt.env.wrt.json
Supabase patching details
When clone detects a Supabase config, it starts one unpatched stack from main. wrt new/wrt add asks whether to create an isolated stack; answering no reuses main. In non-interactive use, auto reuses main when available and otherwise disables Supabase.
--supabase sharedreuses the main stack--supabase isolatedpatches and starts a feature-owned stack--supabase nonedisables Supabase for that worktree--supabase-config apps/api/supabase/config.tomlpersists an alternative repo-relative pathtrueandfalseremain aliases forisolatedandnoneon feature commands
For isolated stacks:
project_idgets a short suffix derived from the worktree nameport,shadow_port,smtp_port,pop3_portare incremented byWRT_PORT_OFFSETacross nested TOML sectionshttp://localhost:<port>/http://127.0.0.1:<port>URL ports inside the config are also incremented- the effective config path is marked
skip-worktreein that worktree to reduce accidental commits
wrtcommands operate only inside managed roots created bywrt cloneorwrt root initwrt runmust be invoked with--exactly likewrt run <name> -- <command> ...(otherwise it exits with code2)wrt envwith no<name>only works when you run it from inside a tracked worktree (it infers fromcwd)- Clone fails its setup phase if a detected Supabase project cannot start or report status. The managed root and failed state remain available for recovery.
- Shared features never automatically reset the main database. Use explicit DB commands or
--db truewhen destructive setup is intended. wrt clone/wrt root initclone committed Git state. They copy.envintomainand.wrt.jsoninto the managed root only when the source is a local directory and those files exist.- Worktree name slugging is intentionally strict. If your
<name>turns into an empty slug, it becomeswrt
- Rust (edition 2021)
- clap (CLI parsing)
- serde / serde_json (state + discovery config)
- toml_edit (Supabase config patching)
- chrono (timestamps)
Project structure
src/ Rust CLI entrypoint + internal modules (git/worktree/state/supabase/codex/pm/ui)
assets/ embedded prompt + JSON schema used by wrt init
tests/ integration tests (temp git repos)
wrt init can shell out to the Codex CLI to generate a shared .wrt.json at the managed root (useful if you want a shared "what services exist / which ports matter" contract for tooling). A checkout-local .wrt.json takes precedence when present.
Offline testing:
# Make init read a pre-generated JSON file instead of calling codex
export WRT_CODEX_MOCK_OUTPUT=/path/to/out.json
wrt init --print# Run from source
just run help
# Run formatting, linting, build, and tests
just check
# Format
just fmt
# Lint
just lint
# Build
just build
# Tests
just testBuilt for people who keep 6 worktrees open and still want predictable ports.