Git Worktrees
Core Principle
Main repo stays on main/master — never edit directly. Every branch gets its own worktree. All worktrees for a project live under one per-project root directory, <project>.worktrees/, a sibling of the main repo. Delete the worktree and its branch after the PR merges.
Think of a worktree as a disposable branch folder, not a long-lived parallel environment.
Exception: Trivial one-liner commits on a solo project can go directly on main to avoid ceremony overhead.
Workflow
Check repo state before creating a worktree. Any worktree workflow description must name this dirty-state check before git worktree add:
git status --short
git branch --show-current
git worktree list
If the current worktree is dirty, ask whether to commit, stash, or create the new worktree anyway before proceeding. Confirm before running cleanup commands that remove worktrees or delete branches.
The per-project root is derived from the main worktree, so create and clean up correctly even when invoked from inside another worktree:
# main worktree is always the first entry of `git worktree list --porcelain`
main_wt=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')
project=$(basename "$main_wt")
root="$(dirname "$main_wt")/$project.worktrees"
# 1. Create worktree for new work (slug = branch with / → -)
mkdir -p "$root"
git worktree add "$root/fix-cron" -b fix-cron
# 2. Work there (open editor/Claude Code in that folder)
cd "$root/fix-cron"
# 3. After PR merges — confirm, then clean up from the main worktree
scripts/cleanup-worktree.sh fix-cron
Both steps are scripted: scripts/setup-worktree.sh <branch> [base] and scripts/cleanup-worktree.sh [branch].
Directory Layout
All worktrees for a project live under one root, <project>.worktrees/, a sibling of the main repo (never nested inside it):
~/projects/
├── myproject/ # main worktree — always on main, always clean
└── myproject.worktrees/ # per-project worktree root
├── fix-cron/ # worktree for fix-cron branch
├── feature-auth/ # worktree for feature/auth branch
└── bugfix-123/ # worktree for bugfix/issue-123 branch
Why a sibling root: one tidy place per project, no .gitignore pollution, clean git status, independent build artifacts. The root is removed automatically once its last worktree is cleaned up.
Naming Convention
Worktree dir is the branch slug only — slashes become dashes. No project prefix; the <project>.worktrees/ root already scopes it.
fix-cron—myproject.worktrees/fix-cronfeature/auth—myproject.worktrees/feature-authbugfix/issue-123—myproject.worktrees/bugfix-issue-123
When to Suggest Worktrees
- User wants to start a new feature, fix, or experiment
- User is about to edit code on main/master
- User wants to try multiple approaches to the same problem
- User has uncommitted changes and wants to start something else
Cleanup on PR Merge
When a PR merges, remove the worktree and delete its branch. State clearly what is being removed and why before running anything destructive.
Squash and rebase merges rewrite the commit, so git branch -d reports the branch as "not fully merged" even though the PR is merged. The source of truth is the PR state, not local ancestry — do not conflate them.
Use scripts/cleanup-worktree.sh [branch]. It is strict by default: unless gh confirms the PR is MERGED it changes nothing and exits with a single clear message. Pass --force to proceed anyway — when gh is not installed (you confirm the merge) or to abandon an unmerged branch on purpose; --force also force-removes a dirty worktree and force-deletes the branch. It defaults to the current worktree's branch, refuses to remove the main worktree, cds out of the worktree before removing it, falls back to git branch -D for the squash/rebase case, runs git fetch --prune, and prunes the empty root. It does not run git pull — pull main yourself afterward only once you have confirmed main is checked out and clean. WORKFLOW.md has the manual command sequence for when the script is unavailable.
Failure handling
- Worktree path already exists: pick a different branch/slug; never force-overwrite.
- Branch already exists remotely: use
git worktree add "$root/<slug>" <branch>(no-b) to check it out. - Dirty main repo when user wants a new worktree: ask to commit, stash, or proceed anyway — do not silently stash.
git worktree removefails with "is dirty": confirm with user before runninggit worktree remove --force.git branch -dsays "not fully merged" after a squash/rebase PR merge: confirm the PR is MERGED, then usegit branch -D— this is expected, not data loss.- Invoked from inside the worktree being removed:
cdto the main worktree first, or the shell ends up in a deleted directory.
Output
WORKTREE READY
==============
Action: CREATE | CLEANUP
Branch: <branch>
Path: <project>.worktrees/<slug>
Status: DONE | BLOCKED
Next:
- cd into the worktree path and open the editor there, or
- pull main yourself after a confirmed cleanup
For cleanup, only report DONE when the PR is confirmed MERGED (or --force was used deliberately); otherwise status is BLOCKED with the single reason the script reported.
References
- WORKFLOW.md - Detailed steps, project setup, cleanup, common mistakes
- scripts/ - setup-worktree.sh and cleanup-worktree.sh