Manage git worktrees across single or multiple repositories with a single command. Pure Bash — no dependencies beyond
gitandbash.
Git worktrees are great for parallel development — but managing them is painful:
- Search noise — worktrees inside repos pollute code search (especially with AI coding tools)
- Multi-repo overhead — creating/cleaning worktrees across 3+ repos is tedious
- Forgotten setup — dependency installation, hooks, and config must be repeated each time
worktree solves this with one command: create isolated workspaces, run hooks, install deps, and clean up — across all repos at once.
git- Bash 4.0 or newer — the scripts use associative arrays (
declare -A).-
Linux distros ship Bash 4+/5 already.
-
macOS ships Bash 3.2 as
/bin/bash. Install a newer bash and make sure it precedes/bininPATH:brew install bash # installs Bash 5 under $(brew --prefix)/binThe
worktreeshebang is#!/usr/bin/env bash, so the newer bash onPATHis used automatically. Running under Bash 3.2 exits early with a clear error.
-
# Install
git clone https://github.com/nanasess/git-worktree-manager.git ~/git-repos/git-worktree-manager
ln -sf ~/git-repos/git-worktree-manager/worktree ~/.local/bin/worktree
# Enable `worktree switch` (cd into worktrees) — add to ~/.zshrc or ~/.bashrc
eval "$(worktree shell-init)"
# Create worktrees for a task
cd ~/git-repos/my-project
worktree create feature-login
# List, switch, pull, checkout, cleanup
worktree list
worktree switch feature-login # cd into the worktree (needs shell-init)
worktree pull
worktree checkout main
worktree cleanup feature-login --force --delete-branches~/git-repos/
├── my-project/ # Your project (single or multi-repo)
│ ├── frontend/ # git repo
│ ├── backend/ # git repo
│ └── CLAUDE.md
│
├── my-project.worktrees/ # Worktrees live OUTSIDE the project
│ ├── feature-login/
│ │ ├── frontend/ # branch: feature-login
│ │ ├── backend/ # branch: feature-login
│ │ ├── CLAUDE.md # symlink to the original (unchanged)
│ │ └── CLAUDE.local.md # worktree context (never touches CLAUDE.md)
│ └── fix-auth/
│ └── ...
Worktrees are placed outside the project directory — no search noise, no IDE confusion.
Works with single-repo projects too:
├── my-app/ # Single git repo
│ ├── src/
│ └── CLAUDE.md
├── my-app.worktrees/
│ └── feature-login/ # Worktree (branch: feature-login)
│ ├── src/
│ ├── CLAUDE.md # unchanged (no diff against the source)
│ └── CLAUDE.local.md # worktree context (added, gitignore recommended)
All commands work from the project root or from inside a .worktrees/ directory.
Creates a worktree (and branch) for each repository.
worktree create feature-login
worktree create fix-bug --branch-prefix nanasess/
worktree create quick-test --no-install
worktree create feature-login --no-cd # don't auto-cd into the new worktreeWith shell integration enabled, create automatically
cds into the new worktree on success (like worktrunk's wt switch --create).
Use --no-cd to opt out. Without shell integration — or from a script / Claude
subagent — no directory change happens.
What happens:
git fetch originon each repo (alsogit fetch upstreamif the remote exists)- Create worktree based on the default branch —
upstreamis preferred overoriginwhen both are configured (fork workflows whereoriginis your fork andupstreamis the canonical repo) - Write the worktree context to
CLAUDE.local.md(the originalCLAUDE.mdis left untouched) - Symlink non-git items (multi-repo)
- Copy
mise.toml/mise.local.tomlinto each worktree (even when gitignored) - Run
.worktreercpost_create()hook - Auto-install dependencies
| Option | Description |
|---|---|
--branch-prefix <prefix> |
Prefix for branch names (e.g., nanasess/) |
--no-install |
Skip dependency installation |
--no-cd |
Do not auto-cd into the new worktree (needs shell integration) |
worktree list # or: worktree ls
worktree list --merged # only tasks with at least one merged sub-repo
worktree list --names-only # task names only, one per line (pipe-friendly)
worktree list --merged --names-only # combine: feed directly into `worktree cleanup`Shows all tasks with branch names, modification status, and untracked files.
Each repository line may carry status labels:
| Label | Meaning |
|---|---|
[modified] |
The working tree or index has uncommitted changes |
[untracked] |
One or more files are not tracked by git |
[merged] |
The branch corresponds to a merged GitHub PR (detected via gh pr list --state merged, so squash/rebase merges are caught) |
The [merged] label requires the gh CLI to be installed and authenticated against the remote; it is silently skipped when gh is missing, the remote is not GitHub, or the API call fails.
| Option | Description |
|---|---|
--merged |
Filter to tasks where at least one sub-repo has a merged head branch (any-merged). Stricter than cleanup --merged, which requires every sub-repo to be merged. |
--names-only |
Print task names only — no headers, colors, or status labels. Combine with --merged to pipe into worktree cleanup. |
worktree checkout # default branch on all repos
worktree checkout develop # specific branch on all repos
worktree co main # alias
# Create a worktree from a GitHub issue or PR URL
worktree checkout https://github.com/owner/repo/issues/42
worktree checkout https://github.com/owner/repo/pull/123When given a branch, switches each repo's HEAD to that branch.
When given a GitHub URL, creates a worktree scoped to that issue/PR:
| URL type | Task dir | Branch behavior |
|---|---|---|
issues/<N> |
issue-<N> |
Creates a new branch issue-<N> from default |
pull/<N> |
pr-<N> |
Fetches the PR head and checks it out under its original branch name |
For PR URLs in multi-repo projects, only the repository matching the URL gets a worktree (the PR is scoped to one repo).
In URL mode, checkout also auto-cds into the new worktree when shell integration is enabled (pass --no-cd to opt out). Branch mode (worktree checkout <branch>) switches branches in place and never changes directory.
The gh CLI is optional: with gh, PR URLs use the PR's original head branch name; without it, a generic pr-<N> branch name is used.
worktree switch feature/foo-bar # cd into the matching worktree
worktree switch foo # match by unique prefix/substring
worktree switch - # cd back to the previous worktree
worktree switch # pick interactively (fzf), or list names
worktree sw foo # aliasswitch changes your shell's current directory to a worktree under
<project>.worktrees/. <name> is matched against worktree task names with the
precedence exact > unique prefix > unique substring; an ambiguous query
prints the candidates and exits non-zero.
Because a child process cannot change its parent shell's directory, switch
requires shell integration. Add this to your ~/.zshrc or ~/.bashrc:
eval "$(worktree shell-init)"This installs a worktree shell function that wraps the binary: for switch
it captures the resolved path and runs cd; every other subcommand is passed
through unchanged. Without it, worktree switch <name> just prints the resolved
path (and a hint) instead of changing directory.
switch - toggles back to the directory you were in before the last switch
(like cd -, but scoped to worktree switches). With no argument, switch opens
an fzf picker when one is available and a
terminal is attached; otherwise it prints the list of worktree names.
worktree pullRuns git pull --ff-only on each repo with a summary of results.
worktree cleanup feature-login --force --delete-branches
worktree cleanup --merged --force --delete-branches
worktree cleanup --merged --dry-run
# Pipe task names in (one per line) — works with `worktree list --names-only`
worktree list --merged --names-only | worktree cleanup --forceAliases: worktree clean, worktree rm
| Option | Description |
|---|---|
--merged |
Auto-detect merged tasks (all sub-repos must be merged) |
--delete-branches |
Also delete branches |
--dry-run |
Preview without deleting |
--force |
Skip confirmation |
When no task name is supplied and stdin is a pipe, cleanup reads one task name per line from stdin. Lines that don't correspond to a known task are skipped with a warning.
worktree install --skills # project-local
worktree install --skills --global # all projectsInstalls Claude Code slash commands: /worktree-create, /worktree-list, /worktree-cleanup, /worktree-checkout, /worktree-pull.
Place in the project root. post_create() runs in the task directory after creation.
post_create() {
ln -sf shared-docs/claude-repository-guide.md CLAUDE.md
ln -sf shared-docs/setup.sh setup.sh
}| Variable | Description |
|---|---|
WORKTREE_TASK_NAME |
Task name |
WORKTREE_TASK_DIR |
Task directory path |
WORKTREE_PROJECT_ROOT |
Original project root |
Detected automatically during worktree create:
| Lock File | Command |
|---|---|
package-lock.json |
npm install |
pnpm-lock.yaml |
pnpm install |
yarn.lock |
yarn install |
composer.lock |
composer install |
*.sln / *.csproj |
dotnet restore |
Each package system is evaluated independently, so polyglot projects
(e.g. EC-CUBE with composer.json + package.json) install every applicable
manager. Within the Node.js family (npm / pnpm / yarn), the managers are
mutually exclusive and resolved with priority npm > pnpm > yarn.
Skip with --no-install.
If mise.toml or mise.local.toml exists in the source (project root for single-repo, each sub-repo root for multi-repo), worktree create copies it into the new worktree. This keeps mise-managed tool versions consistent even when the config is gitignored.
Uses bats-core (git submodules).
git submodule update --init --recursive
./test/bats/bin/bats test/*.batsMIT