
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
- Why Git Worktrees Matter for Claude Code
- The Mental Model: Worktrees as Workspaces
- Designing the Worktree Manager CLI
- Key Design Decisions
- The Core Script
- The Psychology of Worktree Management
- Worktree Naming Conventions
- Session Tracking Integration
- Why Session Tracking Matters
- Auto-Naming from Ticket Numbers
- Jira Integration and Workflow Automation
- Cleanup with Stale Detection
- Understanding Stale Worktree Risks
- Preventing Accidental Data Loss
- Integration Example: Full Workflow
- Making It Production-Ready
- Safety-First Implementation Practices
- Performance Considerations at Scale
- Making It Production-Ready: Advanced
- Configuration and Customization
- Validation and Rollback
- Shell Completion: Tab-Completion for Worktree Names
- Advanced Features: Slack Notifications
- Integration with Git Hooks
- Scaling Your Worktree Manager
- Multi-User Worktrees
- Team-Level Policies
- Monitoring and Analytics
- Troubleshooting and Edge Cases
- Handling Corrupted JSON
- Handling Orphaned Worktrees
- Testing Your Script
- Advanced Patterns: Worktree Archives
- Performance Considerations
- Database vs. JSON at Different Scales
- Real-World Usage: A Day in the Life
- Cross-Team Adoption and Customization
- Packaging and Distribution
- Integration with Git Hooks: Advanced Patterns
- Monitoring and Metrics
- Understanding Worktree Lifecycle
- Scaling the Manager
- Real-World Complications
- Conclusion
- The Broader Impact: Worktree Management as Organizational Memory
- Beyond Scripting: The Philosophy of Worktree Management
Designing the Worktree Manager CLI
A good worktree manager needs to solve these problems:
- Naming: Auto-generate meaningful names from branch names or ticket IDs instead of cryptic hashes.
- Listing: Show all active worktrees with their age, branch, and associated Claude Code session status.
- Session tracking: Record which Claude Code session opened which worktree.
- Cleanup: Detect and remove stale worktrees (not accessed in 30+ days).
- 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:
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
#!/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-providerbeatswt-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:
#!/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:
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:
#!/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:
- Dry-run mode: Always show what would be deleted before deleting
- Confirmation: Prompt before actually removing
- Backup: Keep a log of what was deleted, so recovery is possible
- Retention: Never auto-delete worktrees with uncommitted changes
The implementation above does dry-run, but not full backups. In production, keep a deletion log:
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:
#!/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-refactorMaking It Production-Ready
To make your wtm script production-ready:
- Add bash completion: Generate tab-completion for worktree names
- Error handling: Catch edge cases (worktree corruption, permission errors)
- Atomic operations: Use temporary files and
mvfor safe JSON updates - Logging: Write to
$WTM_HOME/wtm.logfor debugging - 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:
# ~/.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 oldThen source this in your script:
if [[ -f ~/.config/wtm/config.sh ]]; then
source ~/.config/wtm/config.sh
fiThis 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:
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:
#!/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 wtmInstall it:
mkdir -p ~/.bash_completion.d/
cp bash-completion.d/wtm ~/.bash_completion.d/
echo 'source ~/.bash_completion.d/wtm' >> ~/.bashrcFor zsh users:
#!/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:
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:
#!/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
doneAdd this to your cron for periodic notifications:
0 9 * * * ~/.local/bin/wtm-notify-stale.sh # Every morning at 9 AMIntegration with Git Hooks
Automatically update worktree metadata when you commit:
#!/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"
fiMake it executable:
chmod +x .git/hooks/post-commitNow 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:
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:
# ~/.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:
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:
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:
#!/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):
# 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:
#!/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.
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:
# 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
# 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 worktreeMidday: Context Switching
# 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-notificationsAfternoon: Cleanup Detection
# 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
# 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-notificationsThe 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:
# ~/.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 teamsThen 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:
# 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:
#!/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
fiThis 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:
#!/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
EOFThese 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