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-auth
fix/login-bug → fix-login-bug
feat/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
json
1{
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:
bash
1cat ~/.claude/worktree-registry.json | jq '.'
List all worktrees:
bash
1cat ~/.claude/worktree-registry.json | jq '.worktrees[]'
List worktrees for specific project:
bash
1cat ~/.claude/worktree-registry.json | jq '.worktrees[] | select(.project == "my-project")'
Get allocated ports:
bash
1cat ~/.claude/worktree-registry.json | jq '.portPool.allocated'
Find worktree by branch (partial match):
bash
1cat ~/.claude/worktree-registry.json | jq '.worktrees[] | select(.branch | contains("auth"))'
Add worktree entry manually:
bash
1TMP=$(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:
bash
1TMP=$(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:
bash
1TMP=$(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:
bash
1TMP=$(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):
bash
1mkdir -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
bash
1ALLOCATED=$(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)
bash
1for 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
bash
1TMP=$(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:
bash
1~/.claude/skills/worktree-manager/scripts/status.sh
2~/.claude/skills/worktree-manager/scripts/status.sh --project my-project
Manual:
bash
1# 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:
bash
1open -na "Ghostty.app" --args -e fish -c "cd '$WORKTREE_PATH' && claude"
For iTerm2:
bash
1osascript -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:
bash
1tmux new-session -d -s "wt-$PROJECT-$BRANCH_SLUG" -c "$WORKTREE_PATH" "fish -c 'claude'"
4. Cleanup Worktree
With script:
bash
1~/.claude/skills/worktree-manager/scripts/cleanup.sh my-project feature/auth --delete-branch
Manual cleanup:
bash
1# 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:
bash
1# 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:
bash
1# 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:
bash
1cd $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 -d or docker 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:
json
1{
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 orphaned in 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
bash
1~/.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
bash
1~/.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
bash
1~/.claude/skills/worktree-manager/scripts/launch-agent.sh <worktree-path> [task]
2# Opens new terminal window (Ghostty by default) with Claude Code
status.sh
bash
1~/.claude/skills/worktree-manager/scripts/status.sh [--project <name>]
2# Shows all worktrees, or filtered by project
cleanup.sh
bash
1~/.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
bash
1~/.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
bash
1~/.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
json
1{
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"
bash
1git worktree list
2git worktree remove <path> --force
3git worktree prune
"Branch already exists"
bash
1# Use existing branch (omit -b flag)
2git worktree add <path> <branch>
"Port already in use"
bash
1lsof -i :<port>
2# Kill if stale, or pick different port
Registry out of sync
bash
1# 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.