Skip to content

Instantly share code, notes, and snippets.

@taxilian
Created April 4, 2026 02:46
Show Gist options
  • Select an option

  • Save taxilian/c2ad1d32c49c287cc8f4e08d90938a01 to your computer and use it in GitHub Desktop.

Select an option

Save taxilian/c2ad1d32c49c287cc8f4e08d90938a01 to your computer and use it in GitHub Desktop.
Agentic dev skill for git worktrees
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.

Git Worktrees for Parallel Development

This skill manages the full lifecycle of git worktree-based parallel development: creation, development with disciplined commits, rebasing, fast-forward-only merging, and cleanup.

Core principles

  1. Fast-forward only. The --ff-only merge strategy is mandatory. Rebase onto the parent branch before merging — never create merge commits.
  2. Intentional commits. Never git add . blindly. Stage related changes together, use git add -p for partial files, and write conventional commit messages that explain intent.
  3. Clean lifecycle. Always use git worktree remove for teardown. Verify the branch is merged before deleting it. Leave no stale worktrees behind.

Workflow overview

create worktree → develop with atomic commits → rebase onto parent → ff-only merge → cleanup

Phase 1: Creating a worktree

Determine the worktree location

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"

Ensure .worktrees/ is gitignored

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"
fi

If 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"

Identify the parent branch

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)
fi

Create the worktree

BRANCH_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).

Set up the worktree for development

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/null

Phase 2: Development — commit hygiene

This 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.

The commit workflow

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"

Commit message format

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.

What NEVER to do

  • Never git add . or git add -A without first reviewing git status and 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.

When a single file has changes for multiple commits

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.


Phase 3: Preparing to merge — rebase onto parent

Before merging back, the worktree branch must be rebased onto the latest parent branch so that a fast-forward merge is possible.

The rebase sequence

# 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"

Handling rebase conflicts

If conflicts arise during rebase:

  1. Examine the conflict: git diff shows the conflicting hunks. git show REBASE_HEAD --stat shows what the conflicting commit was doing.
  2. Resolve the conflict: Edit the files, then git add <resolved-files>.
  3. Continue: git rebase --continue
  4. 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).

Keeping long-running branches up to date

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"

Optional: interactive rebase to clean up history

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.


Phase 4: Merging back — fast-forward only

The merge sequence

# 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"

If the fast-forward merge fails

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.


Phase 5: Cleanup

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-run

Always 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).

Cleaning up the .worktrees directory

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.


Quick reference: recommended git configuration

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 rebase

The 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.


Edge cases and troubleshooting

Branch already checked out elsewhere

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.

Locked worktrees

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.

Submodules

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.

Shared hooks

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.

Stale worktree detection

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

Example: complete end-to-end session

# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment