An agent-first Git worktree manager for running multiple AI coding agents in parallel on the same project.
wt gives each agent a named, isolated feature worktree while keeping branch creation, project setup and teardown, slot allocation, and port offsets in one consistent workflow. It is agent-agnostic, so switching between Claude Code, Codex, Cursor, OpenCode, or another coding agent does not change how the project manages worktrees.
Although it is built for agent-first workflows, wt also works for human-led parallel feature work.
Single-file Python script. Stdlib only. No runtime dependencies beyond git and Python 3.11+.
If you're juggling several feature branches or AI coding agents, you've probably hit the friction of git stash, git checkout, re-running migrations, restarting your dev server, and dealing with port conflicts between branches. Worktrees solve the "one checkout per branch" half of this. wt solves the other half: per-branch infrastructure setup and teardown, port allocation, and a discoverable interface.
curl -fsSL https://raw.githubusercontent.com/absolutepraya/wt/main/install.sh | bashThis installs:
~/.local/bin/wt— the script~/.config/wt/wt.sh— the shell wrapper (sourced from your~/.zshrc/~/.bashrc)~/.config/wt/wt.fish— the Fish wrapper (source it from your Fish config)
Open a new shell or source ~/.zshrc to pick up the wrapper.
git clone https://github.com/absolutepraya/wt ~/Documents/Projects/wt
cd ~/Documents/Projects/wt
./install.shFor a Node or JavaScript project that wants to pin wt in its dependency lockfile, install the npm package as a development dependency:
npm install --save-dev @absolutepraya/wt
npx --no-install wt --helpThe consuming project's package-lock.json pins the npm adapter version. The adapter forwards to the same Python CLI shipped by the standalone installer, so Python 3.11 or newer and Git are still required. This install mode provides the wt command to npm scripts and npx --no-install; it does not modify shell startup files. Use the standalone installer when you want the bash, zsh, or fish shell wrapper for interactive cd behavior.
Each versioned merge to main can publish the package and create a matching GitHub Release after the repository's release setup is complete. See RELEASING.md for the one-time setup and release contract.
The standalone installation updates from the latest stable GitHub Release:
wt update --check
wt updateThe update verifies the release checksums and downloaded payloads before replacing the CLI and installed shell wrappers. This includes the Fish wrapper when it is present. npm-managed installations belong to the consumer project's dependency graph, so update those with:
npm update --save-dev @absolutepraya/wtThe source checkout itself is never modified by wt update.
Add to ~/.config/fish/config.fish:
source ~/.config/wt/wt.fishThe standalone installer places the wrapper at ~/.config/wt/wt.fish and
wt update keeps it current with the CLI.
- Python 3.11 or newer (uses
tomllib) git2.5+ (worktree support)- bash, zsh, or fish
In a git repo:
# Drop a minimal config (one-time)
mkdir -p .wt && cat > .wt/config.toml <<'EOF'
worktree_path = ".worktrees"
port_offset_interval = 100
max_slots = 9
EOF
# Create your first worktree
wt newwt --version # print the installed wt version
wt new # auto-named worktree off origin/main, runs setup, stays put
wt new --cd # create, then enter it in an interactive shell
wt new my-fix # explicit worktree name
wt new -b you/feat/X-123 # explicit branch (auto dir name)
wt new my-fix -b you/feat/X-123 # explicit name + branch
wt new --from feature-x # check out an existing remote branch (review workflow)
wt new --skip-setup # create the worktree but skip the project's setup script
wt ls # list worktrees with slot/name/branch/path/ports
wt cd # cd to the main worktree
wt cd <name> # cd to an existing worktree
wt update # update an installed standalone wt
wt update --check # check for a stable update without changing files
wt rm <name> # teardown + remove + free slot + delete branch
wt rm <name> --force # bypass dirty / unmerged checks
wt rm <name> --keep-branch # keep the branch when removing the worktree
Run wt <command> -h for full per-command help.
The worktree you're currently inside is marked with a green ✓ next to its slot number. Worktrees git knows about but wt doesn't manage are listed in a separate section.
╔══════╤═════════════╤═══════════════════════╤════════════════════════╤═══════╗
║ SLOT │ NAME │ BRANCH │ PATH │ PORTS ║
╟──────┼─────────────┼───────────────────────┼────────────────────────┼───────╢
║ 0 │ (main) │ main │ . │ base ║
║ 1 │ paris │ you/eng-415-runtime │ .worktrees/paris │ +100 ║
║ 2 ✓ │ strasbourg │ you/merchant-mgmt │ .worktrees/strasbourg │ +200 ║
║ 3 │ new-orleans │ you/mobile-ui-polish │ .worktrees/new-orleans │ +300 ║
╠══════╧═════════════╧═══════════════════════╧════════════════════════╧═══════╣
║ Unmanaged worktrees ║
╠══════╤═════════════╤═══════════════════════╤════════════════════════╤═══════╣
║ │ │ you/old-experiment │ .worktrees/auckland │ ║
╚══════╧═════════════╧═══════════════════════╧════════════════════════╧═══════╝
Creates the worktree, prints a status block, then runs each setup command from .wt/config.toml in order — every step gets its own divider and [i/N] label so you can tell which one's running.
$ wt new
══════════════════════════════════════════════════════════════════
Creating worktree
name rotterdam
branch you/rotterdam
base origin/main
path /home/you/code/myrepo/.worktrees/rotterdam
slot 3
══════════════════════════════════════════════════════════════════
[1/3] setup: pnpm install --frozen-lockfile
Lockfile is up to date, resolution step is skipped
Packages: +1247
...
Done in 12.3s
══════════════════════════════════════════════════════════════════
[2/3] setup: cp .env.example .env.local
══════════════════════════════════════════════════════════════════
[3/3] setup: bash scripts/start-infra.sh
🐳 Starting Postgres on port 5732 (base 5432 + offset 300)…
🐳 Starting Redis on port 6679 (base 6379 + offset 300)…
✓ Infra ready
══════════════════════════════════════════════════════════════════
Created worktree: rotterdam
branch you/rotterdam
base origin/main
path /home/you/code/myrepo/.worktrees/rotterdam
slot 3 (port offset +300 to +399)
══════════════════════════════════════════════════════════════════
By default your shell stays where it is. Add --cd when you are working interactively and want the shell wrapper to enter .worktrees/rotterdam after creation.
Symmetric to wt new: runs every teardown command, then removes the worktree, deletes the branch, and frees the slot.
$ wt rm rotterdam
══════════════════════════════════════════════════════════════════
Removing worktree
name rotterdam
branch you/rotterdam
path /home/you/code/myrepo/.worktrees/rotterdam
slot 3
══════════════════════════════════════════════════════════════════
[1/2] teardown: docker compose -p rotterdam down
[+] Running 3/3
✔ Container rotterdam-redis-1 Removed
✔ Container rotterdam-postgres-1 Removed
✔ Network rotterdam_default Removed
══════════════════════════════════════════════════════════════════
[2/2] teardown: docker network prune -f
Deleted Networks:
rotterdam_default
══════════════════════════════════════════════════════════════════
Removed worktree: rotterdam
branch you/rotterdam
path /home/you/code/myrepo/.worktrees/rotterdam
slot 3 (freed)
══════════════════════════════════════════════════════════════════
If the branch has unmerged commits not in origin/main and not pushed anywhere, wt rm prompts before deleting — pass --force to skip the prompt or --keep-branch to remove the worktree but keep the branch.
Per-project config lives at <repo>/.wt/config.toml:
# Where worktrees go (relative to repo root).
worktree_path = ".worktrees"
# Each non-main worktree gets a slot 1..max_slots and a port offset
# of `slot * port_offset_interval`. Slot 1 → +100, slot 2 → +200, etc.
# Pick an interval larger than the number of ports any single worktree
# needs — 100 is a sensible default.
port_offset_interval = 100
max_slots = 9
# Optional: auto-naming strategy. Defaults to "cities".
# "cities" → paris, strasbourg, new-orleans, kyoto, ...
# "word_pairs" → curious-otter, brave-spruce, ...
name_strategy = "cities"
# Optional: branch name template for `wt new` (no -b given).
# Placeholders: {name} (worktree dir name), {user} ($USER), {date} (YYYY-MM-DD).
branch_template = "{user}/{name}"
# Optional: base ref for new branches. Default "origin/main".
default_base = "origin/main"
# Optional: commands run on `wt new`, in order. Each one gets a
# divider and a "[i/N] setup: <cmd>" label in the output.
setup = [
"pnpm install --frozen-lockfile",
"cp .env.example .env.local",
"bash scripts/start-infra.sh",
]
# Optional: commands run on `wt rm`, in order.
teardown = [
"docker compose -p ${WT_WORKSPACE_NAME} down",
"docker network prune -f",
]Every setup and teardown command runs with these env vars set, so your scripts can derive ports, project names, etc. without hardcoding:
| Variable | Example |
|---|---|
WT_ROOT_PATH |
/home/you/code/myrepo |
WT_WORKSPACE_NAME |
rotterdam |
WT_WORKSPACE_PATH |
/home/you/code/myrepo/.worktrees/rotterdam |
WT_BRANCH |
you/rotterdam |
WT_SLOT |
3 |
WT_PORT_BASE |
300 (= slot × port_offset_interval) |
Example: bind your Postgres to $((5432 + WT_PORT_BASE)) in your start-infra script and every worktree gets its own non-colliding port.
- Dirty worktree:
wt rmprompts unless--forceis given. - Unmerged commits:
wt rmprompts if the branch has commits not inorigin/mainand not pushed to a remote. - cwd inside target:
wt rmrefuses if you're sitting in the worktree you're trying to remove (so your shell doesn't end up in a phantom directory). - Partial setup failure: if a
setupcommand fails (or you Ctrl-C mid-setup), the worktree is rolled back automatically — no half-baked directories. - Concurrent
wt new: name + slot allocation happens under anflock, so two parallel calls can't pick the same slot or name. - Live branch detection:
wt rmchecks the branch actually checked out in the worktree right now, not whateverwt neworiginally created — so checking out a different branch into a worktree and then removing it still cleans up the right ref.
wt is a thin layer over git worktree. It keeps a per-project state file under ~/.wt/<project-id>/state.json mapping slots to worktrees, and uses flock for concurrency. wt new does git fetch → reserve a slot → git worktree add → run setup. wt rm runs teardown → safety checks → git worktree remove → git branch -D → free the slot. wt cd and wt new --cd print a __cd__:<path> sentinel line that the shell wrapper consumes to actually cd your shell.
If you use Claude Code, Codex, Cursor, OpenCode, or a similar agent, this repo ships an Agent Skill at skills/wt/SKILL.md that teaches the agent how and when to use wt instead of raw git worktree add.
Install it with the Vercel Skills CLI:
# Install the wt skill for detected agents in the current project.
npx skills add absolutepraya/wt --skill wt
# Or install it globally for one agent.
npx skills add absolutepraya/wt --skill wt --global --agent codexThe CLI discovers skills/wt/SKILL.md directly and uses symlinks by default, so npx skills update can refresh it later. Coding agents should use wt new without --cd: a subprocess cannot change the agent host's working directory.
To install it:
# Pick one — wherever your agent reads skills from
mkdir -p ~/.claude/skills && ln -s "$PWD/skills/wt" ~/.claude/skills/wt # Claude Code
mkdir -p ~/.config/opencode/skills && ln -s "$PWD/skills/wt" ~/.config/opencode/skills/wt # OpenCode
mkdir -p ~/.codex/skills && ln -s "$PWD/skills/wt" ~/.codex/skills/wt # Codex
mkdir -p ~/.cursor/skills && ln -s "$PWD/skills/wt" ~/.cursor/skills/wt # CursorOr copy the directory into your agent's skill folder. After that, ask your agent "create a worktree for X" — it'll invoke the wt skill and use the CLI correctly (config check, safety prompts, env vars passed to setup scripts, etc.).
The npm package smoke test requires Node.js 18 or newer and npm, in addition to Python 3.11 or newer.
git clone https://github.com/absolutepraya/wt
cd wt
pip install pytest pytest-mock
pytest tests/
bash scripts/check-npm-package.shTests use real git repos (no mocks for git) and run on Python 3.11, 3.12, and 3.13 across macOS and Linux in CI.