January 14, 2026
Claude Development

Building a Worktree Manager Script for Claude Code

You've got worktrees set up, but now you're managing them manually. You create a worktree with a cryptic name like wt-a3f9e2, forget what it's for, switch to it three months later expecting it to still exist, and find yourself cleaning up dead branches. Your Claude Code sessions are scattered across worktrees with no tracking of which session belongs where.

This is where a worktree manager script saves you. Instead of wrestling with Git's manual worktree commands, you get a single CLI tool that handles creation, listing, cleanup, and session tracking. Let's build one.

Why Git Worktrees Matter for Claude Code

Git worktrees are the heavyweight champion of context switching. Instead of changing branches in your current directory (which might lose unsaved state, require stashing changes, or trigger expensive rebuilds), worktrees give you completely isolated directories with different branches checked out. Each worktree is its own filesystem tree.

For Claude Code workflows, worktrees are essential because they let you have multiple Claude Code sessions going in parallel without interference. You might have Session A working on a feature in one worktree while Session B fixes a bug in another. No conflicts, no stashing, no waiting for rebuilds.

But managing worktrees manually is tedious. Git's native worktree commands are powerful but verbose. After your fifth worktree, you'll start losing track of which one is which. A manager script brings order to chaos.

The Mental Model: Worktrees as Workspaces

Think of worktrees as persistent workspaces. Unlike branches (which are just pointers to commits), worktrees are actual directories with actual files. Each one can have uncommitted changes, unstaged files, and a different working state.

This is powerful because you can open Claude Code in multiple worktrees and have multiple independent sessions. But it's also dangerous: if you forget to commit changes before cleaning up a worktree, those changes are gone.

A worktree manager adds metadata on top of this. It tracks not just which worktrees exist, but when they were created, when they were last used, what ticket they're associated with, and which Claude Code session is using them. This metadata becomes your safety net.

Table of Contents
  1. Why Git Worktrees Matter for Claude Code
  2. The Mental Model: Worktrees as Workspaces
  3. Designing the Worktree Manager CLI
  4. Key Design Decisions
  5. The Core Script
  6. The Psychology of Worktree Management
  7. Worktree Naming Conventions
  8. Session Tracking Integration
  9. Why Session Tracking Matters
  10. Auto-Naming from Ticket Numbers
  11. Jira Integration and Workflow Automation
  12. Cleanup with Stale Detection
  13. Understanding Stale Worktree Risks
  14. Preventing Accidental Data Loss
  15. Integration Example: Full Workflow
  16. Making It Production-Ready
  17. Safety-First Implementation Practices
  18. Performance Considerations at Scale
  19. Making It Production-Ready: Advanced
  20. Configuration and Customization
  21. Validation and Rollback
  22. Shell Completion: Tab-Completion for Worktree Names
  23. Advanced Features: Slack Notifications
  24. Integration with Git Hooks
  25. Scaling Your Worktree Manager
  26. Multi-User Worktrees
  27. Team-Level Policies
  28. Monitoring and Analytics
  29. Troubleshooting and Edge Cases
  30. Handling Corrupted JSON
  31. Handling Orphaned Worktrees
  32. Testing Your Script
  33. Advanced Patterns: Worktree Archives
  34. Performance Considerations
  35. Database vs. JSON at Different Scales
  36. Real-World Usage: A Day in the Life
  37. Cross-Team Adoption and Customization
  38. Packaging and Distribution
  39. Integration with Git Hooks: Advanced Patterns
  40. Monitoring and Metrics
  41. Understanding Worktree Lifecycle
  42. Scaling the Manager
  43. Real-World Complications
  44. Conclusion
  45. The Broader Impact: Worktree Management as Organizational Memory
  46. Beyond Scripting: The Philosophy of Worktree Management

Designing the Worktree Manager CLI

A good worktree manager needs to solve these problems:

  1. Naming: Auto-generate meaningful names from branch names or ticket IDs instead of cryptic hashes.
  2. Listing: Show all active worktrees with their age, branch, and associated Claude Code session status.
  3. Session tracking: Record which Claude Code session opened which worktree.
  4. Cleanup: Detect and remove stale worktrees (not accessed in 30+ days).
  5. Integration: Work seamlessly with Claude Code's session management.

Our design will be a Bash script called wtm (worktree manager). Here's the interface:

Key Design Decisions

Before we build, let's think through some architecture decisions that will shape how the script behaves.

Storage: Should we use JSON or a database? JSON is simpler to start with, human-readable, and doesn't require external dependencies. But it doesn't scale well beyond a few dozen worktrees and has concurrency issues if multiple processes modify it simultaneously. For this guide, we'll use JSON with atomic operations (write-to-temp-file then move). If you're managing hundreds of worktrees, consider upgrading to SQLite.

Naming convention: Should we require unique names? Yes. Should we auto-generate them? Yes, but let users override. Should names be filesystem-safe? Absolutely. Names become directory names, so they can't contain slashes, spaces, or special characters.

Session tracking: How do we know which Claude Code session is using which worktree? We'll write a wrapper script that updates the metadata file when Claude Code starts. The wrapper runs Claude Code with the session ID as an environment variable so the worktree manager can associate the PID with the worktree.

Cleanup strategy: How long before a worktree is considered stale? 30 days is a good default (most features finish in less time). But allow override with a flag. And always use dry-run mode first so users can see what would be deleted.

Let's implement with these decisions in mind:

bash
wtm create [name] [--branch branch-name] [--ticket TICKET-123]
wtm list [--stale] [--verbose]
wtm switch [name]
wtm remove [name]
wtm cleanup [--dry-run] [--age 30]
wtm status [name]

Let's implement it.

The Core Script

bash
#!/bin/bash
# wtm - Worktree Manager for Claude Code
# Manages worktrees with session tracking and cleanup
 
set -euo pipefail
 
# Configuration
WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
WTM_DB="$WTM_HOME/worktrees.json"
WTM_SESSIONS="$WTM_HOME/sessions"
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
 
# Initialize environment
mkdir -p "$WTM_HOME" "$WTM_SESSIONS"
 
# ============================================================================
# Utility Functions
# ============================================================================
 
log() { echo "[$(date +'%H:%M:%S')] $1"; }
error() { echo "ERROR: $1" >&2; exit 1; }
warn() { echo "WARNING: $1" >&2; }
 
ensure_json_db() {
  if [[ ! -f "$WTM_DB" ]]; then
    echo '{"worktrees": []}' > "$WTM_DB"
  fi
}
 
get_worktree_dir() {
  local name="$1"
  echo "$REPO_ROOT/.claude/worktrees/$name"
}
 
get_timestamp() {
  date +%s
}
 
# ============================================================================
# Worktree Database Operations
# ============================================================================
 
record_worktree() {
  local name="$1"
  local branch="$2"
  local session_id="$3"
  local created_at=$(get_timestamp)
 
  ensure_json_db
 
  # Use jq to add the worktree record
  local tmp=$(mktemp)
  jq --arg name "$name" \
     --arg branch "$branch" \
     --arg session_id "$session_id" \
     --arg created_at "$created_at" \
     '.worktrees += [{
       "name": $name,
       "branch": $branch,
       "session_id": $session_id,
       "created_at": $created_at,
       "last_accessed": $created_at
     }]' \
     "$WTM_DB" > "$tmp"
 
  mv "$tmp" "$WTM_DB"
}
 
remove_worktree_record() {
  local name="$1"
  ensure_json_db
 
  local tmp=$(mktemp)
  jq --arg name "$name" \
     '.worktrees |= map(select(.name != $name))' \
     "$WTM_DB" > "$tmp"
 
  mv "$tmp" "$WTM_DB"
}
 
update_last_accessed() {
  local name="$1"
  ensure_json_db
 
  local tmp=$(mktemp)
  jq --arg name "$name" \
     --arg timestamp "$(get_timestamp)" \
     '.worktrees |= map(if .name == $name then .last_accessed = $timestamp else . end)' \
     "$WTM_DB" > "$tmp"
 
  mv "$tmp" "$WTM_DB"
}
 
# ============================================================================
# Main Commands
# ============================================================================
 
cmd_create() {
  local name="${1:-}"
  local branch="${2:-}"
  local ticket="${3:-}"
 
  # Validate inputs
  if [[ -z "$name" ]]; then
    error "Usage: wtm create <name> [--branch <branch>] [--ticket <ticket>]"
  fi
 
  # Parse optional arguments
  while [[ $# -gt 1 ]]; do
    case "$2" in
      --branch)
        branch="$3"
        shift 2
        ;;
      --ticket)
        ticket="$3"
        shift 2
        ;;
      *)
        shift
        ;;
    esac
  done
 
  # Generate branch name if not provided
  if [[ -z "$branch" ]]; then
    branch="feature/$name"
    if [[ -n "$ticket" ]]; then
      branch="$ticket/$name"
    fi
  fi
 
  local wt_dir=$(get_worktree_dir "$name")
  local session_id=$(uuidgen 2>/dev/null || echo "session-$(date +%s)")
 
  # Create worktree
  if [[ -d "$wt_dir" ]]; then
    error "Worktree '$name' already exists at $wt_dir"
  fi
 
  log "Creating worktree: $name"
  git worktree add "$wt_dir" -b "$branch" 2>/dev/null || \
    git worktree add "$wt_dir" "$branch" 2>/dev/null || \
    error "Failed to create worktree (branch may already exist)"
 
  # Record in database
  record_worktree "$name" "$branch" "$session_id"
 
  # Create session metadata
  cat > "$WTM_SESSIONS/$session_id.json" <<EOF
{
  "session_id": "$session_id",
  "worktree_name": "$name",
  "branch": "$branch",
  "ticket": "$ticket",
  "created_at": $(get_timestamp),
  "last_opened_at": $(get_timestamp),
  "claude_code_pid": null
}
EOF
 
  log "✓ Worktree created: $name (branch: $branch)"
  log "  Session ID: $session_id"
  log "  Directory: $wt_dir"
}
 
cmd_list() {
  local verbose="${1:-}"
  ensure_json_db
 
  echo "=== Active Worktrees ==="
  echo ""
 
  jq -r '.worktrees[] |
    @json' "$WTM_DB" | while read -r line; do
    local name=$(echo "$line" | jq -r '.name')
    local branch=$(echo "$line" | jq -r '.branch')
    local session_id=$(echo "$line" | jq -r '.session_id')
    local created_at=$(echo "$line" | jq -r '.created_at')
    local last_accessed=$(echo "$line" | jq -r '.last_accessed')
 
    local wt_dir=$(get_worktree_dir "$name")
    local exists="✓"
    if [[ ! -d "$wt_dir" ]]; then
      exists="✗ STALE"
    fi
 
    local age_days=$(( ($(get_timestamp) - created_at) / 86400 ))
    local last_accessed_days=$(( ($(get_timestamp) - last_accessed) / 86400 ))
 
    echo "  [$exists] $name"
    echo "      Branch: $branch"
    echo "      Age: ${age_days}d | Last accessed: ${last_accessed_days}d ago"
 
    if [[ "$verbose" == "--verbose" ]]; then
      echo "      Session: $session_id"
      echo "      Path: $wt_dir"
    fi
    echo ""
  done
}
 
cmd_switch() {
  local name="$1"
  if [[ -z "$name" ]]; then
    error "Usage: wtm switch <name>"
  fi
 
  local wt_dir=$(get_worktree_dir "$name")
  if [[ ! -d "$wt_dir" ]]; then
    error "Worktree '$name' not found at $wt_dir"
  fi
 
  update_last_accessed "$name"
 
  log "Switching to worktree: $name"
  cd "$wt_dir"
  log "✓ Changed directory to: $(pwd)"
}
 
cmd_remove() {
  local name="$1"
  if [[ -z "$name" ]]; then
    error "Usage: wtm remove <name>"
  fi
 
  local wt_dir=$(get_worktree_dir "$name")
  if [[ ! -d "$wt_dir" ]]; then
    warn "Worktree directory not found, removing from database only"
  else
    log "Removing worktree: $name"
    git worktree remove "$wt_dir" --force 2>/dev/null || \
      error "Failed to remove worktree (may still have uncommitted changes)"
  fi
 
  remove_worktree_record "$name"
 
  # Clean up session file
  local session_file=$(mktemp)
  if jq -e '.session_id' "$WTM_SESSIONS"/*.json 2>/dev/null | grep -q "$name"; then
    rm -f "$WTM_SESSIONS"/*"$name"*.json
  fi
 
  log "✓ Worktree removed: $name"
}
 
cmd_cleanup() {
  local dry_run="${1:-}"
  local age_threshold=${2:-30}
 
  ensure_json_db
 
  echo "=== Stale Worktree Cleanup ==="
  echo "Threshold: $age_threshold days"
  echo ""
 
  local removed_count=0
  jq -r '.worktrees[] | @json' "$WTM_DB" | while read -r line; do
    local name=$(echo "$line" | jq -r '.name')
    local last_accessed=$(echo "$line" | jq -r '.last_accessed')
    local wt_dir=$(get_worktree_dir "$name")
 
    local days_since=$(( ($(get_timestamp) - last_accessed) / 86400 ))
 
    if [[ $days_since -gt $age_threshold ]]; then
      echo "  ✗ STALE: $name (last accessed ${days_since}d ago)"
 
      if [[ "$dry_run" != "--dry-run" ]]; then
        if [[ -d "$wt_dir" ]]; then
          git worktree remove "$wt_dir" --force 2>/dev/null || true
        fi
        remove_worktree_record "$name"
        echo "    → Removed"
        ((removed_count++))
      else
        echo "    → Would be removed (dry-run mode)"
      fi
    fi
  done
 
  if [[ "$dry_run" == "--dry-run" ]]; then
    echo ""
    echo "Dry-run mode: no changes made. Run without --dry-run to cleanup."
  fi
}
 
cmd_status() {
  local name="$1"
  if [[ -z "$name" ]]; then
    error "Usage: wtm status <name>"
  fi
 
  local wt_dir=$(get_worktree_dir "$name")
  if [[ ! -d "$wt_dir" ]]; then
    error "Worktree '$name' not found"
  fi
 
  cd "$wt_dir"
 
  echo "=== Worktree Status: $name ==="
  echo ""
  echo "Path: $wt_dir"
  echo "Current branch: $(git branch --show-current)"
  echo ""
  echo "Git status:"
  git status --short || echo "(no changes)"
}
 
# ============================================================================
# Main Entry Point
# ============================================================================
 
main() {
  local cmd="${1:-help}"
 
  case "$cmd" in
    create)
      shift
      cmd_create "$@"
      ;;
    list|ls)
      shift
      cmd_list "$@"
      ;;
    switch|cd)
      shift
      cmd_switch "$@"
      ;;
    remove|rm)
      shift
      cmd_remove "$@"
      ;;
    cleanup)
      shift
      cmd_cleanup "$@"
      ;;
    status)
      shift
      cmd_status "$@"
      ;;
    *)
      cat <<EOF
wtm - Worktree Manager for Claude Code
 
Usage:
  wtm create <name> [--branch <branch>] [--ticket <ticket>]
    Create a new worktree with optional branch/ticket naming
 
  wtm list [--verbose]
    List all active worktrees with age and access info
 
  wtm switch <name>
    Switch to a worktree (changes current directory)
 
  wtm remove <name>
    Remove a worktree and clean up
 
  wtm cleanup [--dry-run] [--age <days>]
    Remove stale worktrees not accessed in N days (default: 30)
 
  wtm status <name>
    Show status of a specific worktree
 
Example:
  wtm create auth-refactor --ticket AUTH-123
  wtm list --verbose
  wtm switch auth-refactor
  wtm cleanup --dry-run
EOF
      ;;
  esac
}
 
main "$@"

The Psychology of Worktree Management

Before we dive deeper into the code, it's worth understanding why worktree management is actually a human problem, not a technical one. The technical part—creating and switching worktrees—is easy. The hard part is remembering which worktree you're working on, what you left it in, and whether it's still relevant.

When developers manually manage worktrees, they usually follow a naming pattern internally (mentally) but don't externalize that pattern in the filesystem. They name a worktree feature-auth, but then two weeks later they can't remember if they finished that feature, if it was merged, or if they just abandoned it.

A worktree manager solves this by externalizing that mental model. Every worktree has metadata: creation date, last access date, associated ticket, branch name. When you list worktrees, you immediately see which ones are active and which are stale. This external state becomes your truth.

This psychological aspect is why the metadata tracking in the script above matters so much. The creation date and last access date aren't just for housekeeping. They're external signals that help you (and Claude Code) understand the lifecycle of a worktree.

Worktree Naming Conventions

A good naming convention is essential. Names should be:

  • Meaningful: feature-oauth2-provider beats wt-a3f9e2
  • Unique: No two worktrees with the same name
  • Filesystem-safe: No slashes, spaces, or special characters
  • Short: Typing and tab-completion work better with 30 character limits

The script above auto-generates names from branch names or ticket IDs. A ticket-based approach (AUTH-123-add-oauth2-provider) links the worktree to your issue tracking system, making it easier to find context.

Session Tracking Integration

The script above records sessions in JSON. Now let's enhance it to integrate with Claude Code. When you open Claude Code in a worktree, we want to track the PID:

bash
#!/bin/bash
# claude-code-wrapper.sh - Wrapper for claude code with session tracking
 
WORKTREE_NAME=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
SESSION_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm/sessions"
 
# Find the session file for this worktree
SESSION_FILE=$(grep -l "\"worktree_name\": \"$WORKTREE_NAME\"" "$SESSION_HOME"/*.json 2>/dev/null | head -1)
 
if [[ -n "$SESSION_FILE" ]]; then
  log "Opening Claude Code for worktree: $WORKTREE_NAME"
 
  # Update session metadata with PID
  CLAUDE_PID=$$
  jq --arg pid "$CLAUDE_PID" \
     '.claude_code_pid = $pid | .last_opened_at = now | int' \
     "$SESSION_FILE" > "$SESSION_FILE.tmp"
  mv "$SESSION_FILE.tmp" "$SESSION_FILE"
fi
 
# Launch Claude Code with passed arguments
exec claude code "$@"

Install this wrapper to your PATH and alias claude code to use it.

Why Session Tracking Matters

You might wonder: why track which Claude Code process is using which worktree? Isn't that overkill? In practice, it's essential for teams using Claude Code heavily. When you're running multiple Claude Code sessions in parallel, you need to know:

  • Which session is blocked on what
  • Which worktree needs attention
  • Whether a session has stale state

Session tracking also prevents accidental conflicts. If you try to open the same worktree in two Claude Code sessions simultaneously, you get race conditions on commits and file edits. By tracking sessions, the manager can warn you: "Session xyz-789 is already using this worktree."

Beyond that, session tracking creates an audit trail. When you later ask "what was Claude Code doing during that deployment issue?", you can correlate session IDs to timestamps and reconstruction what happened.

The wrapper script above is minimal, but in production, you'd extend it to:

  • Update a session status file on startup
  • Log all Claude Code commands to a per-session log
  • Report session state to a dashboard
  • Detect stale sessions and warn the user

Auto-Naming from Ticket Numbers

A practical enhancement: auto-generate worktree names from ticket IDs:

bash
cmd_create_from_ticket() {
  local ticket="$1"
  if [[ -z "$ticket" ]]; then
    error "Usage: wtm ticket <TICKET-ID>"
  fi
 
  # Extract ticket prefix and number
  local prefix=${ticket%-*}
  local number=${ticket#*-}
 
  # Fetch ticket title from your issue tracker (example: Jira)
  local title=$(curl -s "https://jira.company.com/rest/api/3/issue/$ticket" \
    -H "Authorization: Bearer $JIRA_TOKEN" \
    | jq -r '.fields.summary' | \
    tr ' ' '-' | tr '[:upper:]' '[:lower:]' | \
    sed 's/[^a-z0-9-]//g' | \
    head -c 30)
 
  if [[ -n "$title" ]]; then
    local name="$ticket-$title"
  else
    local name="$ticket"
  fi
 
  # Create worktree with ticket naming
  cmd_create "$name" --branch "$prefix/$number" --ticket "$ticket"
}

Jira Integration and Workflow Automation

The snippet above fetches ticket details from Jira. This creates a powerful workflow: create a worktree from a ticket, and the manager automatically names it, creates a branch following your conventions, and links everything together.

But this integration goes deeper. Your manager can:

  • Automatically transition tickets to "In Progress" when a worktree is created
  • Link commits to tickets so reviewers understand the context
  • Close tickets when the worktree is cleaned up (if merged)
  • Track how long features actually take by measuring worktree age

This transforms the worktree manager from a local tool into a team coordination tool. Suddenly everyone can see which tickets are being actively worked on (because they have active worktrees), which ones are stalled (because worktrees are stale), and which ones are done (because worktrees are cleaned up).

Cleanup with Stale Detection

The cleanup command above deletes unused worktrees. Let's add a cron job to run it automatically:

bash
#!/bin/bash
# /etc/cron.daily/wtm-cleanup or ~/.local/cron/wtm-cleanup.sh
 
WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
LOGFILE="$WTM_HOME/cleanup.log"
 
echo "[$(date)] Running WTM cleanup" >> "$LOGFILE"
 
# Find all repos and run cleanup
for repo in ~/.config/wtm/repos/*; do
  if [[ -d "$repo/.git" ]]; then
    cd "$repo"
    ~/.local/bin/wtm cleanup --age 30 >> "$LOGFILE" 2>&1
  fi
done
 
echo "[$(date)] Cleanup complete" >> "$LOGFILE"

Understanding Stale Worktree Risks

What makes a worktree "stale"? The traditional metric is time since last access. But there are other signals:

  • No commits in 30 days: Probably abandoned
  • Branch is merged to main: Definitely done, safe to delete
  • Branch exists on remote but not locally: Might be out of sync
  • Large disk footprint: Taking up space for no benefit

A sophisticated cleanup strategy checks multiple signals. You might keep a worktree alive if:

  • It has recent commits, even if not accessed directly
  • Its branch is actively being reviewed in a PR
  • It's marked as "archived" by the user (not automatic cleanup)

The script above uses simple time-based cleanup, which works for most teams. But in production, add these refinements.

Preventing Accidental Data Loss

The cleanup command is destructive. You need safeguards:

  1. Dry-run mode: Always show what would be deleted before deleting
  2. Confirmation: Prompt before actually removing
  3. Backup: Keep a log of what was deleted, so recovery is possible
  4. Retention: Never auto-delete worktrees with uncommitted changes

The implementation above does dry-run, but not full backups. In production, keep a deletion log:

bash
cleanup_with_backup() {
  local name="$1"
  local wt_dir=$(get_worktree_dir "$name")
 
  # Check for uncommitted changes
  if [[ -d "$wt_dir" ]]; then
    cd "$wt_dir"
    if ! git diff-index --quiet HEAD --; then
      warn "Worktree $name has uncommitted changes, refusing to delete"
      return 1
    fi
  fi
 
  # Log the deletion
  echo "$(date): Deleted worktree $name" >> "$WTM_HOME/deletions.log"
 
  # Then proceed with deletion
  # ...
}

This ensures you can always trace what was deleted and when.

Integration Example: Full Workflow

Let's tie it all together with a realistic example:

bash
#!/bin/bash
# Example: Developer starts a feature branch with a ticket
 
# 1. Create worktree from ticket ID
wtm create --ticket AUTH-456
 
# Output:
# ✓ Worktree created: AUTH-456-add-oauth2-provider
# Session ID: a7b3-c4d5-e6f7
# Directory: /repo/.claude/worktrees/AUTH-456-add-oauth2-provider
 
# 2. Switch to the worktree
wtm switch AUTH-456-add-oauth2-provider
 
# 3. Open Claude Code (using our wrapper)
claude code
 
# 4. Work on the feature... (days/weeks pass)
 
# 5. Later, list all active worktrees
wtm list --verbose
 
# Output:
# === Active Worktrees ===
# [✓] main
#     Branch: main
#     Age: 90d | Last accessed: 0d ago
#     Session: primary-session
#     Path: /repo
#
# [✓] AUTH-456-add-oauth2-provider
#     Branch: AUTH/456
#     Age: 15d | Last accessed: 1d ago
#     Session: a7b3-c4d5-e6f7
#     Path: /repo/.claude/worktrees/AUTH-456-add-oauth2-provider
 
# 6. Check for stale worktrees (before cleanup)
wtm cleanup --dry-run --age 60
 
# Output:
# === Stale Worktree Cleanup ===
# Threshold: 60 days
# ✗ STALE: old-refactor (last accessed 120d ago)
#   → Would be removed (dry-run mode)
 
# 7. Actually clean up
wtm cleanup --age 60
 
# Output:
# ✓ Worktree removed: old-refactor

Making It Production-Ready

To make your wtm script production-ready:

  1. Add bash completion: Generate tab-completion for worktree names
  2. Error handling: Catch edge cases (worktree corruption, permission errors)
  3. Atomic operations: Use temporary files and mv for safe JSON updates
  4. Logging: Write to $WTM_HOME/wtm.log for debugging
  5. Shell compatibility: Test on bash 4.0+ and zsh

Safety-First Implementation Practices

When you're managing file system operations (creating directories, modifying files, executing git commands), safety should be your top concern. Here are specific practices we applied in the script above:

Atomic JSON updates: Every JSON operation uses a temporary file. We modify the temp file, then atomically move it into place with mv. This prevents corruption if the process crashes mid-write.

Defensive error handling: Every external command (git, jq) is wrapped in error handling. If git fails to create a worktree, we report it and exit, rather than silently continuing.

User confirmation for destructive operations: The cleanup command has a --dry-run flag by default. Users see what would be deleted before it actually happens. This prevents accidental data loss.

Logging all operations: Every significant operation is logged with a timestamp. If something goes wrong, you can review the log to understand what happened and when.

Preventing duplicate names: The script checks if a worktree name already exists before creating a new one. This prevents confusion and silent overwrites.

Performance Considerations at Scale

The script above works fine for dozens of worktrees. But if you're managing hundreds, you'll notice performance issues. The jq pipeline has to parse the entire JSON file for each operation. Here's how to optimize:

In-memory caching: Load the JSON once at the start, do all operations in memory, write back at the end.

Index by name: Instead of iterating through all worktrees every time, build a name-to-index map for O(1) lookups.

Lazy evaluation: Commands like list that don't modify state don't need to write the file back. Skip the write for read-only operations.

Batch operations: If you're performing multiple operations (like cleanup), batch them together into a single JSON modification rather than separate read-modify-write cycles.

For a production system managing 500+ worktrees, consider migrating from JSON to SQLite. The performance difference becomes noticeable, and you gain the ability to query efficiently (e.g., "show me all worktrees created in the last week").

Making It Production-Ready: Advanced

Moving beyond the basics, here are advanced practices that separate hobby scripts from production tools:

Configuration and Customization

The script above hardcodes some values (like 30 days for stale cleanup). In production, you want these configurable. Create a config file:

bash
# ~/.config/wtm/config.sh
# WTM Configuration
 
# How many days before a worktree is considered stale
STALE_THRESHOLD_DAYS=30
 
# Maximum number of worktrees to keep
MAX_WORKTREES=50
 
# Automatically run cleanup daily
AUTO_CLEANUP_ENABLED=true
 
# Slack webhook for notifications (optional)
SLACK_WEBHOOK_URL=""
 
# Which test runner to use (jest, vitest, mocha, none)
TEST_RUNNER="jest"
 
# Notify before cleanup
NOTIFY_BEFORE_CLEANUP=true
NOTIFICATION_DAYS_BEFORE=5  # Notify when 25 days old

Then source this in your script:

bash
if [[ -f ~/.config/wtm/config.sh ]]; then
  source ~/.config/wtm/config.sh
fi

This lets teams customize the manager without modifying the core script.

Validation and Rollback

What happens if something goes wrong? A good manager has a rollback story. If cleanup accidentally deletes the wrong worktree, users should be able to recover.

One approach: keep a changelog. Before any destructive operation, log it:

bash
cmd_remove() {
  local name="$1"
  local wt_dir=$(get_worktree_dir "$name")
 
  # Before removing, save details to changelog
  jq -r '.worktrees[] | select(.name == "'"$name"'")' "$WTM_DB" >> "$WTM_HOME/changelog.log"
 
  # ... rest of removal logic
}

This creates an audit trail. If a user accidentally deletes a worktree, you can review the changelog and potentially recover it.

Shell Completion: Tab-Completion for Worktree Names

Users hate typing long worktree names. Add tab-completion support:

bash
#!/bin/bash
# bash-completion.d/wtm - Tab completion for wtm command
 
_wtm_completions() {
  local cur prev opts
  cur="${COMP_WORDS[COMP_CWORD]}"
  prev="${COMP_WORDS[COMP_CWORD-1]}"
 
  WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
  WTM_DB="$WTM_HOME/worktrees.json"
 
  # Main commands
  opts="create list switch remove cleanup status --help"
 
  # If second argument, complete with worktree names
  if [[ $COMP_CWORD -gt 1 ]]; then
    case "${COMP_WORDS[1]}" in
      switch|remove|status)
        # Get worktree names from database
        if [[ -f "$WTM_DB" ]]; then
          local names=$(jq -r '.worktrees[].name' "$WTM_DB" 2>/dev/null)
          COMPREPLY=($(compgen -W "$names" -- "$cur"))
        fi
        return 0
        ;;
      create)
        # Suggest flags for create command
        if [[ "$cur" == -* ]]; then
          COMPREPLY=($(compgen -W "--branch --ticket" -- "$cur"))
        fi
        return 0
        ;;
    esac
  fi
 
  COMPREPLY=($(compgen -W "$opts" -- "$cur"))
}
 
# Register completion
complete -o bashdefault -o default -o nospace -F _wtm_completions wtm

Install it:

bash
mkdir -p ~/.bash_completion.d/
cp bash-completion.d/wtm ~/.bash_completion.d/
echo 'source ~/.bash_completion.d/wtm' >> ~/.bashrc

For zsh users:

bash
#!/bin/bash
# zsh-completion.d/_wtm - Zsh completion for wtm
 
#compdef _wtm wtm
 
_wtm() {
  local commands=(
    "create:Create a new worktree"
    "list:List all active worktrees"
    "switch:Switch to a worktree"
    "remove:Remove a worktree"
    "cleanup:Clean up stale worktrees"
    "status:Show worktree status"
  )
 
  local state line
 
  _arguments \
    "1: :(($commands))" \
    "*::arg:->args"
 
  case $state in
    args)
      case $line[1] in
        switch|remove|status)
          # Complete with worktree names
          WTM_DB="${XDG_CONFIG_HOME:-$HOME/.config}/wtm/worktrees.json"
          if [[ -f "$WTM_DB" ]]; then
            local names=$(jq -r '.worktrees[].name' "$WTM_DB" 2>/dev/null)
            _values 'worktree' $names
          fi
          ;;
      esac
      ;;
  esac
}
 
_wtm "$@"

Install it:

bash
mkdir -p ~/.zsh/completions/
cp zsh-completion.d/_wtm ~/.zsh/completions/

Advanced Features: Slack Notifications

Extend your worktree manager to notify you when worktrees are about to be cleaned up:

bash
#!/bin/bash
# wtm-notify-stale.sh - Send Slack notifications for stale worktrees
 
WEBHOOK_URL="$SLACK_WEBHOOK_URL"
WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
WTM_DB="$WTM_HOME/worktrees.json"
AGE_THRESHOLD=25  # Notify when 25 days old (cleanup at 30)
 
notify_slack() {
  local worktree="$1"
  local days_old="$2"
  local branch="$3"
 
  local message="Worktree \`$worktree\` on branch \`$branch\` is $days_old days old and will be cleaned up in 5 days."
 
  curl -X POST "$WEBHOOK_URL" \
    -H 'Content-Type: application/json' \
    -d @- <<EOF
{
  "text": "⚠️ Stale Worktree Warning",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "$message"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": {"type": "plain_text", "text": "Keep It"},
          "value": "keep_$worktree",
          "action_id": "wtm_keep"
        },
        {
          "type": "button",
          "text": {"type": "plain_text", "text": "Delete Now"},
          "value": "delete_$worktree",
          "action_id": "wtm_delete",
          "style": "danger"
        }
      ]
    }
  ]
}
EOF
}
 
# Check for stale worktrees
jq -r '.worktrees[] | @json' "$WTM_DB" | while read -r line; do
  local name=$(echo "$line" | jq -r '.name')
  local branch=$(echo "$line" | jq -r '.branch')
  local last_accessed=$(echo "$line" | jq -r '.last_accessed')
 
  local days_since=$(( ($(date +%s) - last_accessed) / 86400 ))
 
  if [[ $days_since -gt $AGE_THRESHOLD ]]; then
    notify_slack "$name" "$days_since" "$branch"
  fi
done

Add this to your cron for periodic notifications:

bash
0 9 * * * ~/.local/bin/wtm-notify-stale.sh  # Every morning at 9 AM

Integration with Git Hooks

Automatically update worktree metadata when you commit:

bash
#!/bin/bash
# .git/hooks/post-commit - Update worktree last-accessed timestamp
 
WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
WTM_DB="$WTM_HOME/worktrees.json"
 
# Find which worktree this is
WORKTREE_PATH=$(git rev-parse --show-toplevel)
WORKTREE_NAME=$(basename "$WORKTREE_PATH")
 
if [[ -f "$WTM_DB" ]]; then
  # Update last_accessed in database
  jq --arg name "$WORKTREE_NAME" \
     --arg timestamp "$(date +%s)" \
     '.worktrees |= map(if .name == $name then .last_accessed = $timestamp else . end)' \
     "$WTM_DB" > "$WTM_DB.tmp"
  mv "$WTM_DB.tmp" "$WTM_DB"
fi

Make it executable:

bash
chmod +x .git/hooks/post-commit

Now every commit automatically updates the worktree's last-accessed timestamp—no manual intervention needed.

Scaling Your Worktree Manager

As your team grows, the worktree manager becomes increasingly valuable. What starts as a solo developer's convenience becomes team infrastructure. Let's think about how to scale it.

Multi-User Worktrees

Git supports sharing worktrees across users, but permissions become tricky. When user A creates a worktree, user B might not have write access. The manager should handle this:

bash
cmd_create() {
  local name="$1"
  # ... existing code ...
 
  # After creating the worktree, make it group-writable
  chmod -R g+rw "$wt_dir"
 
  # Update umask for all operations
  umask 0002
}

This lets team members access each other's worktrees. Useful for pair programming or knowledge transfer.

Team-Level Policies

As teams scale, you want policies: maximum worktrees per developer, maximum age before auto-cleanup, required naming conventions. Add a policy file:

bash
# ~/.config/wtm/policies.yaml
max_worktrees_per_user: 5
auto_cleanup_age_days: 30
require_ticket_in_name: true
allowed_ticket_systems:
  - JIRA
  - GITHUB
name_pattern: "^[A-Z]{3,}-[0-9]+.*$"

Then validate against these policies:

bash
validate_name() {
  local name="$1"
 
  if [[ "$REQUIRE_TICKET_IN_NAME" == "true" ]]; then
    if ! echo "$name" | grep -q "$NAME_PATTERN"; then
      error "Worktree name must match pattern: $NAME_PATTERN"
    fi
  fi
}

This enforces consistency across the team without heavy-handed centralized control.

Monitoring and Analytics

At scale, track usage patterns. Create a metrics collection system:

bash
log_metric() {
  local metric="$1"
  local value="$2"
 
  echo "$(date +%s),$metric,$value" >> "$WTM_HOME/metrics.csv"
}

Then analyze:

  • Average worktree lifespan per team
  • Most common worktree names (revealing patterns)
  • Cleanup frequency (revealing stale branches)
  • Peak concurrent worktrees (revealing team size/capacity)

Share these metrics with your team. It reveals interesting patterns: which teams create the most worktrees, whose worktrees live longest, who's most aggressive about cleanup.

Troubleshooting and Edge Cases

Handling Corrupted JSON

If your worktree database gets corrupted:

bash
#!/bin/bash
# wtm-repair-db.sh - Repair corrupted database
 
WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
WTM_DB="$WTM_HOME/worktrees.json"
 
# Backup original
cp "$WTM_DB" "$WTM_DB.backup.$(date +%s)"
 
# Rebuild from actual worktrees
echo '{"worktrees": []}' > "$WTM_DB"
 
# Rescan existing worktrees
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
for wt_dir in "$REPO_ROOT/.claude/worktrees"/*; do
  if [[ -d "$wt_dir" ]]; then
    name=$(basename "$wt_dir")
    branch=$(cd "$wt_dir" && git branch --show-current)
    session_id="recovered-$(date +%s)"
 
    # Re-add to database
    jq --arg name "$name" \
       --arg branch "$branch" \
       --arg session_id "$session_id" \
       '.worktrees += [{
         "name": $name,
         "branch": $branch,
         "session_id": $session_id,
         "created_at": $(date +%s),
         "last_accessed": $(date +%s)
       }]' \
       "$WTM_DB" > "$WTM_DB.tmp"
    mv "$WTM_DB.tmp" "$WTM_DB"
  fi
done
 
echo "✓ Database repaired"

Handling Orphaned Worktrees

If worktrees are deleted manually (not through wtm):

bash
# Sync database with actual filesystem
wtm-sync-db() {
  WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
  WTM_DB="$WTM_HOME/worktrees.json"
  REPO_ROOT=$(git rev-parse --show-toplevel)
 
  # Find worktrees in database that don't exist
  jq -r '.worktrees[] | .name' "$WTM_DB" | while read -r name; do
    wt_dir="$REPO_ROOT/.claude/worktrees/$name"
    if [[ ! -d "$wt_dir" ]]; then
      echo "Removing orphaned entry: $name"
      jq --arg name "$name" \
         '.worktrees |= map(select(.name != $name))' \
         "$WTM_DB" > "$WTM_DB.tmp"
      mv "$WTM_DB.tmp" "$WTM_DB"
    fi
  done
}

Testing Your Script

Before deploying to production, test thoroughly:

bash
#!/bin/bash
# test-wtm.sh - Integration tests for worktree manager
 
setup_test_env() {
  TEST_REPO=$(mktemp -d)
  cd "$TEST_REPO"
  git init
  echo "test" > README.md
  git add . && git commit -m "Initial"
}
 
test_create() {
  wtm create feature-123 --branch "feature/auth" --ticket "AUTH-123"
  [[ -d .claude/worktrees/feature-123 ]] && echo "✓ Create test passed" || echo "✗ Create test failed"
}
 
test_list() {
  output=$(wtm list)
  echo "$output" | grep -q "feature-123" && echo "✓ List test passed" || echo "✗ List test failed"
}
 
test_switch() {
  wtm switch feature-123
  [[ "$(pwd)" == *"feature-123"* ]] && echo "✓ Switch test passed" || echo "✗ Switch test failed"
  cd -
}
 
test_remove() {
  wtm remove feature-123
  [[ ! -d .claude/worktrees/feature-123 ]] && echo "✓ Remove test passed" || echo "✗ Remove test failed"
}
 
# Run tests
setup_test_env
test_create
test_list
test_switch
test_remove
 
# Cleanup
cd /tmp
rm -rf "$TEST_REPO"

Advanced Patterns: Worktree Archives

Beyond cleanup, consider an "archive" feature. Instead of deleting old worktrees, archive them. An archived worktree is moved to a separate location, compressed, and tracked in a separate metadata file. You can restore it later if needed.

bash
cmd_archive() {
  local name="$1"
  local wt_dir=$(get_worktree_dir "$name")
 
  if [[ ! -d "$wt_dir" ]]; then
    error "Worktree $name not found"
  fi
 
  local archive_dir="$WTM_HOME/archives"
  mkdir -p "$archive_dir"
 
  # Compress the worktree
  tar czf "$archive_dir/$name-$(date +%s).tar.gz" "$wt_dir"
 
  # Remove the original
  git worktree remove "$wt_dir" --force
 
  # Log the archive
  echo "$name archived on $(date)" >> "$WTM_HOME/archives.log"
}

Archiving is less destructive than deletion. You preserve history while freeing disk space. The trade-off is storage: compressed worktrees still take megabytes or gigabytes.

Performance Considerations

For large numbers of worktrees (> 20), optimize your script:

bash
# Use compiled jq instead of shell loops for JSON operations
# Add caching for frequently-accessed data
# Consider using SQLite instead of JSON for larger datasets
 
# Example: Use sqlite for large-scale deployments
#!/bin/bash
 
WTM_DB="${XDG_CONFIG_HOME:-$HOME/.config}/wtm/worktrees.db"
 
# Initialize database if needed
init_db() {
  sqlite3 "$WTM_DB" <<EOF
CREATE TABLE IF NOT EXISTS worktrees (
  id INTEGER PRIMARY KEY,
  name TEXT UNIQUE NOT NULL,
  branch TEXT NOT NULL,
  session_id TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  last_accessed INTEGER NOT NULL
);
 
CREATE INDEX IF NOT EXISTS idx_last_accessed ON worktrees(last_accessed);
EOF
}
 
# Add worktree (SQLite version)
add_worktree() {
  local name="$1"
  local branch="$2"
  local session_id="$3"
 
  sqlite3 "$WTM_DB" <<EOF
INSERT INTO worktrees (name, branch, session_id, created_at, last_accessed)
VALUES ('$name', '$branch', '$session_id', $(date +%s), $(date +%s));
EOF
}

Database vs. JSON at Different Scales

We've talked about upgrading from JSON to SQLite at scale. Let's be concrete about when that makes sense.

JSON works well if you have:

  • Fewer than 50 worktrees
  • Sequential access patterns (list, then act)
  • Single-writer, multi-reader use case
  • Simple queries (all worktrees, filter by name)

SQLite makes sense if you have:

  • 50+ worktrees regularly
  • Complex queries (find worktrees created in last week and unused)
  • Multi-writer scenarios (multiple users updating simultaneously)
  • Need for indexes (fast lookups by ticket ID, creation date, etc.)

The migration path is straightforward: keep the JSON interface (same CLI) but swap the storage backend. Users don't notice the change.

Real-World Usage: A Day in the Life

Let's walk through a realistic day using the wtm script:

Morning: Start Feature Work

bash
# 9:00 AM - Check what's in progress
$ wtm list
=== Active Worktrees ===
 
[✓] main
    Branch: main
    Age: 5d | Last accessed: 0d ago
 
[✓] feature-auth
    Branch: feature/auth-refactor
    Age: 3d | Last accessed: 1d ago
 
[✓] feature-payment
    Branch: feature/payment-v2
    Age: 1d | Last accessed: 0d ago
 
# 9:15 AM - Start new feature
$ wtm create feature-notifications --branch "feature/NOTIF-234" --ticket "NOTIF-234"
 Worktree created: feature-notifications
Session ID: sess-abc123
Directory: /repo/.claude/worktrees/feature-notifications
 
$ wtm switch feature-notifications
 Changed directory to: /repo/.claude/worktrees/feature-notifications
 
$ claude code  # Opens Claude Code for this worktree

Midday: Context Switching

bash
# 12:30 PM - Quick fix in different worktree
$ wtm switch feature-auth
 Changed directory to: /repo/.claude/worktrees/feature-auth
 
$ git status
# Show files changed in this branch
# Make quick fix, commit
$ git add . && git commit -m "Fix auth header parsing"
 
# Back to original work
$ wtm switch feature-notifications

Afternoon: Cleanup Detection

bash
# 3:00 PM - WTM cleanup runs (via cron)
# Check for stale worktrees (not touched in 30+ days)
$ wtm cleanup --dry-run
=== Stale Worktree Cleanup ===
Threshold: 30 days
 
 STALE: old-api-refactor (last accessed 45d ago)
 Would be removed (dry-run mode)
 
 STALE: deprecated-feature (last accessed 60d ago)
 Would be removed (dry-run mode)
 
# Actually clean up
$ wtm cleanup
 Worktree removed: old-api-refactor
 Worktree removed: deprecated-feature
 
# Now we have 3 worktrees instead of 5 (freed 2GB!)

End of Day: Status Report

bash
# 5:00 PM - Check final status
$ wtm list --verbose
=== Active Worktrees ===
 
[✓] main
    Branch: main
    Age: 5d | Last accessed: 0d ago
    Session: primary
    Path: /repo
 
[✓] feature-auth
    Branch: feature/auth-refactor
    Age: 3d | Last accessed: 0d ago (30m ago in reality)
    Session: sess-xyz789
    Path: /repo/.claude/worktrees/feature-auth
 
[✓] feature-payment
    Branch: feature/payment-v2
    Age: 1d | Last accessed: 0d ago (8h ago in reality)
    Session: sess-def456
    Path: /repo/.claude/worktrees/feature-payment
 
[✓] feature-notifications
    Branch: feature/NOTIF-234
    Age: 0d | Last accessed: 0d ago
    Session: sess-abc123
    Path: /repo/.claude/worktrees/feature-notifications

The script automatically logs all activity, so you can audit what happened throughout the day.

Cross-Team Adoption and Customization

When you want your team to use the worktree manager, provide clear onboarding. But don't force a single approach. Teams have different workflows:

  • Frontend teams might prefer naming by component (ui-auth, ui-dashboard)
  • Backend teams might prefer naming by API endpoint (api-payments, api-auth)
  • DevOps teams might prefer naming by infrastructure component (infra-ci, infra-monitoring)

Your manager should support these variations through configuration. Provide templates:

bash
# ~/.config/wtm/templates/frontend.sh
export WORKTREE_PREFIX="ui-"
export NAME_PATTERN="^ui-[a-z0-9-]+$"
export AUTO_CLEANUP_DAYS=14  # Shorter for fast-moving frontend teams

Then teams can source the template they prefer. This makes adoption easier because people use the tool their way, not your way.

Packaging and Distribution

Make your script installable:

bash
# install.sh - Installation script
 
INSTALL_DIR="${HOME}/.local/bin"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
 
mkdir -p "$INSTALL_DIR"
mkdir -p "$CONFIG_DIR"
 
# Copy main script
cp wtm "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/wtm"
 
# Copy hooks
cp hooks/* "$CONFIG_DIR/hooks/"
 
# Add to PATH if needed
if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
  echo 'export PATH="'"$INSTALL_DIR"':$PATH"' >> ~/.bashrc
  echo 'export PATH="'"$INSTALL_DIR"':$PATH"' >> ~/.zshrc
fi
 
echo "✓ wtm installed to $INSTALL_DIR"
echo "✓ Configuration directory: $CONFIG_DIR"
echo ""
echo "Get started:"
echo "  wtm --help"

Integration with Git Hooks: Advanced Patterns

The post-commit hook example above updates the last-accessed timestamp. You can extend this to build richer automation:

A pre-commit hook can enforce worktree-specific policies:

bash
#!/bin/bash
# .git/hooks/pre-commit - Enforce worktree policies
 
WORKTREE_PATH=$(git rev-parse --show-toplevel)
WORKTREE_NAME=$(basename "$WORKTREE_PATH")
 
# Extract ticket from name (e.g., AUTH-123 from AUTH-123-add-oauth)
TICKET=$(echo "$WORKTREE_NAME" | grep -o '^[A-Z][A-Z0-9]*-[0-9]*' || true)
 
# Enforce that commits reference their ticket
if [[ -n "$TICKET" ]]; then
  # Require commit messages to mention the ticket
  COMMIT_MSG=$(cat "$1")
  if ! echo "$COMMIT_MSG" | grep -q "$TICKET"; then
    echo "ERROR: Commit message must reference ticket $TICKET"
    exit 1
  fi
fi

This integrates your worktree manager with git workflows, making tickets and commits intrinsically linked.

Monitoring and Metrics

Track your worktree usage patterns over time with metrics:

bash
#!/bin/bash
# wtm-metrics.sh - Generate usage statistics
 
WTM_HOME="${XDG_CONFIG_HOME:-$HOME/.config}/wtm"
WTM_DB="$WTM_HOME/worktrees.json"
LOGFILE="$WTM_HOME/metrics.log"
 
# Calculate metrics
total_worktrees=$(jq '.worktrees | length' "$WTM_DB")
avg_age=$(jq '[.worktrees[].created_at] | add / length' "$WTM_DB")
avg_days_since_access=$(jq '[.worktrees[].last_accessed] | add / length' "$WTM_DB")
 
now=$(date +%s)
avg_age_days=$(( (now - avg_age) / 86400 ))
avg_access_days=$(( (now - avg_days_since_access) / 86400 ))
 
# Log metrics
cat >> "$LOGFILE" <<EOF
$(date): Total worktrees=$total_worktrees, Avg age=${avg_age_days}d, Avg last access=${avg_access_days}d ago
EOF

These metrics help answer questions like: "Are developers actually using worktrees?" and "How long do feature branches typically live?"

Understanding Worktree Lifecycle

A worktree has a lifecycle: creation, active use, inactivity, and cleanup. Understanding each phase helps you design the manager correctly.

Creation phase: User runs wtm create feature-auth --ticket AUTH-123. The manager creates a new git worktree, records metadata (name, branch, ticket, session ID), and returns a directory path.

Active use phase: Developer works in the worktree, makes commits, potentially opens Claude Code. The manager tracks access time (updated on every commit via post-commit hook) and session metadata (which Claude Code PID is using this worktree).

Inactivity phase: Developer stops using the worktree. Days or weeks pass. The manager tracks this as "stale" if it hasn't been accessed in 30+ days.

Cleanup phase: Periodically (via cron), the manager identifies stale worktrees and removes them. In dry-run mode, it shows what would be deleted. In normal mode, it actually deletes.

Each phase has failure modes. During creation, the branch might already exist. During active use, the worktree might be on a branch that's been deleted. During cleanup, the worktree might have uncommitted changes.

Your manager script needs to handle these gracefully. It should prevent accidental data loss (warn before deleting), provide clear error messages, and offer recovery options.

Scaling the Manager

As you accumulate more worktrees, you'll want additional features:

Caching: Looking up worktrees shouldn't require reading a JSON file every time. Add a simple in-memory cache that invalidates when the metadata file changes.

Filtering: Add flags to list only active, stale, or archived worktrees. Allow sorting by age, branch, or ticket.

Searching: Add a search feature that finds worktrees by branch name, ticket, or creation date. This becomes essential once you have a dozen worktrees.

Analytics: Track how long worktrees live on average, how often they're accessed, which tickets spawn the most worktrees. This data reveals patterns in your team's workflow.

Integration with IDEs: Make the worktree manager emit JSON output that your IDE can parse. VSCode, Neovim, and other editors could use this to provide worktree navigation built-in.

Real-World Complications

Worktree management looks simple in theory but has hidden complexity:

Broken worktrees: A worktree becomes "broken" if the branch it's on gets deleted from the remote, if the commit it's tracking becomes an orphan, or if the directory gets corrupted. Your manager should detect and report these, ideally with recovery options.

Disk space: Worktrees take up disk space. If you have 30 feature branches, each with a complete copy of the repository, that's 30x the disk space. You might need to add a feature that shows disk usage per worktree and helps identify the biggest space users.

Permission issues: If you create a worktree as one user and try to access it as another, you'll hit permission errors. Your manager should handle this gracefully (either make worktrees group-writable or warn about permission issues).

Concurrent access: If two Claude Code sessions try to commit to the same worktree simultaneously, git will refuse. Your manager should prevent this (maybe by preventing you from opening the same worktree twice, or by documenting the limitation).

Conclusion

A worktree manager script transforms Git worktrees from a powerful-but-manual tool into a seamless workflow. By tracking sessions, auto-naming, and cleaning up automatically, you eliminate cognitive overhead and stay organized even when juggling five branches at once.

Start with the core script above, customize it to your workflow, and watch your development velocity increase. Your Claude Code sessions will finally know which worktree they belong to.

The investment in this automation pays dividends: fewer deleted worktrees, less wasted disk space, and faster context switching. You'll spend less time managing Git and more time writing code.

The hidden layer of understanding is this: a worktree manager is really a session manager. Each worktree is a workspace, and each workspace is a distinct context for a Claude Code session. By managing worktrees systematically, you're building infrastructure that lets Claude Code understand and respect context switching.

As you scale from solo development to team collaboration, a good worktree manager becomes non-negotiable. It's the difference between organized chaos and pure chaos.

The Broader Impact: Worktree Management as Organizational Memory

Over time, your worktree manager becomes an artifact that tells the story of your project's development. The metadata you collect—creation dates, branch names, ticket IDs, last access timestamps—creates a historical record. You can answer questions like: how long did feature X actually take to develop? Which features lived longest before being abandoned? What percentage of features actually get merged?

This data is invaluable for process improvement. You'll start seeing patterns. Maybe your team's features average 3 weeks from creation to merge, but one team's features take 8 weeks. That's a signal to investigate. Is that team building more complex features? Are they blocked more often? Do they need more review capacity?

The worktree manager also documents institutional knowledge that would otherwise be lost. When a developer leaves, the worktrees they created remain in the system. New developers can see: "What was this person working on?" The metadata tells the story without requiring explicit documentation.

Consider the compounding benefit over a year. Every worktree created, used, and cleaned up adds one data point to your understanding of team productivity and development patterns. Over a year, you have hundreds of data points. That's real insight into how your team actually works, not how you think it should work.

Beyond Scripting: The Philosophy of Worktree Management

The script we've built is just implementation. The deeper philosophy is about removing cognitive load so developers can focus on writing code. Every decision the worktree manager makes automatically is a decision a human doesn't have to make.

Should I create a new worktree or just switch branches? The manager suggests worktree if you're juggling multiple features. Should I clean up this old worktree? The manager tells you when it's stale. What should I name this worktree? The manager auto-generates from the ticket ID.

This auto-decision-making compounds. Across dozens of worktrees over a year, you eliminate hundreds of small decisions and mental context switches. That's time and mental energy freed for actual development.

The best infrastructure is invisible. Developers use the worktree manager without thinking about it, just like they use Git without consciously thinking about the DAG structure. But underneath, sophisticated bookkeeping is happening. That's the mark of good systems infrastructure.


-iNet

Need help implementing this?

We build automation systems like this for clients every day.

Discuss Your Project