| name | worktree-workflow |
| description | Git worktree workflow for isolated feature development and PR creation
When user starts new work, needs to switch contexts, or wants parallel development
|
Git Worktree Workflow Agent
Branch & PR management in shepherdjerred/monorepo uses git-spice — every PR is a stacked PR, and a worktree holds one stack. Load the git-spice-helper skill first (it's authoritative); create/update PRs with git-spice branch/stack submit — a single PR is a stack of one. The gh pr create and manual-git rebase examples below are the generic fallback for repos without git-spice.
What's New in Git Worktree & AI Agents (2025)
- AI Agent Integration: 4-5 parallel Claude Code agents working independently on different features
- Complete Isolation: Each worktree prevents agents from modifying wrong branches or interfering with each other
- Structured Organization:
./worktrees/feature/, ./worktrees/bugfix/, ./worktrees/review/ patterns for clarity
- Production Adoption: incident.io uses worktrees for parallel AI agent development
- Emergency Hotfix Pattern: Create worktree on release branch without disrupting ongoing development
- Cleanup Best Practices: Systematic removal of orphaned worktrees with
git worktree prune
- Meaningful Directory Names: Avoid confusion when multiple agents or developers work simultaneously
Overview
This agent teaches using Git worktrees to isolate changes in separate working directories, enabling parallel development without branch switching and creating clean PRs when complete. Worktrees are particularly powerful for AI agent workflows, where multiple autonomous agents can work on different features simultaneously without conflict.
Core Concept
Git worktrees let you have multiple working directories from the same repository:
- Main worktree: Your primary working directory (usually
main or master)
- Linked worktrees: Additional directories for features/fixes, each on different branches
- No branch switching: Each worktree has its own branch checked out
- Shared .git: All worktrees share the same repository data
CLI Commands
Creating Worktrees
git worktree add ../feature-auth feature/auth
git worktree add -b fix/login-bug ../fix-login
git worktree add -b feature/api ../api-work origin/main
git worktree add worktrees/feature-x -b feature/x
Listing Worktrees
git worktree list
git worktree list --porcelain
Removing Worktrees
git worktree remove ../feature-auth
git worktree remove --force ../feature-auth
git worktree prune
Moving Between Worktrees
cd ../feature-auth
cd ~/git/myproject-feature-auth
cd ~/git/myproject
Complete Workflow
Starting New Work
#!/bin/bash
set -euo pipefail
FEATURE_NAME=${1:?Usage: start-work.sh <feature-name>}
BRANCH_NAME="feature/${FEATURE_NAME}"
WORKTREE_DIR="../${FEATURE_NAME}"
echo "Starting new work: $FEATURE_NAME"
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" origin/main
cd "$WORKTREE_DIR"
echo "✅ Worktree created at: $WORKTREE_DIR"
echo " Branch: $BRANCH_NAME"
echo " Ready to start coding!"
Working in Worktree
cd ../feature-auth
echo "new feature" > feature.ts
git add feature.ts
git commit -m "feat: add authentication"
git push -u origin feature/auth
Creating PR from Worktree
#!/bin/bash
set -euo pipefail
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
echo "❌ Cannot create PR from main branch"
exit 1
fi
echo "Creating PR from branch: $CURRENT_BRANCH"
git push -u origin "$CURRENT_BRANCH"
gh pr create --fill
echo "✅ PR created!"
echo "View: gh pr view --web"
Completing Work
#!/bin/bash
set -euo pipefail
CURRENT_BRANCH=$(git branch --show-current)
WORKTREE_PATH=$(pwd)
echo "Completing work on: $CURRENT_BRANCH"
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "❌ You have uncommitted changes"
git status
exit 1
fi
git push
if ! gh pr view &>/dev/null; then
echo "Creating PR..."
gh pr create --fill
fi
gh pr view
cd "$(git rev-parse --show-toplevel)"
echo ""
echo "Next steps:"
echo " 1. Review PR: gh pr view --web"
echo " 2. After merge: git worktree remove $WORKTREE_PATH"
echo " 3. Clean up: git branch -d $CURRENT_BRANCH"
Advanced Patterns
Organized Worktree Layout
WORKTREE_BASE="$HOME/worktrees/$(basename $(git rev-parse --show-toplevel))"
mkdir -p "$WORKTREE_BASE"
git worktree add "$WORKTREE_BASE/feature-auth" -b feature/auth
Quick Switch Script
#!/bin/bash
WORKTREE_NAME=${1:?Usage: switch-worktree.sh <worktree-name>}
WORKTREE_BASE="$HOME/worktrees/$(basename $(git rev-parse --show-toplevel))"
WORKTREE_PATH="$WORKTREE_BASE/$WORKTREE_NAME"
if [ -d "$WORKTREE_PATH" ]; then
cd "$WORKTREE_PATH"
echo "✅ Switched to: $WORKTREE_PATH"
else
echo "❌ Worktree not found: $WORKTREE_PATH"
echo ""
echo "Available worktrees:"
git worktree list
exit 1
fi
List Worktrees with Branch Status
#!/bin/bash
git worktree list | while IFS= read -r line; do
path=$(echo "$line" | awk '{print $1}')
branch=$(echo "$line" | awk '{print $3}' | tr -d '[]')
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📁 $path"
echo "🌿 $branch"
if [ -d "$path" ]; then
(
cd "$path" 2>/dev/null && {
if git diff --quiet && git diff --cached --quiet; then
echo "✅ Clean"
else
echo "⚠️ Uncommitted changes"
fi
if git rev-parse --abbrev-ref @{u} &>/dev/null; then
ahead=$(git rev-list --count @{u}..HEAD)
behind=$(git rev-list --count HEAD..@{u})
[ $ahead -gt 0 ] && echo "⬆️ $ahead commit(s) ahead"
[ $behind -gt 0 ] && echo "⬇️ $behind commit(s) behind"
else
echo "🔗 No upstream branch"
fi
}
)
fi
echo ""
done
Cleanup Merged Worktrees
#!/bin/bash
set -euo pipefail
DEFAULT_BRANCH=$(git remote show origin | grep "HEAD branch" | sed 's/.*: //')
echo "Cleaning up merged worktrees (base: $DEFAULT_BRANCH)..."
git fetch origin --prune
git worktree list --porcelain | grep -E "^worktree|^branch" | while read -r line; do
if [[ $line =~ ^worktree ]]; then
current_worktree=$(echo "$line" | awk '{print $2}')
elif [[ $line =~ ^branch ]]; then
branch=$(echo "$line" | awk '{print $2}' | sed 's|refs/heads/||')
[ "$branch" = "$DEFAULT_BRANCH" ] && continue
if git branch --merged "origin/$DEFAULT_BRANCH" | grep -q "^[* ]*$branch$"; then
echo "🗑️ Removing merged worktree: $current_worktree ($branch)"
git worktree remove "$current_worktree" || true
git branch -d "$branch" || true
fi
fi
done
git worktree prune
echo "✅ Cleanup complete"
AI Agent Workflows (2025)
The incident.io Case Study
Real-world example: incident.io runs 4-5 Claude Code agents in parallel using worktrees, enabling multiple AI agents to work on different features simultaneously without conflicts.
Key benefits:
- Complete isolation: Each agent operates in its own worktree with its own branch and file state
- No cross-contamination: Agents can't accidentally modify files from other agents' work
- Parallel execution: 4-5 features developed concurrently by autonomous agents
- Clean git history: Each agent creates focused, single-purpose PRs
- Zero coordination overhead: No need to orchestrate agent work order
Structured Directory Organization for AI Agents
project/
├── .git/
├── main/
└── worktrees/
├── feature/
│ ├── agent-1-auth/
│ ├── agent-2-api/
│ └── agent-3-ui/
├── bugfix/
│ ├── agent-4-login-fix/
│ └── agent-5-perf/
└── review/
└── human-review-pr-123/
Setting Up AI Agent Worktrees
#!/bin/bash
set -euo pipefail
AGENT_ID=${1:?Usage: setup-ai-agent-worktree.sh <agent-id> <task-type> <task-name>}
TASK_TYPE=${2:?Usage: setup-ai-agent-worktree.sh <agent-id> <task-type> <task-name>}
TASK_NAME=${3:?Usage: setup-ai-agent-worktree.sh <agent-id> <task-type> <task-name>}
REPO_ROOT=$(git rev-parse --show-toplevel)
WORKTREE_BASE="$REPO_ROOT/worktrees"
TASK_DIR="$WORKTREE_BASE/$TASK_TYPE/$AGENT_ID-$TASK_NAME"
BRANCH_NAME="$TASK_TYPE/$TASK_NAME"
echo "🤖 Setting up AI agent worktree"
echo " Agent: $AGENT_ID"
echo " Task: $TASK_TYPE/$TASK_NAME"
echo " Path: $TASK_DIR"
mkdir -p "$WORKTREE_BASE/$TASK_TYPE"
git fetch origin
git worktree add -b "$BRANCH_NAME" "$TASK_DIR" origin/main
echo "✅ AI agent worktree ready!"
echo ""
echo "Next steps:"
echo " 1. Navigate: cd $TASK_DIR"
echo " 2. Agent starts working in isolated environment"
echo " 3. Agent commits: git commit -m 'feat: ...'"
echo " 4. Agent creates PR: gh pr create --fill"
AI Agent Isolation Benefits
Complete isolation prevents:
- ✅ Agent A modifying Agent B's files
- ✅ Merge conflicts between parallel agent work
- ✅ Branch checkout race conditions
- ✅ Uncommitted changes interfering with other agents
- ✅ Accidental deletion of other agents' work
Example scenario (4 parallel agents):
cd worktrees/feature/agent-1-auth/
cd worktrees/feature/agent-2-api/
cd worktrees/feature/agent-3-ui/
cd worktrees/feature/agent-4-db/
Emergency Hotfix with AI Agent (No Main Disruption)
git worktree add worktrees/bugfix/agent-5-hotfix -b hotfix/critical-bug release/v1.0
cd worktrees/bugfix/agent-5-hotfix/
echo "fix" > critical-fix.ts
git add critical-fix.ts
git commit -m "fix: critical production bug"
git push -u origin hotfix/critical-bug
gh pr create --base release/v1.0 --title "fix: critical bug" --fill
Cleanup After AI Agent Completion
#!/bin/bash
set -euo pipefail
AGENT_ID=${1:?}
TASK_TYPE=${2:?}
TASK_NAME=${3:?}
REPO_ROOT=$(git rev-parse --show-toplevel)
TASK_DIR="$REPO_ROOT/worktrees/$TASK_TYPE/$AGENT_ID-$TASK_NAME"
BRANCH_NAME="$TASK_TYPE/$TASK_NAME"
echo "🧹 Cleaning up AI agent worktree: $AGENT_ID"
if gh pr view "$BRANCH_NAME" --json state --jq .state 2>/dev/null | grep -q "MERGED"; then
echo "✅ PR merged, cleaning up..."
git worktree remove "$TASK_DIR" 2>/dev/null || {
echo "⚠️ Worktree already removed or has uncommitted changes"
git worktree remove --force "$TASK_DIR"
}
git branch -d "$BRANCH_NAME" 2>/dev/null || {
echo "⚠️ Force deleting branch"
git branch -D "$BRANCH_NAME"
}
git push origin --delete "$BRANCH_NAME" 2>/dev/null || {
echo "ℹ️ Remote branch already deleted"
}
echo "✅ Cleanup complete for $AGENT_ID"
else
echo "⚠️ PR not merged yet"
gh pr view "$BRANCH_NAME"
exit 1
fi
Orphaned Worktree Cleanup
#!/bin/bash
set -euo pipefail
echo "🔍 Searching for orphaned worktrees..."
git fetch --prune
ORPHANED=0
git worktree list --porcelain | grep -E "^worktree|^branch" | while read -r line; do
if [[ $line =~ ^worktree ]]; then
current_worktree=$(echo "$line" | awk '{print $2}')
elif [[ $line =~ ^branch ]]; then
branch=$(echo "$line" | awk '{print $2}' | sed 's|refs/heads/||')
[[ "$branch" =~ ^(main|master)$ ]] && continue
if ! git show-ref --verify --quiet "refs/remotes/origin/$branch"; then
echo "🗑️ Orphaned worktree found:"
echo " Path: $current_worktree"
echo " Branch: $branch (remote deleted)"
echo " Cleanup: git worktree remove $current_worktree && git branch -d $branch"
echo ""
ORPHANED=$((ORPHANED + 1))
fi
fi
done
git worktree prune
if [ $ORPHANED -eq 0 ]; then
echo "✅ No orphaned worktrees found"
else
echo "⚠️ Found $ORPHANED orphaned worktree(s)"
fi
Best Practices for AI Agent Workflows
-
Meaningful Directory Names: Use descriptive names like agent-1-auth instead of agent-1 or temp-worktree
worktrees/feature/agent-1-authentication/
worktrees/feature/agent-2-api-endpoints/
worktrees/feature/agent-1/
worktrees/feature/temp/
-
Structured Categories: Organize by task type (feature/bugfix/review)
worktrees/
├── feature/
├── bugfix/
├── refactor/
├── docs/
└── review/
-
Agent Coordination: Use clear branch naming for visibility
feature/add-authentication
feature/add-api-endpoints
bugfix/fix-login-validation
-
Automatic Cleanup: Run cleanup scripts after PR merge
cleanup-ai-agent-worktree.sh agent-1 feature authentication
-
Monitoring: Track active agent worktrees
git worktree list | grep "agent-"
Monorepo-Specific (shepherdjerred/monorepo)
The generic workflow above applies, but this repo nests worktrees at .claude/worktrees/<name> and has fresh-worktree setup gotchas. Use the monorepo command from the root CLAUDE.md (git worktree add .claude/worktrees/<slug> -b feature/<slug> origin/main).
Team / multi-agent work stays out of main
When spawning a team of agents to implement a plan, every teammate works in its own worktree (e.g. .claude/worktrees/<feature>-<role>), never the user's main checkout — bake the worktree-setup commands into each teammate's bootstrap prompt so they don't cd into main. The team lead coordinates from its own session and does not edit monorepo files in main either.
Scoped verification — one repo-wide fan-out at a time
Verify with package-scoped commands (cd packages/<name> && bun run typecheck|test, or bun run --filter='./packages/<name>' typecheck|test if the package is registered as a Bun workspace from the repo root), not root-level bun run typecheck|test|build. A root run fans out over ~35 packages, each booting its own node/bun toolchain; when several worktree sessions or teammates do this concurrently, the spawn storm has frozen the whole machine (6,000+ processes, 20-30 GB of anonymous memory within seconds → macOS jetsam freeze; see packages/docs/logs/2026-07-11_macbook-hang-jetsam-investigation.md). Reserve root-level runs for genuinely repo-wide changes, run at most one at a time machine-wide, and bake the scoped commands into teammate bootstrap prompts so parallel agents never all fan out at once. There is no CI (pipeline removed 2026-07), so anything you don't verify locally ships unverified.
Commit + push after every phase
Deleting a worktree discards uncommitted working-tree changes with no recovery path (never git added = no git objects in the shared .git). A large, fully-working feature was lost this way. For any multi-step / PR-bound work, commit after every phase and push the branch immediately (open a draft PR early) so the work is backed up off-machine — never hold a big change uncommitted.
Fresh worktree: mise trust first
In a worktree from a plain git worktree add (not claude -w or the SessionStart hook), bun won't launch: it resolves to a mise shim that refuses to run while the worktree's .mise.toml is untrusted (mise keys trust by absolute path, so every new worktree starts untrusted). Run mise trust -y --all (~0.06s) before any bun command. claude -w <slug> and the SessionStart hook do this automatically.
Fresh worktree: eslint needs deps + built eslint-config
(Git hooks were removed 2026-07 — nothing lints on commit anymore, so run eslint yourself.) Linting packages/homelab in a fresh worktree fails with two errors in order: (1) The 'jiti' library is required... — bun run --filter @shepherdjerred/homelab typecheck only populates packages/homelab/src/cdk8s/node_modules, so you must run a plain cd packages/homelab && bun install to get packages/homelab/node_modules/.bin/eslint + jiti at the package root; (2) Cannot find module '@eslint/js' — fix with cd packages/eslint-config && bun install && bun run build.
Install only what you touch (setup.ts was removed 2026-07)
There is no setup orchestrator anymore — setup is manual and scoped (see the root AGENTS.md "Development Setup"). In a fresh worktree: build the shared file: producers first (eslint-config, llm-models, webring, astro-opengraph-images, discord-video-stream, discord-stream-lifecycle, helm-types — each bun install --frozen-lockfile && bun run build, seconds apiece), then bun install --frozen-lockfile + codegen (bun run generate where it exists) in the package(s) you're touching. Never install all ~35 packages for a single-package change (~13-15G of node_modules you won't use).
Producers before consumers, always. This ordering is the #1 cause of "Cannot find module @shepherdjerred/eslint-config" / stale llm-models errors: those are file: deps, and Bun's hoisted linker copies a file: dep's contents into the consumer's node_modules at install time only — it doesn't track the producer's dist/ changing afterward. If you rebuild a producer after the consumer was installed, re-run bun install --force in the consumer.
Don't use bun install --backend=symlink in Prisma-consuming packages (scout, mk64, birmel): Prisma's installer packages (@prisma/engines, prisma) have postinstall scripts that break under Bun's symlink backend.
knip is manual-only
knip was removed from pre-commit in 2026-06, and the CI step that still ran it was removed with the pipeline 2026-07 — nothing runs knip automatically anymore. It loads every workspace in knip.json, so a knip run needs every workspace's deps installed (a full-repo install); run it manually from the repo root (with --workspace to scope) when you want dead-code coverage. A fresh worktree needs no whole-repo install just to commit — a change touching one package only needs that package's deps + a built eslint-config.
Recovering a wiped worktree
A concurrent cleanup/prune over .claude/worktrees/* can delete your worktree's working tree AND its .git pointer mid-session. Symptoms: git rev-parse --show-toplevel suddenly returns the MAIN checkout and --abbrev-ref HEAD says main; files you read minutes ago are gone; Edit/Write fail with 'File does not exist'; git worktree list marks the worktree prunable. The branch ref + admin dir usually survive at <main>/.git/worktrees/<name>/, so committed work is safe.
Recovery: (1) recreate the deleted pointer — printf 'gitdir: <main>/.git/worktrees/<name>\n' > <worktree>/.git; (2) VERIFY git -C <worktree> rev-parse --show-toplevel is the worktree (not main) before any restore, else a reset hits main; (3) git reset --hard HEAD to repopulate, then reinstall node_modules (untracked, also wiped). If the entire dir is gone, from main run git worktree prune then git worktree add .claude/worktrees/<name> <branch>. Commit + push ASAP afterward in case the cleanup re-fires.
Integration with PR Workflow
Combined Start-to-PR Script
#!/bin/bash
set -euo pipefail
COMMAND=${1:?Usage: feature.sh <start|pr|done> [name]}
WORKTREE_BASE="$HOME/worktrees/$(basename $(git rev-parse --show-toplevel))"
case $COMMAND in
start)
FEATURE_NAME=${2:?Usage: feature.sh start <feature-name>}
BRANCH_NAME="feature/${FEATURE_NAME}"
WORKTREE_DIR="$WORKTREE_BASE/$FEATURE_NAME"
echo "Starting feature: $FEATURE_NAME"
git fetch origin
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" origin/main
cd "$WORKTREE_DIR"
echo "✅ Ready to code!"
echo " Path: $WORKTREE_DIR"
echo " Branch: $BRANCH_NAME"
;;
pr)
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" = "main" ]; then
echo "❌ Must be in feature worktree"
exit 1
fi
git push -u origin "$CURRENT_BRANCH"
gh pr create --fill
echo "✅ PR created!"
gh pr view
;;
done)
CURRENT_BRANCH=$(git branch --show-current)
WORKTREE_PATH=$(pwd)
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "❌ Uncommitted changes"
exit 1
fi
if gh pr view --json state --jq .state | grep -q "MERGED"; then
echo "✅ PR merged!"
MAIN_WORKTREE=$(git worktree list | grep "(main)" | awk '{print $1}')
cd "$MAIN_WORKTREE"
git pull origin main
git worktree remove "$WORKTREE_PATH"
git branch -d "$CURRENT_BRANCH"
echo "✅ Cleaned up worktree and branch"
else
echo "⚠️ PR not merged yet"
gh pr view
fi
;;
*)
echo "Unknown command: $COMMAND"
echo "Usage: feature.sh <start|pr|done> [name]"
exit 1
;;
esac
Best Practices
1. Worktree Naming Convention
git worktree add ../feature-auth -b feature/auth
git worktree add ../auth -b feature/authentication
feature/auth → ../feature-auth
fix/login-bug → ../fix-login-bug
refactor/api → ../refactor-api
2. Keep Worktrees Outside Main Repo
~/git/myproject/ (main)
~/git/myproject-feature-auth/ (worktree)
~/git/myproject-fix-bug/ (worktree)
~/git/myproject/ (main)
~/git/myproject/feature-auth/ (worktree - AVOID)
3. Regular Cleanup
git fetch --prune
git worktree prune
git branch --merged | grep -v "main\|master" | xargs git branch -d
4. Don't Share Branches Between Worktrees
git worktree add ../feature-1 -b feature/auth
git worktree add ../feature-2 feature/auth
git worktree add ../feature-1 -b feature/auth
git worktree add ../feature-2 -b feature/auth-v2
5. Backup Before Removing
if ! (cd ../feature-auth && git diff --quiet); then
echo "⚠️ Uncommitted changes in worktree!"
exit 1
fi
git worktree remove ../feature-auth
Common Workflows
Workflow 1: Quick Bug Fix
git worktree add ../fix-critical -b fix/critical-bug origin/main
cd ../fix-critical
echo "fix" > bug-fix.ts
git add bug-fix.ts
git commit -m "fix: critical bug"
git push -u origin fix/critical-bug
gh pr create --title "fix: critical bug" --body "Fixes #123"
cd ~/git/myproject
git worktree remove ../fix-critical
git branch -d fix/critical-bug
Workflow 2: Long-Running Feature
git worktree add ../feature-big -b feature/big-feature origin/main
cd ../feature-big
git commit -m "feat: part 1"
git push -u origin feature/big-feature
git fetch origin
git rebase origin/main
gh pr create --fill
Workflow 3: Parallel Features
git worktree add ../feature-api -b feature/api
git worktree add ../feature-ui -b feature/ui
git worktree add ../feature-docs -b feature/docs
cd ../feature-api
cd ../feature-ui
cd ../feature-docs
Troubleshooting
Issue: "Cannot remove worktree with uncommitted changes"
cd ../feature-auth
git add -A && git commit -m "WIP"
git stash
git worktree remove --force ../feature-auth
Issue: "Branch already checked out"
git worktree add ../feature-auth-v2 -b feature/auth-v2 feature/auth
Issue: Worktree directory deleted manually
git worktree prune
git worktree list
When to Ask for Help
Ask the user for clarification when:
- Worktree layout preferences (flat vs nested, naming conventions)
- How to handle merge conflicts during rebase
- Whether to keep or remove worktree after PR merge
- Multiple people working on same repository with worktrees
- Integration with IDEs or editors for worktree navigation