Global Worktree Manager
Manage parallel development across ALL projects using git worktrees with Claude Code agents. Each worktree is an isolated copy of the repo on a different branch, stored centrally at ~/tmp/worktrees/.
IMPORTANT: You (Claude) can perform ALL operations manually using standard tools (jq, git, bash). Scripts are helpers, not requirements. If a script fails, fall back to manual operations described in this document.
When This Skill Activates
Trigger phrases:
- "spin up worktrees for X, Y, Z"
- "create 3 worktrees for features A, B, C"
- "new worktree for feature/auth"
- "what's the status of my worktrees?"
- "show all worktrees" / "show worktrees for this project"
- "clean up merged worktrees"
- "clean up the auth worktree"
- "launch agent in worktree X"
- "sync worktrees" / "sync worktree registry"
- "create PR" (when in a worktree - updates registry with PR number)
File Locations
| File | Purpose |
|---|---|
~/.claude/worktree-registry.json | Global registry - tracks all worktrees across all projects |
~/.claude/skills/worktree-manager/config.json | Skill config - terminal, shell, port range settings |
~/.claude/skills/worktree-manager/scripts/ | Helper scripts - optional, can do everything manually |
~/tmp/worktrees/ | Worktree storage - all worktrees live here |
.claude/worktree.json (per-project) | Project config - optional custom settings |
Core Concepts
Centralized Worktree Storage
All worktrees live in ~/tmp/worktrees/<project-name>/<branch-slug>/
~/tmp/worktrees/
├── obsidian-ai-agent/
│ ├── feature-auth/ # branch: feature/auth
│ ├── feature-payments/ # branch: feature/payments
│ └── fix-login-bug/ # branch: fix/login-bug
└── another-project/
└── feature-dark-mode/
Branch Slug Convention
Branch names are slugified for filesystem safety by replacing / with -:
feature/auth→feature-authfix/login-bug→fix-login-bugfeat/user-profile→feat-user-profile
Slugify manually: echo "feature/auth" | tr '/' '-' → feature-auth
Port Allocation Rules
- Global pool: 8100-8199 (100 ports total)
- Per worktree: 2 ports allocated (for API + frontend patterns)
- Globally unique: Ports are tracked globally to avoid conflicts across projects
- Check before use: Always verify port isn't in use by system:
lsof -i :<port>
Global Registry
Location
~/.claude/worktree-registry.json
Schema
json1{ 2 "worktrees": [ 3 { 4 "id": "unique-uuid", 5 "project": "obsidian-ai-agent", 6 "repoPath": "/Users/rasmus/Projects/obsidian-ai-agent", 7 "branch": "feature/auth", 8 "branchSlug": "feature-auth", 9 "worktreePath": "/Users/rasmus/tmp/worktrees/obsidian-ai-agent/feature-auth", 10 "ports": [8100, 8101], 11 "createdAt": "2025-12-04T10:00:00Z", 12 "validatedAt": "2025-12-04T10:02:00Z", 13 "agentLaunchedAt": "2025-12-04T10:03:00Z", 14 "task": "Implement OAuth login", 15 "prNumber": null, 16 "status": "active" 17 } 18 ], 19 "portPool": { 20 "start": 8100, 21 "end": 8199, 22 "allocated": [8100, 8101] 23 } 24}
Field Descriptions
Worktree entry fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (UUID) |
project | string | Project name (from git remote or directory) |
repoPath | string | Absolute path to original repository |
branch | string | Full branch name (e.g., feature/auth) |
branchSlug | string | Filesystem-safe name (e.g., feature-auth) |
worktreePath | string | Absolute path to worktree |
ports | number[] | Allocated port numbers (usually 2) |
createdAt | string | ISO 8601 timestamp |
validatedAt | string|null | When validation passed |
agentLaunchedAt | string|null | When agent was launched |
task | string|null | Task description for the agent |
prNumber | number|null | Associated PR number if exists |
status | string | active, orphaned, or merged |
Port pool fields:
| Field | Type | Description |
|---|---|---|
start | number | First port in pool (default: 8100) |
end | number | Last port in pool (default: 8199) |
allocated | number[] | Currently allocated ports |
Manual Registry Operations
Read entire registry:
bash1cat ~/.claude/worktree-registry.json | jq '.'
List all worktrees:
bash1cat ~/.claude/worktree-registry.json | jq '.worktrees[]'
List worktrees for specific project:
bash1cat ~/.claude/worktree-registry.json | jq '.worktrees[] | select(.project == "my-project")'
Get allocated ports:
bash1cat ~/.claude/worktree-registry.json | jq '.portPool.allocated'
Find worktree by branch (partial match):
bash1cat ~/.claude/worktree-registry.json | jq '.worktrees[] | select(.branch | contains("auth"))'
Add worktree entry manually:
bash1TMP=$(mktemp) 2jq '.worktrees += [{ 3 "id": "'$(uuidgen)'", 4 "project": "my-project", 5 "repoPath": "/path/to/repo", 6 "branch": "feature/auth", 7 "branchSlug": "feature-auth", 8 "worktreePath": "/Users/me/tmp/worktrees/my-project/feature-auth", 9 "ports": [8100, 8101], 10 "createdAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", 11 "validatedAt": null, 12 "agentLaunchedAt": null, 13 "task": "My task", 14 "prNumber": null, 15 "status": "active" 16}]' ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json
Add ports to allocated pool:
bash1TMP=$(mktemp) 2jq '.portPool.allocated += [8100, 8101] | .portPool.allocated |= unique | .portPool.allocated |= sort_by(.)' \ 3 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json
Remove worktree entry:
bash1TMP=$(mktemp) 2jq 'del(.worktrees[] | select(.project == "my-project" and .branch == "feature/auth"))' \ 3 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json
Release ports from pool:
bash1TMP=$(mktemp) 2jq '.portPool.allocated = (.portPool.allocated | map(select(. != 8100 and . != 8101)))' \ 3 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json
Initialize empty registry (if missing):
bash1mkdir -p ~/.claude 2cat > ~/.claude/worktree-registry.json << 'EOF' 3{ 4 "worktrees": [], 5 "portPool": { 6 "start": 8100, 7 "end": 8199, 8 "allocated": [] 9 } 10} 11EOF
Manual Port Allocation
If scripts/allocate-ports.sh fails, allocate ports manually:
Step 1: Get currently allocated ports
bash1ALLOCATED=$(cat ~/.claude/worktree-registry.json | jq -r '.portPool.allocated[]' | sort -n) 2echo "Currently allocated: $ALLOCATED"
Step 2: Find first available port (not in allocated list AND not in use by system)
bash1for PORT in $(seq 8100 8199); do 2 # Check if in registry 3 if ! echo "$ALLOCATED" | grep -q "^${PORT}$"; then 4 # Check if in use by system 5 if ! lsof -i :"$PORT" &>/dev/null; then 6 echo "Available: $PORT" 7 break 8 fi 9 fi 10done
Step 3: Add to allocated pool
bash1TMP=$(mktemp) 2jq '.portPool.allocated += [8100] | .portPool.allocated |= unique | .portPool.allocated |= sort_by(.)' \ 3 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json
What You (Claude) Do vs What Scripts Do
| Task | Script Available | Manual Fallback |
|---|---|---|
| Determine project name | No | Parse git remote get-url origin or basename $(pwd) |
| Detect package manager | No | Check for lockfiles (see Detection section) |
| Create git worktree | No | git worktree add <path> -b <branch> |
| Copy .agents/ directory | No | cp -r .agents <worktree-path>/ |
| Install dependencies | No | Run detected install command |
| Validate (health check) | No | Start server, curl endpoint, stop server |
| Allocate ports | scripts/allocate-ports.sh 2 | Manual (see above) |
| Register worktree | scripts/register.sh | Manual jq (see above) |
| Launch agent in terminal | scripts/launch-agent.sh | Manual (see below) |
| Show status | scripts/status.sh | cat ~/.claude/worktree-registry.json | jq ... |
| Cleanup worktree | scripts/cleanup.sh | Manual (see Cleanup section) |
Workflows
1. Create Multiple Worktrees with Agents
User says: "Spin up 3 worktrees for feature/auth, feature/payments, and fix/login-bug"
You do (can parallelize with subagents):
For EACH branch (can run in parallel):
1. SETUP
a. Get project name:
PROJECT=$(basename $(git remote get-url origin 2>/dev/null | sed 's/\.git$//') 2>/dev/null || basename $(pwd))
b. Get repo root:
REPO_ROOT=$(git rev-parse --show-toplevel)
c. Slugify branch:
BRANCH_SLUG=$(echo "feature/auth" | tr '/' '-')
d. Determine worktree path:
WORKTREE_PATH=~/tmp/worktrees/$PROJECT/$BRANCH_SLUG
2. ALLOCATE PORTS
Option A (script): ~/.claude/skills/worktree-manager/scripts/allocate-ports.sh 2
Option B (manual): Find 2 unused ports from 8100-8199, add to registry
3. CREATE WORKTREE
mkdir -p ~/tmp/worktrees/$PROJECT
git worktree add $WORKTREE_PATH -b $BRANCH
# If branch exists already, omit -b flag
4. COPY UNCOMMITTED RESOURCES
cp -r .agents $WORKTREE_PATH/ 2>/dev/null || true
cp .env.example $WORKTREE_PATH/.env 2>/dev/null || true
5. INSTALL DEPENDENCIES
cd $WORKTREE_PATH
# Detect and run: npm install / uv sync / etc.
6. VALIDATE (start server, health check, stop)
a. Start server with allocated port
b. Wait and health check: curl -sf http://localhost:$PORT/health
c. Stop server
d. If FAILS: report error but continue with other worktrees
7. REGISTER IN GLOBAL REGISTRY
Option A (script): ~/.claude/skills/worktree-manager/scripts/register.sh ...
Option B (manual): Update ~/.claude/worktree-registry.json with jq
8. LAUNCH AGENT
Option A (script): ~/.claude/skills/worktree-manager/scripts/launch-agent.sh $WORKTREE_PATH "task"
Option B (manual): Open terminal manually, cd to path, run claude
AFTER ALL COMPLETE:
- Report summary table to user
- Note any failures with details
2. Check Status
With script:
bash1~/.claude/skills/worktree-manager/scripts/status.sh 2~/.claude/skills/worktree-manager/scripts/status.sh --project my-project
Manual:
bash1# All worktrees 2cat ~/.claude/worktree-registry.json | jq -r '.worktrees[] | "\(.project)\t\(.branch)\t\(.ports | join(","))\t\(.status)\t\(.task // "-")"' 3 4# For current project 5PROJECT=$(basename $(git remote get-url origin 2>/dev/null | sed 's/\.git$//')) 6cat ~/.claude/worktree-registry.json | jq -r ".worktrees[] | select(.project == \"$PROJECT\") | \"\(.branch)\t\(.ports | join(\",\"))\t\(.status)\""
3. Launch Agent Manually
If launch-agent.sh fails:
For Ghostty:
bash1open -na "Ghostty.app" --args -e fish -c "cd '$WORKTREE_PATH' && claude"
For iTerm2:
bash1osascript -e 'tell application "iTerm2" to create window with default profile' \ 2 -e 'tell application "iTerm2" to tell current session of current window to write text "cd '"$WORKTREE_PATH"' && claude"'
For tmux:
bash1tmux new-session -d -s "wt-$PROJECT-$BRANCH_SLUG" -c "$WORKTREE_PATH" "fish -c 'claude'"
4. Cleanup Worktree
With script:
bash1~/.claude/skills/worktree-manager/scripts/cleanup.sh my-project feature/auth --delete-branch
Manual cleanup:
bash1# 1. Get worktree info from registry 2ENTRY=$(cat ~/.claude/worktree-registry.json | jq '.worktrees[] | select(.project == "my-project" and .branch == "feature/auth")') 3WORKTREE_PATH=$(echo "$ENTRY" | jq -r '.worktreePath') 4PORTS=$(echo "$ENTRY" | jq -r '.ports[]') 5REPO_PATH=$(echo "$ENTRY" | jq -r '.repoPath') 6 7# 2. Kill processes on ports 8for PORT in $PORTS; do 9 lsof -ti:"$PORT" | xargs kill -9 2>/dev/null || true 10done 11 12# 3. Remove worktree 13cd "$REPO_PATH" 14git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || rm -rf "$WORKTREE_PATH" 15git worktree prune 16 17# 4. Remove from registry 18TMP=$(mktemp) 19jq 'del(.worktrees[] | select(.project == "my-project" and .branch == "feature/auth"))' \ 20 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json 21 22# 5. Release ports 23TMP=$(mktemp) 24for PORT in $PORTS; do 25 jq ".portPool.allocated = (.portPool.allocated | map(select(. != $PORT)))" \ 26 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json 27done 28 29# 6. Optionally delete branch 30git branch -D feature/auth 31git push origin --delete feature/auth
5. Create PR from Worktree
When creating a PR from a worktree branch, update the registry with the PR number:
bash1# After gh pr create succeeds, get the PR number 2BRANCH=$(git branch --show-current) 3PR_NUM=$(gh pr view --json number -q '.number') 4 5# Update registry with PR number 6if [ -n "$PR_NUM" ] && [ -f ~/.claude/worktree-registry.json ]; then 7 TMP=$(mktemp) 8 jq "(.worktrees[] | select(.branch == \"$BRANCH\")).prNumber = $PR_NUM" \ 9 ~/.claude/worktree-registry.json > "$TMP" && mv "$TMP" ~/.claude/worktree-registry.json 10 echo "Updated worktree registry with PR #$PR_NUM" 11fi
This enables cleanup.sh --merged to automatically find and clean up worktrees after their PRs are merged.
6. Sync Registry
Reconcile registry with actual worktrees and PR status:
bash1# Check status (no changes) 2~/.claude/skills/worktree-manager/scripts/sync.sh 3 4# Auto-fix issues (update PR numbers, remove missing entries) 5~/.claude/skills/worktree-manager/scripts/sync.sh --fix 6 7# Quiet mode (only show problems) 8~/.claude/skills/worktree-manager/scripts/sync.sh --quiet
Package Manager Detection
Detect by checking for lockfiles in priority order:
| File | Package Manager | Install Command |
|---|---|---|
bun.lockb | bun | bun install |
pnpm-lock.yaml | pnpm | pnpm install |
yarn.lock | yarn | yarn install |
package-lock.json | npm | npm install |
uv.lock | uv | uv sync |
pyproject.toml (no uv.lock) | uv | uv sync |
requirements.txt | pip | pip install -r requirements.txt |
go.mod | go | go mod download |
Cargo.toml | cargo | cargo build |
Detection logic:
bash1cd $WORKTREE_PATH 2if [ -f "bun.lockb" ]; then bun install 3elif [ -f "pnpm-lock.yaml" ]; then pnpm install 4elif [ -f "yarn.lock" ]; then yarn install 5elif [ -f "package-lock.json" ]; then npm install 6elif [ -f "uv.lock" ]; then uv sync 7elif [ -f "pyproject.toml" ]; then uv sync 8elif [ -f "requirements.txt" ]; then pip install -r requirements.txt 9elif [ -f "go.mod" ]; then go mod download 10elif [ -f "Cargo.toml" ]; then cargo build 11fi
Dev Server Detection
Look for dev commands in this order:
- docker-compose.yml / compose.yml:
docker-compose up -dordocker compose up -d - package.json scripts: Look for
dev,start:dev,serve - Python with uvicorn:
uv run uvicorn app.main:app --port $PORT - Python with Flask:
flask run --port $PORT - Go:
go run .
Port injection: Most servers accept PORT env var or --port flag
Project-Specific Config (Optional)
Projects can provide .claude/worktree.json for custom settings:
json1{ 2 "ports": { 3 "count": 2, 4 "services": ["api", "frontend"] 5 }, 6 "install": "uv sync && cd frontend && npm install", 7 "validate": { 8 "start": "docker-compose up -d", 9 "healthCheck": "curl -sf http://localhost:{{PORT}}/health", 10 "stop": "docker-compose down" 11 }, 12 "copyDirs": [".agents", ".env.example", "data/fixtures"] 13}
If this file exists, use its settings. Otherwise, auto-detect.
Parallel Worktree Creation
When creating multiple worktrees, use subagents for parallelization:
User: "Spin up worktrees for feature/a, feature/b, feature/c"
You:
1. Allocate ports for ALL worktrees upfront (6 ports total)
2. Spawn 3 subagents, one per worktree
3. Each subagent:
- Creates its worktree
- Installs deps
- Validates
- Registers (with its pre-allocated ports)
- Launches agent
4. Collect results from all subagents
5. Report unified summary with any failures noted
Safety Guidelines
-
Before cleanup, check PR status:
- PR merged → safe to clean everything
- PR open → warn user, confirm before proceeding
- No PR → warn about unsubmitted work
-
Before deleting branches, confirm if:
- PR not merged
- No PR exists
- Worktree has uncommitted changes
-
Port conflicts: If port in use by non-worktree process, pick different port
-
Orphaned worktrees: If original repo deleted, mark as
orphanedin status -
Max worktrees: With 100-port pool and 2 ports each, max ~50 concurrent worktrees
Script Reference
Scripts are in ~/.claude/skills/worktree-manager/scripts/
allocate-ports.sh
bash1~/.claude/skills/worktree-manager/scripts/allocate-ports.sh <count> 2# Returns: space-separated port numbers (e.g., "8100 8101") 3# Automatically updates registry
register.sh
bash1~/.claude/skills/worktree-manager/scripts/register.sh \ 2 <project> <branch> <branch-slug> <worktree-path> <repo-path> <ports> [task] 3# Example: 4~/.claude/skills/worktree-manager/scripts/register.sh \ 5 "my-project" "feature/auth" "feature-auth" \ 6 "$HOME/tmp/worktrees/my-project/feature-auth" \ 7 "/path/to/repo" "8100,8101" "Implement OAuth"
launch-agent.sh
bash1~/.claude/skills/worktree-manager/scripts/launch-agent.sh <worktree-path> [task] 2# Opens new terminal window (Ghostty by default) with Claude Code
status.sh
bash1~/.claude/skills/worktree-manager/scripts/status.sh [--project <name>] 2# Shows all worktrees, or filtered by project
cleanup.sh
bash1~/.claude/skills/worktree-manager/scripts/cleanup.sh <project> <branch> [--delete-branch] 2# Kills ports, removes worktree, updates registry 3# --delete-branch also removes local and remote git branches 4 5# Or cleanup ALL merged worktrees at once: 6~/.claude/skills/worktree-manager/scripts/cleanup.sh --merged [--delete-branch] 7# Finds all worktrees with merged PRs and cleans them up
sync.sh
bash1~/.claude/skills/worktree-manager/scripts/sync.sh [--quiet] [--fix] 2# Reconciles registry with actual worktrees and PR status 3# --quiet: Only show issues, not OK entries 4# --fix: Automatically remove missing entries and update PR numbers/status 5 6# Example: Check status without changing anything 7~/.claude/skills/worktree-manager/scripts/sync.sh 8 9# Example: Auto-fix registry issues 10~/.claude/skills/worktree-manager/scripts/sync.sh --fix
release-ports.sh
bash1~/.claude/skills/worktree-manager/scripts/release-ports.sh <port1> [port2] ... 2# Releases ports back to pool
Skill Config
Location: ~/.claude/skills/worktree-manager/config.json
json1{ 2 "terminal": "ghostty", 3 "shell": "fish", 4 "claudeCommand": "claude", 5 "portPool": { 6 "start": 8100, 7 "end": 8199 8 }, 9 "portsPerWorktree": 2, 10 "worktreeBase": "~/tmp/worktrees", 11 "defaultCopyDirs": [".agents", ".env.example"] 12}
Terminal options: ghostty, iterm2, tmux, wezterm, kitty, alacritty
Common Issues
"Worktree already exists"
bash1git worktree list 2git worktree remove <path> --force 3git worktree prune
"Branch already exists"
bash1# Use existing branch (omit -b flag) 2git worktree add <path> <branch>
"Port already in use"
bash1lsof -i :<port> 2# Kill if stale, or pick different port
Registry out of sync
bash1# Compare registry to actual worktrees 2cat ~/.claude/worktree-registry.json | jq '.worktrees[].worktreePath' 3find ~/tmp/worktrees -maxdepth 2 -type d 4 5# Remove orphaned entries or add missing ones
Validation failed
- Check stderr/logs for error message
- Common issues: missing env vars, database not running, wrong port
- Report to user with details
- Continue with other worktrees
- User can fix and re-validate manually
Example Session
User: "Spin up 2 worktrees for feature/dark-mode and fix/login-bug"
You:
- Detect project:
obsidian-ai-agent(from git remote) - Detect package manager:
uv(found uv.lock) - Allocate 4 ports:
~/.claude/skills/worktree-manager/scripts/allocate-ports.sh 4→8100 8101 8102 8103 - Create worktrees:
bash
1mkdir -p ~/tmp/worktrees/obsidian-ai-agent 2git worktree add ~/tmp/worktrees/obsidian-ai-agent/feature-dark-mode -b feature/dark-mode 3git worktree add ~/tmp/worktrees/obsidian-ai-agent/fix-login-bug -b fix/login-bug - Copy .agents/:
bash
1cp -r .agents ~/tmp/worktrees/obsidian-ai-agent/feature-dark-mode/ 2cp -r .agents ~/tmp/worktrees/obsidian-ai-agent/fix-login-bug/ - Install deps in each worktree:
bash
1(cd ~/tmp/worktrees/obsidian-ai-agent/feature-dark-mode && uv sync) 2(cd ~/tmp/worktrees/obsidian-ai-agent/fix-login-bug && uv sync) - Validate each (start server, health check, stop)
- Register both worktrees in
~/.claude/worktree-registry.json - Launch agents:
bash
1~/.claude/skills/worktree-manager/scripts/launch-agent.sh \ 2 ~/tmp/worktrees/obsidian-ai-agent/feature-dark-mode "Implement dark mode toggle" 3~/.claude/skills/worktree-manager/scripts/launch-agent.sh \ 4 ~/tmp/worktrees/obsidian-ai-agent/fix-login-bug "Fix login redirect bug" - Report:
Created 2 worktrees with agents: | Branch | Ports | Path | Task | |--------|-------|------|------| | feature/dark-mode | 8100, 8101 | ~/tmp/worktrees/.../feature-dark-mode | Implement dark mode | | fix/login-bug | 8102, 8103 | ~/tmp/worktrees/.../fix-login-bug | Fix login redirect | Both agents running in Ghostty windows.