Claude Cursor Skill

using-git-worktrees

Imported from alexei-led/cc-thingz/src/skills/using-git-worktrees.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alexei-led-cc-thingz-src_skills_using-git-worktrees-ce56bb4.zip · 7 KB
Part of alexei-led/cc-thingz — 91 skills

Install

skills CLI npx skills add https://github.com/alexei-led/cc-thingz/tree/master/src/skills/using-git-worktrees
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alexei-led-cc-thingz@llmmart
Git git clone https://github.com/alexei-led/cc-thingz.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alexei-led/cc-thingz collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Git Worktrees

A worktree gives parallel work its own folder and branch while the main worktree stays clean on the integration branch. Each project gets one sibling root, <project>.worktrees/, with one directory per branch named by its slug (/ → -, so feature/auth → feature-auth). Remove the worktree and branch after the PR merges.

A plain branch switch with no parallel work needs no worktree: check git status --short, then git switch <branch>. Trivial solo one-liners may also stay in the main worktree.

Create

Check git status --short and git worktree list first. Leave uncommitted changes where they are and never stash them silently; pass --allow-dirty only when the user has authorized isolating new work while keeping them.

scripts/setup-worktree.sh <branch> [--base <ref>] [--allow-dirty] [--setup] [--test]

The script runs git worktree add from the main worktree root. It checks out an existing local or origin branch instead of recreating it, and refuses an existing path, a branch checked out in another worktree, or a dirty tree without --allow-dirty. It reports base divergence from local refs without fetching.

  • --setup does a frozen install with the declared package manager and lockfile (uv for Python). Conflicting lockfiles or manager declarations stop it. A setup failure exits non-zero but still prints the path.
  • --test runs the detected baseline tests. Failures only warn.
  • A skipped setup or test is not a pass.

Clean up one worktree

scripts/cleanup-worktree.sh [branch]

It removes the worktree and deletes the branch only when gh confirms the PR is MERGED. It also refuses when the branch has commits past the merged PR head. Pass --force only after the user confirms the merge without gh, confirms those extra commits are throwaway, or abandons the branch; --force can remove a dirty worktree and force-delete the branch. For bulk or stale cleanup, use cleanup-git. Leave git pull out of cleanup; the user pulls the main worktree once it is clean on the integration branch.

For cases the scripts refuse or don't cover, read workflow.md.

Output

WORKTREE READY | WORKTREE REMOVED | BLOCKED
Branch: <branch>
Path: <project>.worktrees/<slug>
Next: cd <path>, or the script's refusal reason

Report a cleanup as done only when the PR is confirmed MERGED or --force was deliberate.

Files (cc-thingz)
  • .agentbundler
    • targets
      • claude.json 548 B
        {
          "frontmatterPatch": {
            "allowed-tools": [
              "Read",
              "AskUserQuestion",
              "Bash(git status *)",
              "Bash(git branch *)",
              "Bash(git worktree *)",
              "Bash(git rev-parse *)",
              "Bash(git symbolic-ref *)",
              "Bash(git fetch *)",
              "Bash(mkdir *)",
              "Bash(rmdir *)",
              "Bash(scripts/setup-worktree.sh *)",
              "Bash(scripts/cleanup-worktree.sh *)",
              "Bash(git switch *)",
              "Bash(git log *)",
              "Bash(git -C * status *)"
            ],
            "context": "fork",
            "user-invocable": true
          }
        }
        
  • references
    • workflow.md 1.4 KB
      # Worktree Edge Cases
      
      Cases the scripts refuse or leave to you.
      
      - Path exists: pick another branch name, or remove the leftover directory after the user confirms. Remove only paths under `<project>.worktrees/`.
      - Branch checked out in another worktree: work there, or pick another branch. If that worktree's directory is gone, `git worktree prune` clears the stale registration.
      - Base ref not found: fetch or pass `--base <ref>`.
      - Setup refused (no lockfile, conflicting lockfiles or manager): run the project's documented install command, and ask if none exists.
      - Cleanup when `gh` is missing or cannot see the PR: ask the user to confirm the merge and check `git -C <worktree> status --short`, since `--force` also discards dirty files; then run `scripts/cleanup-worktree.sh --force <branch>`.
      - Refused because the branch has commits past the merged PR head, or the PR head is not local: `git fetch` first; if still refused, show `git log <pr-head>..<branch>` and use `--force` only when the user says those commits are throwaway.
      - `git worktree remove` fails on a merged worktree because it is dirty: show `git -C <worktree> status --short`, and use `--force` only when the user says those changes are throwaway.
      - `git branch -d` refuses after a squash or rebase merge: that is expected; use `-D` once the PR is confirmed merged.
      - The shell was inside the removed worktree: `cd` to the main worktree before running further commands.
      
  • scripts
    • cleanup-worktree.sh 3.3 KB
      #!/usr/bin/env bash
      # Remove a worktree and delete its branch after its PR has merged.
      
      set -euo pipefail
      
      FORCE=0
      BRANCH=""
      
      usage() {
      	cat <<'EOF'
      Usage: cleanup-worktree.sh [--force] [branch-name]
      
      Default is strict: if gh cannot confirm the PR is MERGED, nothing is touched.
      Use --force only after confirming the merge yourself or deciding to abandon the
      branch. --force may remove a dirty worktree and force-delete the branch.
      EOF
      }
      
      while [ "$#" -gt 0 ]; do
      	case "$1" in
      	--force)
      		FORCE=1
      		shift
      		;;
      	-h | --help)
      		usage
      		exit 0
      		;;
      	-*)
      		echo "Error: unknown option: $1" >&2
      		usage >&2
      		exit 2
      		;;
      	*)
      		[ -z "$BRANCH" ] || {
      			echo "Error: branch name already set: $BRANCH" >&2
      			exit 2
      		}
      		BRANCH=$1
      		shift
      		;;
      	esac
      done
      
      porcelain=$(git worktree list --porcelain 2>/dev/null) || {
      	echo "Error: Not in a git repository"
      	exit 1
      }
      
      MAIN_WT=$(awk '/^worktree /{print $2; exit}' <<<"$porcelain")
      
      if [ -z "$BRANCH" ]; then
      	BRANCH=$(git rev-parse --abbrev-ref HEAD)
      	[ "$BRANCH" = "HEAD" ] && {
      		echo "Error: detached HEAD — pass a branch name explicitly"
      		exit 1
      	}
      fi
      
      WT=$(awk -v b="refs/heads/$BRANCH" '
      	/^worktree /{p=$2} $0=="branch "b{print p; exit}' <<<"$porcelain")
      
      if [ -z "$WT" ]; then
      	echo "Error: no worktree checked out on branch '$BRANCH'"
      	git worktree list
      	exit 1
      fi
      if [ "$WT" = "$MAIN_WT" ]; then
      	echo "Error: '$BRANCH' is checked out in the MAIN worktree ($MAIN_WT) — refusing to remove it"
      	exit 1
      fi
      
      GH=0
      STATE=""
      PR_HEAD=""
      if command -v gh >/dev/null 2>&1; then
      	GH=1
      	pr_info=$(gh pr view "$BRANCH" --json state,headRefOid --template '{{printf "%s\t%s" .state .headRefOid}}' 2>/dev/null || echo "")
      	if [ -n "$pr_info" ]; then
      		IFS=$'\t' read -r STATE PR_HEAD <<<"$pr_info"
      	fi
      fi
      [ "$GH" = 1 ] && pr_desc="PR for '$BRANCH' is ${STATE:-not found}" || pr_desc="gh not installed, PR state for '$BRANCH' unknown"
      
      if [ "$STATE" = MERGED ]; then
      	if [ "$FORCE" != 1 ]; then
      		if ! git cat-file -e "${PR_HEAD}^{commit}" 2>/dev/null || ! git merge-base --is-ancestor "$BRANCH" "$PR_HEAD" 2>/dev/null; then
      			echo "Refusing: '$BRANCH' has commits beyond the merged PR, or the PR head is not local (try git fetch)."
      			echo "Nothing was changed. Re-run with --force once confirmed, or to abandon the branch."
      			exit 1
      		fi
      	fi
      	echo "PR for '$BRANCH' is MERGED — cleaning up."
      elif [ "$FORCE" = 1 ]; then
      	echo "$pr_desc — proceeding due to --force. This may force-remove dirty files and force-delete the branch."
      else
      	echo "Refusing: $pr_desc."
      	echo "Nothing was changed. Re-run with --force once the PR has merged or to abandon the branch."
      	exit 1
      fi
      
      cd "$MAIN_WT"
      
      echo "Removing worktree: $WT"
      if [ "$FORCE" = 1 ]; then
      	git worktree remove --force "$WT"
      else
      	git worktree remove "$WT" || {
      		echo "Error: 'git worktree remove' failed (likely dirty). Re-run with --force after confirming."
      		exit 1
      	}
      fi
      
      echo "Deleting branch: $BRANCH"
      git branch -d "$BRANCH" 2>/dev/null || {
      	echo "  -d refused after confirmed merge/force — deleting with -D."
      	git branch -D "$BRANCH"
      }
      
      git fetch --prune --quiet || true
      
      ROOT=$(dirname "$WT")
      rmdir "$ROOT" 2>/dev/null && echo "Removed empty worktree root: $ROOT" || true
      
      echo ""
      echo "Done. Worktree removed and branch handled for '$BRANCH'."
      echo "Main worktree: $MAIN_WT (pull it yourself once it is on the integration branch and clean)."
      
    • setup-worktree.sh 8.2 KB
      #!/usr/bin/env bash
      # Create a git worktree under <project>.worktrees/<branch-slug>.
      
      set -euo pipefail
      
      ALLOW_DIRTY=false
      RUN_SETUP=false
      RUN_TESTS=false
      BASE_REF=""
      BRANCH_NAME=""
      
      usage() {
      	cat <<'EOF'
      Usage: setup-worktree.sh [--base <ref>] [--allow-dirty] [--setup] [--test] <branch-name>
      
      Creates <project>.worktrees/<branch-slug> from the main worktree root.
      Refuses dirty current state unless --allow-dirty is passed after user approval.
      Existing local and origin branches are checked out instead of recreated.
      
      Options:
        --base <ref>    Base ref for new branches (default: remote default, local main/master/etc, then HEAD)
        --allow-dirty   Proceed even when the current worktree has uncommitted changes
        --setup         Run detected dependency setup after creation
        --test          Run detected baseline test command after creation
      EOF
      }
      
      while [ "$#" -gt 0 ]; do
      	case "$1" in
      	--base)
      		shift
      		[ "$#" -gt 0 ] || {
      			echo "Error: --base requires a ref" >&2
      			exit 2
      		}
      		BASE_REF=$1
      		shift
      		;;
      	--base=*)
      		BASE_REF=${1#--base=}
      		shift
      		;;
      	--allow-dirty)
      		ALLOW_DIRTY=true
      		shift
      		;;
      	--setup)
      		RUN_SETUP=true
      		shift
      		;;
      	--test)
      		RUN_TESTS=true
      		shift
      		;;
      	-h | --help)
      		usage
      		exit 0
      		;;
      	-*)
      		echo "Error: unknown option: $1" >&2
      		usage >&2
      		exit 2
      		;;
      	*)
      		[ -z "$BRANCH_NAME" ] || {
      			echo "Error: branch name already set: $BRANCH_NAME" >&2
      			exit 2
      		}
      		BRANCH_NAME=$1
      		shift
      		;;
      	esac
      done
      
      [ -n "$BRANCH_NAME" ] || {
      	usage >&2
      	exit 2
      }
      
      git check-ref-format --branch "$BRANCH_NAME" >/dev/null 2>&1 || {
      	echo "Error: invalid branch name: $BRANCH_NAME" >&2
      	exit 2
      }
      
      REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || {
      	echo "Error: not in a git repository" >&2
      	exit 1
      }
      
      if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ] && ! $ALLOW_DIRTY; then
      	echo "Error: current worktree is dirty; commit, stash, or re-run with --allow-dirty after approval" >&2
      	exit 1
      fi
      
      PORCELAIN=$(git -C "$REPO_ROOT" worktree list --porcelain)
      MAIN_WT=$(awk '/^worktree /{sub(/^worktree /, ""); print; exit}' <<<"$PORCELAIN")
      PROJECT=$(basename "$MAIN_WT")
      ROOT="$(dirname "$MAIN_WT")/$PROJECT.worktrees"
      SLUG=$(printf '%s' "$BRANCH_NAME" | tr '/' '-')
      WORKTREE_PATH="$ROOT/$SLUG"
      
      if [ -e "$WORKTREE_PATH" ]; then
      	echo "Error: worktree path already exists: $WORKTREE_PATH" >&2
      	exit 1
      fi
      
      if grep -Fxq "branch refs/heads/$BRANCH_NAME" <<<"$PORCELAIN"; then
      	echo "Error: branch is already checked out in another worktree: $BRANCH_NAME" >&2
      	exit 1
      fi
      
      has_ref() {
      	git -C "$REPO_ROOT" rev-parse --verify --quiet "$1^{commit}" >/dev/null
      }
      
      detect_base() {
      	local remote_head branch candidate
      	remote_head=$(git -C "$REPO_ROOT" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
      	if [ -n "$remote_head" ]; then
      		branch=${remote_head#origin/}
      		if has_ref "$branch"; then
      			printf '%s\n' "$branch"
      			return 0
      		fi
      		if has_ref "$remote_head"; then
      			printf '%s\n' "$remote_head"
      			return 0
      		fi
      	fi
      
      	for candidate in main master trunk develop dev; do
      		if has_ref "$candidate"; then
      			printf '%s\n' "$candidate"
      			return 0
      		fi
      	done
      
      	printf 'HEAD\n'
      }
      
      if [ -z "$BASE_REF" ]; then
      	BASE_REF=$(detect_base)
      fi
      
      has_ref "$BASE_REF" || {
      	echo "Error: base ref not found: $BASE_REF" >&2
      	exit 1
      }
      
      UPSTREAM=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref --symbolic-full-name "$BASE_REF@{upstream}" 2>/dev/null || true)
      if [ -n "$UPSTREAM" ]; then
      	DIVERGENCE=$(git -C "$REPO_ROOT" rev-list --left-right --count "$BASE_REF...$UPSTREAM")
      	read -r AHEAD BEHIND <<<"$DIVERGENCE"
      	echo "Base $BASE_REF vs $UPSTREAM: ahead $AHEAD, behind $BEHIND (local tracking data; not fetched)." >&2
      fi
      
      mkdir -p "$ROOT"
      
      if git -C "$REPO_ROOT" show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
      	echo "Checking out existing branch at $WORKTREE_PATH..." >&2
      	git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
      elif git -C "$REPO_ROOT" show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
      	echo "Checking out origin/$BRANCH_NAME at $WORKTREE_PATH..." >&2
      	git -C "$REPO_ROOT" worktree add --track -b "$BRANCH_NAME" "$WORKTREE_PATH" "origin/$BRANCH_NAME"
      else
      	echo "Creating worktree at $WORKTREE_PATH from $BASE_REF..." >&2
      	git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" -b "$BRANCH_NAME" "$BASE_REF"
      fi
      
      detect_node_manager() {
      	local declared manager="" candidate count=0
      	declared=$(node -e 'const p=require(process.cwd()+"/package.json"); process.stdout.write(p.packageManager || "")') || return 1
      	for candidate in npm pnpm yarn bun; do
      		case "$candidate" in
      		npm) [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ] || continue ;;
      		pnpm) [ -f pnpm-lock.yaml ] || continue ;;
      		yarn) [ -f yarn.lock ] || continue ;;
      		bun) [ -f bun.lock ] || [ -f bun.lockb ] || continue ;;
      		esac
      		manager=$candidate
      		count=$((count + 1))
      	done
      	if [ "$count" -gt 1 ]; then
      		echo "Error: conflicting Node lockfiles; choose the project package manager manually." >&2
      		return 1
      	fi
      	if [ -n "$declared" ]; then
      		candidate=${declared%%@*}
      		case "$candidate" in npm | pnpm | yarn | bun) ;; *)
      			echo "Error: unsupported packageManager: $declared" >&2
      			return 1
      			;;
      		esac
      		if [ -n "$manager" ] && [ "$manager" != "$candidate" ]; then
      			echo "Error: packageManager conflicts with lockfile." >&2
      			return 1
      		fi
      		manager=$candidate
      	fi
      	if [ -z "$manager" ]; then
      		echo "Error: no packageManager or lockfile; dependency commands need manual selection." >&2
      		return 1
      	fi
      	printf '%s\n' "$manager"
      }
      
      run_node_setup() {
      	local manager version
      	manager=$(detect_node_manager) || return 1
      	if [ ! -f package-lock.json ] && [ ! -f npm-shrinkwrap.json ] && [ ! -f pnpm-lock.yaml ] && [ ! -f yarn.lock ] && [ ! -f bun.lock ] && [ ! -f bun.lockb ]; then
      		echo "Error: frozen setup requires a committed lockfile." >&2
      		return 1
      	fi
      	case "$manager" in
      	npm) npm ci ;;
      	pnpm) pnpm install --frozen-lockfile ;;
      	bun) bun install --frozen-lockfile ;;
      	yarn)
      		version=$(yarn --version) || return 1
      		if [[ "$version" == 1.* ]]; then yarn install --frozen-lockfile; else yarn install --immutable; fi
      		;;
      	esac
      }
      
      run_setup() {
      	cd "$WORKTREE_PATH"
      	if [ -f package.json ]; then
      		run_node_setup
      	elif [ -f go.mod ]; then
      		go mod download
      	elif [ -f pyproject.toml ]; then
      		uv sync --locked
      	elif [ -f requirements.txt ]; then
      		uv venv && uv pip sync --python .venv/bin/python requirements.txt
      	elif [ -f Cargo.toml ]; then
      		cargo build
      	elif [ -x ./gradlew ]; then
      		./gradlew testClasses
      	elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then
      		gradle testClasses
      	elif [ -x ./mvnw ]; then
      		./mvnw -q -DskipTests compile
      	elif [ -f pom.xml ]; then
      		mvn -q -DskipTests compile
      	else
      		echo "No known dependency setup detected; skipped." >&2
      	fi
      }
      
      run_tests() {
      	cd "$WORKTREE_PATH"
      	if [ -f Makefile ]; then
      		make test
      	elif [ -f package.json ]; then
      		local manager
      		manager=$(detect_node_manager) || return 1
      		"$manager" run test
      	elif [ -f go.mod ]; then
      		go test ./...
      	elif [ -f pyproject.toml ]; then
      		uv run --locked --extra test python -m pytest
      	elif [ -f requirements.txt ]; then
      		uv run --no-project --python .venv/bin/python python -m pytest
      	elif [ -f Cargo.toml ]; then
      		cargo test
      	elif [ -x ./gradlew ]; then
      		./gradlew test
      	elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then
      		gradle test
      	elif [ -x ./mvnw ]; then
      		./mvnw -q test
      	elif [ -f pom.xml ]; then
      		mvn -q test
      	else
      		echo "No known baseline test command detected; skipped." >&2
      	fi
      }
      
      SETUP_FAILED=false
      if $RUN_SETUP; then
      	echo "Running dependency setup..." >&2
      	run_setup || {
      		SETUP_FAILED=true
      		echo "warning: dependency setup failed" >&2
      	}
      fi
      
      if $RUN_TESTS; then
      	echo "Running baseline tests..." >&2
      	run_tests || echo "warning: baseline tests failed" >&2
      fi
      
      cat <<EOF
      WORKTREE READY
      ==============
      Branch: $BRANCH_NAME
      Path: $WORKTREE_PATH
      Base: $BASE_REF
      
      Next:
      - cd "$WORKTREE_PATH"
      - clean up after PR merge with: scripts/cleanup-worktree.sh "$BRANCH_NAME"
      EOF
      
      # Dependency setup failure leaves the worktree unusable for further work
      # (missing deps break everything run in it), so the script still reports
      # READY with the path an agent needs to fix it manually, but exits non-zero
      # to signal the operation needs attention. A failing baseline test suite is
      # just information (pre-existing/flaky failures are common) and does not
      # block using the worktree, so it only warns and exits 0.
      if $SETUP_FAILED; then
      	exit 1
      fi
      
  • SKILL.md 2.8 KB
    ---
    description:
      Creates and removes isolated git worktrees for parallel development.
      Use when starting feature work needing isolation, working on multiple branches
      simultaneously, or removing one specific worktree and its branch after its PR
      merges. NOT for simple branch switching, sweeping multiple stale worktrees or
      merged branches at once (use cleanup-git), or git hook/config setup (use
      configuring-git-hygiene).
    name: using-git-worktrees
    ---
    
    # Git Worktrees
    
    A worktree gives parallel work its own folder and branch while the main worktree stays clean on the integration branch. Each project gets one sibling root, `<project>.worktrees/`, with one directory per branch named by its slug (`/` → `-`, so `feature/auth` → `feature-auth`). Remove the worktree and branch after the PR merges.
    
    A plain branch switch with no parallel work needs no worktree: check `git status --short`, then `git switch <branch>`. Trivial solo one-liners may also stay in the main worktree.
    
    ## Create
    
    Check `git status --short` and `git worktree list` first. Leave uncommitted changes where they are and never stash them silently; pass `--allow-dirty` only when the user has authorized isolating new work while keeping them.
    
    ```bash
    scripts/setup-worktree.sh <branch> [--base <ref>] [--allow-dirty] [--setup] [--test]
    ```
    
    The script runs `git worktree add` from the main worktree root. It checks out an existing local or `origin` branch instead of recreating it, and refuses an existing path, a branch checked out in another worktree, or a dirty tree without `--allow-dirty`. It reports base divergence from local refs without fetching.
    
    - `--setup` does a frozen install with the declared package manager and lockfile (uv for Python). Conflicting lockfiles or manager declarations stop it. A setup failure exits non-zero but still prints the path.
    - `--test` runs the detected baseline tests. Failures only warn.
    - A skipped setup or test is not a pass.
    
    ## Clean up one worktree
    
    ```bash
    scripts/cleanup-worktree.sh [branch]
    ```
    
    It removes the worktree and deletes the branch only when `gh` confirms the PR is `MERGED`. It also refuses when the branch has commits past the merged PR head. Pass `--force` only after the user confirms the merge without `gh`, confirms those extra commits are throwaway, or abandons the branch; `--force` can remove a dirty worktree and force-delete the branch. For bulk or stale cleanup, use cleanup-git. Leave `git pull` out of cleanup; the user pulls the main worktree once it is clean on the integration branch.
    
    For cases the scripts refuse or don't cover, read [workflow.md](references/workflow.md).
    
    ## Output
    
    ```text
    WORKTREE READY | WORKTREE REMOVED | BLOCKED
    Branch: <branch>
    Path: <project>.worktrees/<slug>
    Next: cd <path>, or the script's refusal reason
    ```
    
    Report a cleanup as done only when the PR is confirmed `MERGED` or `--force` was deliberate.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related