| name | git-worktrees |
|---|---|
| description | Parallel development using git worktrees. Use this skill whenever the user asks to work on a feature, bugfix, or task in isolation using a worktree, or when asked to set up parallel development, create a worktree, merge a worktree branch back, or clean up after worktree work. Also trigger when asked to work on something "in parallel", "on a separate branch without switching", or "without touching my current working tree". Trigger on mentions of "worktree", "parallel branch", "isolated branch work", or any request to develop a feature while keeping the main checkout clean. This skill enforces --ff-only merge policy, intentional commit hygiene, and proper worktree lifecycle management. |
This skill manages the full lifecycle of git worktree-based parallel development: creation, development with disciplined commits, rebasing, fast-forward-only merging, and cleanup.
- Fast-forward only. The
--ff-onlymerge strategy is mandatory. Rebase onto the parent branch before merging — never create merge commits. - Intentional commits. Never
git add .blindly. Stage related changes together, usegit add -pfor partial files, and write conventional commit messages that explain intent. - Clean lifecycle. Always use
git worktree removefor teardown. Verify the branch is merged before deleting it. Leave no stale worktrees behind.
create worktree → develop with atomic commits → rebase onto parent → ff-only merge → cleanup
Default to $PROJECT_ROOT/.worktrees/<branch-name>/ unless the user specifies
a different location. $PROJECT_ROOT is the root of the git repository (the
directory containing .git/).
# Determine project root
PROJECT_ROOT=$(git rev-parse --show-toplevel)
WORKTREE_DIR="$PROJECT_ROOT/.worktrees"Before creating the first worktree, check that .worktrees/ is in
.gitignore. If not, add it. Be careful not to duplicate an existing entry
or disrupt the file's structure.
# Check and add to .gitignore if missing
GITIGNORE="$PROJECT_ROOT/.gitignore"
if [ ! -f "$GITIGNORE" ] || ! grep -qxF '.worktrees/' "$GITIGNORE"; then
# Add with a blank line separator if the file doesn't end with one
if [ -f "$GITIGNORE" ] && [ -s "$GITIGNORE" ] && [ "$(tail -c1 "$GITIGNORE")" != "" ]; then
echo '' >> "$GITIGNORE"
fi
echo '# Git worktrees for parallel development' >> "$GITIGNORE"
echo '.worktrees/' >> "$GITIGNORE"
fiIf the .gitignore change is uncommitted, commit it as a standalone commit
before creating the worktree:
git add "$GITIGNORE"
git commit -m "chore: add .worktrees/ to .gitignore"The "parent branch" is the branch currently checked out in the main worktree.
Do not assume it is main or master — detect it:
PARENT_BRANCH=$(git branch --show-current)
# If detached HEAD, fall back to checking for main/master
if [ -z "$PARENT_BRANCH" ]; then
PARENT_BRANCH=$(git rev-parse --verify main 2>/dev/null && echo main || echo master)
fiBRANCH_NAME="feature/xyz" # or bugfix/, chore/, etc.
WORKTREE_PATH="$WORKTREE_DIR/$BRANCH_NAME"
# Fetch latest from remote
git fetch origin "$PARENT_BRANCH"
# Create worktree with a new branch based on the latest remote parent
mkdir -p "$(dirname "$WORKTREE_PATH")"
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" "origin/$PARENT_BRANCH"Two worktrees cannot have the same branch checked out. If the branch already
exists, use git worktree add "$WORKTREE_PATH" "$BRANCH_NAME" (without -b).
Each worktree is a clean checkout — node_modules, .env, and build
artifacts are NOT shared. After creation:
cd "$WORKTREE_PATH"
# Install dependencies
if [ -f pnpm-lock.yaml ]; then
pnpm install
elif [ -f package-lock.json ]; then
npm install
elif [ -f yarn.lock ]; then
yarn install
fi
# Copy environment files if they exist in the main worktree
[ -f "$PROJECT_ROOT/.env" ] && cp -n "$PROJECT_ROOT/.env" .env 2>/dev/null
[ -f "$PROJECT_ROOT/.env.local" ] && cp -n "$PROJECT_ROOT/.env.local" .env.local 2>/dev/nullThis is the most critical section. Every commit must be intentional, atomic, and well-described. The goal is a clean history that's easy to review, revert, and bisect.
For every logical unit of change, follow this exact sequence:
# 1. Review all changes
git status
git diff --stat
# 2. Stage ONLY files belonging to ONE logical change
git add src/auth/oauth.ts src/auth/oauth.test.ts
# 3. If a file contains changes for multiple concerns, use patch mode
git add -p src/shared/config.ts
# Review each hunk: y=stage, n=skip, s=split, e=edit
# 4. Verify what's staged matches your intent
git diff --staged --stat
git diff --staged
# 5. Run relevant checks before committing
npm test # or the relevant test command
npm run lint # if available
# 6. Commit with a conventional message
git commit -m "feat(auth): add OAuth2 token refresh
Implement automatic token refresh using refresh_token grant type.
Tokens are cached with TTL matching the refresh window.
Closes #456"Use Conventional Commits. The subject line must:
- Start with a type:
feat,fix,chore,refactor,test,docs,style,perf,ci,build - Optionally include a scope in parentheses:
feat(auth): - Use imperative mood: "add" not "added" or "adds"
- Stay under 72 characters
- Not end with a period
The body (separated by a blank line) explains why, not just what. Include issue references if applicable.
- Never
git add .orgit add -Awithout first reviewinggit statusand confirming every changed file belongs in the same logical commit. - Never commit generated files like
node_modules/,dist/,build/,coverage/,*.tsbuildinfo, or*.log. - Never mix concerns in a single commit. A rename and a logic change are two commits. A dependency update and a feature are two commits.
- Never commit with a vague message like "updates", "fix", "WIP", or "changes". Every commit message must describe the specific change.
Use git add -p (patch mode) to stage individual hunks. This is essential
when you've modified a shared utility file for two different features. Stage
the hunks for feature A, commit, then stage the remaining hunks for feature B
and commit separately.
If hunks are too interleaved for git add -p to split cleanly, use
git add -p with the e (edit) option to manually edit the diff and select
exactly which lines to stage.
Before merging back, the worktree branch must be rebased onto the latest parent branch so that a fast-forward merge is possible.
# From the worktree directory
cd "$WORKTREE_PATH"
# 1. Ensure all work is committed (never rebase with uncommitted changes)
git status # must be clean
# 2. Fetch the latest parent
git fetch origin "$PARENT_BRANCH"
# 3. Rebase onto the remote-tracking parent branch
# Use origin/$PARENT_BRANCH — not the local branch — to avoid needing
# to switch worktrees just to pull
git rebase "origin/$PARENT_BRANCH"If conflicts arise during rebase:
- Examine the conflict:
git diffshows the conflicting hunks.git show REBASE_HEAD --statshows what the conflicting commit was doing. - Resolve the conflict: Edit the files, then
git add <resolved-files>. - Continue:
git rebase --continue - If the conflict is complex or unclear, abort and report to the user:
git rebase --abort— this is always safe and returns to the pre-rebase state.
Never use git rebase --skip silently — it drops a commit entirely. Only use
it if you've confirmed the commit is genuinely redundant (its changes are
already in the parent branch).
For branches that span multiple days, rebase frequently — ideally whenever the parent branch receives new commits. Small incremental rebases produce smaller, easier conflicts than one massive rebase at merge time.
# Quick sync pattern (do this regularly)
git fetch origin "$PARENT_BRANCH"
git rebase "origin/$PARENT_BRANCH"Before the final merge, consider squashing or reorganizing commits:
# Interactively rebase the commits unique to this branch
git rebase -i "origin/$PARENT_BRANCH"This allows squashing fixup commits, reordering for logical flow, and rewording messages. The result should be a series of clean, atomic commits that tell a coherent story.
# 1. Return to the main worktree
cd "$PROJECT_ROOT"
# 2. Ensure local parent branch is up to date
git pull --ff-only origin "$PARENT_BRANCH"
# 3. Fast-forward merge the feature branch
git merge --ff-only "$BRANCH_NAME"
# 4. Push
git push origin "$PARENT_BRANCH"git merge --ff-only will fail if the parent branch has received new commits
since the rebase. The fix is always the same:
# Go back to the worktree and rebase again
cd "$WORKTREE_PATH"
git fetch origin "$PARENT_BRANCH"
git rebase "origin/$PARENT_BRANCH"
# Return to main and retry
cd "$PROJECT_ROOT"
git pull --ff-only origin "$PARENT_BRANCH"
git merge --ff-only "$BRANCH_NAME"Never fall back to a non-fast-forward merge. If --ff-only fails, rebase and
retry. This is non-negotiable.
After a successful merge, clean up the worktree and branch completely.
# 1. Verify the branch is fully merged
git merge-base --is-ancestor "$BRANCH_NAME" "$PARENT_BRANCH" && echo "Merged" || echo "NOT merged — do not clean up"
# 2. Remove the worktree (includes safety checks for uncommitted changes)
git worktree remove "$WORKTREE_PATH"
# 3. Delete the local branch (safe delete — fails if not fully merged)
git branch -d "$BRANCH_NAME"
# 4. Delete the remote branch if it was pushed
git push origin --delete "$BRANCH_NAME" 2>/dev/null
# 5. Verify no stale worktrees remain
git worktree list
git worktree prune --dry-runAlways use git worktree remove — never rm -rf the worktree directory. The
remove command cleans up git's internal metadata. If a directory was already
deleted manually, run git worktree prune to clear stale entries.
If git worktree remove refuses because of uncommitted changes, either commit
them first or use git worktree remove --force (only after confirming the
changes are truly disposable).
After removing all worktrees, if .worktrees/ is empty, leave it in place —
the .gitignore entry still references it and it causes no harm. Git will not
track it.
These settings make the worktree workflow smoother. Set them if not already configured:
git config merge.ff only # enforce ff-only for all merges
git config pull.rebase true # rebase on pull instead of merge
git config rerere.enabled true # remember conflict resolutions
git config rebase.autoStash true # auto-stash dirty files during rebaseThe rerere setting is especially valuable — it records how conflicts are
resolved and automatically reapplies the same resolution if the same conflict
appears again. There's no downside to enabling it.
If git worktree add fails because the branch is already checked out in
another worktree, use git worktree list to find where. Either remove the
existing worktree or choose a different branch name.
A worktree can become locked (via git worktree lock or --lock flag).
Locked worktrees resist removal. Unlock with git worktree unlock <path>
before removing. Never lock worktrees unless they're on removable storage.
Git's own documentation warns that submodule support in worktrees is
incomplete. If the project uses submodules, run
git submodule update --init --recursive in each new worktree. Be aware that
worktrees with submodules cannot be moved and may require --force to remove.
Git hooks are shared across all worktrees via the common hooks directory.
Hooks that reference $GIT_DIR may break in linked worktrees — they should
use git rev-parse --git-common-dir instead for shared resources.
If a worktree directory was deleted without git worktree remove, the branch
remains marked as checked out. Detect and fix with:
git worktree list --verbose # shows "prunable" annotation
git worktree prune # cleans stale metadata# Setup
PROJECT_ROOT=$(git rev-parse --show-toplevel)
PARENT_BRANCH=$(git branch --show-current)
BRANCH_NAME="feat/user-avatars"
# Ensure .worktrees is ignored (first time only)
grep -qxF '.worktrees/' "$PROJECT_ROOT/.gitignore" 2>/dev/null || {
echo -e '\n# Git worktrees\n.worktrees/' >> "$PROJECT_ROOT/.gitignore"
git add .gitignore && git commit -m "chore: add .worktrees/ to .gitignore"
}
# Create
git fetch origin "$PARENT_BRANCH"
git worktree add -b "$BRANCH_NAME" "$PROJECT_ROOT/.worktrees/$BRANCH_NAME" "origin/$PARENT_BRANCH"
cd "$PROJECT_ROOT/.worktrees/$BRANCH_NAME"
npm install
# Develop (atomic commits)
git add src/components/Avatar.vue src/components/Avatar.test.ts
git diff --staged --stat
git commit -m "feat(ui): add Avatar component with image fallback"
git add src/api/upload.ts
git diff --staged --stat
git commit -m "feat(api): add avatar upload endpoint"
# Rebase and merge
git fetch origin "$PARENT_BRANCH"
git rebase "origin/$PARENT_BRANCH"
cd "$PROJECT_ROOT"
git pull --ff-only origin "$PARENT_BRANCH"
git merge --ff-only "$BRANCH_NAME"
git push origin "$PARENT_BRANCH"
# Cleanup
git worktree remove "$PROJECT_ROOT/.worktrees/$BRANCH_NAME"
git branch -d "$BRANCH_NAME"
git push origin --delete "$BRANCH_NAME" 2>/dev/null