Claude Skill

ia-git-worktree

Manage Git worktrees for isolated parallel development. Use when creating, listing, switching, or cleaning up git worktrees, or when needing isolated branches for concurrent reviews or feature work.

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

Full trust report

Download iliaal-whetstone-plugins_whetstone_skills_ia-git-worktree-0611f65.zip · 15 KB
Part of iliaal/whetstone — 58 skills

Install

skills CLI npx skills add https://github.com/iliaal/whetstone/tree/master/plugins/whetstone/skills/ia-git-worktree
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install iliaal-whetstone@llmmart
Git git clone https://github.com/iliaal/whetstone.git

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

Skill manifest

Git worktree manager

GATE: If the task runs inside an existing worktree (a worktree path is given and no create/remove/switch is requested), none of the creation flow applies: work in place and skip this skill. To check: git rev-parse --show-toplevel appears as a linked entry in git worktree list.

Working rules

  • Work in place when given an existing worktree without a creation/removal request.
  • Keep one writer per worktree and preserve other sessions' branches, files, and staged work.
  • Verify dependencies resolve first-party code from the intended worktree.
  • Keep merge, push, and cleanup within user authorization; report the exact exercised checkout and resulting commit.

Always use the manager script

Never call git worktree add directly; always use the worktree-manager.sh script.

The script handles critical setup that raw git commands don't:

  1. Copies .env, .env.local, .env.test, etc. from main repo
  2. Ensures .worktrees is in .gitignore
  3. Creates consistent directory structure
  4. After creation, install dependencies if detected: package.json → npm install, composer.json → composer install, pyproject.toml → pip install -e ., go.mod → go mod download

All commands use: bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh <command>. If CLAUDE_PLUGIN_ROOT is unset (non-Claude-Code harness), resolve the script relative to this skill's own directory.

Before creating worktrees, export a unique WORKTREE_SESSION_ID and retain that same value for this session's later manager calls. For example, export WORKTREE_SESSION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')". The manager records ownership in each new worktree's Git metadata. Creation without a session ID remains available, but manager cleanup then refuses that tree; never adopt a previous session's ID to bypass ownership.

The manager script fetches origin/<base> fresh and branches from it; it never checks out <base> in the caller's working tree. If the fetch fails (offline, no remote), it falls back to the local <base> ref. Details: troubleshooting.md.

Commands

Command Description Example
create <branch> [from] Create worktree + branch (default: from main) ...worktree-manager.sh create feature-login
list / ls List all worktrees with status ...worktree-manager.sh list
switch <name> / go Print the registered worktree's absolute path; the caller applies it as workdir target=$(...worktree-manager.sh switch feature-login)
copy-env <name> Copy .env files to existing worktree ...worktree-manager.sh copy-env feature-login
cleanup <name> [...] / clean Confirm removal of named, session-owned, clean worktrees ...worktree-manager.sh cleanup feature-login

Run commands with env -C "$target" <command> or the harness workdir argument. A child script cannot change the caller's working directory. Listing and resolving names work from the main checkout, linked checkouts, and their subdirectories.

Cleanup refuses the current checkout, another session's tree, and tracked, untracked, or ignored files. Auto-copied .env files and installed dependencies therefore require explicit user-managed disposition before cleanup. Confirm no process uses the named trees; the manager cannot detect every external reader. Git's normal removal safeguards remain enabled, including locked-tree refusal. Do not force deletion or suppress failures to finish cleanup.

Safety Verification

Before creating a worktree, verify the worktree directory is gitignored:

# Verify .worktrees is ignored (should output ".worktrees")
git check-ignore .worktrees || echo "WARNING: .worktrees not in .gitignore"

If not ignored, add it to .gitignore before proceeding.

After creating a worktree, run the project's test suite (or the fastest relevant subset if the full suite exceeds a few minutes) to establish a clean baseline. Catch pre-existing failures in the worktree before starting new work, not mid-implementation.

Ownership

  • One writer per worktree. Treat every git worktree list entry not created in the current session as read-only; a tree left from a previous round is not the current session's either. Reuse is most tempting exactly where it is most dangerous: an existing tree already has dependencies and env wired up, and another session may be running a suite in it.
  • Do not mutate a tree while the current session's suite runs there. Test runners load source files as they reach them, so a mid-run edit produces a mass-failure result that looks exactly like a real regression.
  • A failure burst that contradicts a claim is a harness hypothesis, not a conclusion. Do not record or report the self-inflicted attribution until a re-run on a tree just asserted clean (git status --short empty) has returned.
  • When a mutation is unavoidable, assert the restore (grep the token back to its original count, plus git status --short) rather than trusting git checkout --.
  • One checkout has one index, so staging explicit paths does not scope a commit: git add <mine> && git commit also commits whatever a peer staged, under the current session's message. The protection is a pathspec on the commit itself (git commit -- <paths>), which takes those paths from the working tree and ignores the index; new files still need git add. It constrains that commit, not a peer's, so the residual control is latency between writing and committing. Read git show --stat HEAD afterwards and confirm only the intended files are there.
  • git -C <repo> push <remote> HEAD:<branch> resolves HEAD in that repo, not in the worktree that was edited. Edits made in a linked worktree and pushed with -C at the main checkout publish the main checkout's commit onto the feature branch, and --force-with-lease does not catch it because the lease checks the branch's old value, not what HEAD names. Never spell HEAD: in a -C push; resolve the SHA in the worktree and push it explicitly, then confirm with git ls-remote. Two branches "updated" to one SHA, or a pushed subject unrelated to the work, is the tell.
  • Linked worktrees share one stash stack. git stash writes to the common git directory, so a red/green cycle in one worktree can pop and drop a stash another worktree pushed in between. Never stash for red/green here: git diff > /tmp/red.patch, git checkout -- <files> (worktree-local) for the red run, git apply /tmp/red.patch for green. A dropped stash is still recoverable while its commit survives: git stash store -m <message> <sha> re-registers the SHA that Dropped refs/stash@{0} (<sha>) printed.
  • A worktree's HEAD is shared mutable state, so answer branch questions from refs. Any other session can check something else out there, which makes git -C <worktree> rev-parse HEAD describe a different branch and report a correct push as a mismatch. Refs are shared across every worktree: ask any one of them about the branch by name (rev-parse <branch>, rev-list --count origin/<branch>..<branch>, reflog <branch>).

Use env -C <worktree> <cmd> for every command, never cd. A shell's cwd persists across calls, so one cd <repo-root> for an unrelated reason silently relocates every later command: probe files get written into the shared main tree and run against its bytes, and the tidy-up reflex git checkout -- <path> becomes a write aimed at the wrong tree. The git -C habit does not generalize: interpreters, test runners, linters, and a heredoc cat > all take the cwd. Have any probe print the tree it ran in.

Verify

  • git worktree list shows the new entry
  • .worktrees directory confirmed in .gitignore
  • Dependencies installed in the worktree
  • Baseline test suite passes in the worktree

References

Task-specific references

Read the relevant reference before implementing or reviewing the matching behavior:

  • For dependency provenance, environment adaptation, review/work workflows, branch completion, or hooks: worktree-integration.md.

Existing specialized references, when the corresponding topic applies:

Files (whetstone)
  • references
    • hooks-and-excludes.md 3.2 KB
      # Hooks and Local Excludes
      
      ## Hook Safety Under Husky
      
      Installing hooks into `.git/hooks/` silently fails on any repo that uses Husky. Husky sets `core.hooksPath` (typically to `.husky/_`) and git ignores `.git/hooks/` entirely when that config is non-empty. The hook file lands on disk, is executable, is correct, and is dead. Invisible failure until someone asks why the post-merge behavior isn't running.
      
      ### Detection rule before writing any hook
      
      ```bash
      hooks_path=$(git -C "$repo" config --get core.hooksPath)
      ```
      
      - Empty output: write to `$(git rev-parse --git-common-dir)/hooks/<name>` as usual.
      - `.husky/_` (or any path containing `husky.sh` / `h` trampoline): Husky v9 setup. Write to `.husky/<name>`; do NOT include the v8-era `. "$(dirname "$0")/_/husky.sh"` line (v9 prints a deprecation warning if you do).
      - Unrecognized non-empty value: refuse to write the hook and surface the path to the user. Silent writes to the wrong location waste debug cycles later.
      
      ### Worktree-safe hook body
      
      `.git/hooks/` lives in the common git dir and runs for every worktree of the clone. A hook installed once fires across all trees. Two rules to stay safe:
      
      1. Resolve the invoking worktree's root inside the hook body with `git rev-parse --show-toplevel`, not a hardcoded path. Hardcoding means the last `install-hooks` invocation wins for every worktree.
      2. Guard the tool invocation with an existence check on the tool's per-tree state dir. Siblings without your tool's setup must no-op, not spawn a failing background process per git op.
      
      ```sh
      #!/bin/sh
      root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
      [ -d "$root/.my-tool" ] || exit 0
      ( cd "$root" && my-tool index ) >>"$root/.my-tool/hook.log" 2>&1 &
      exit 0
      ```
      
      Redirect to a log file inside the tool's state dir, not `/dev/null`; silent failures produce stale state you only notice hours later.
      
      ## Local Excludes: .git/info/exclude vs .gitignore
      
      Tooling artifacts (local index dirs, hook helpers, per-developer scratch files) belong in `.git/info/exclude`, NOT in the tracked `.gitignore`.
      
      - `.gitignore` is content: tracked, shared with the team, reviewed in PRs. Adding a personal tooling rule there pollutes a shared file. On foreign repos (upstream projects, third-party clones) the rule either rides into a PR by accident or sits as a dirty working tree forever.
      - `.git/info/exclude` is local: untracked, lives in the common git dir, shared across every worktree of the clone. Same syntax and semantics as `.gitignore` without the leakage.
      
      ### Resolving the path correctly under worktrees
      
      Don't hardcode `$repo/.git/info/exclude`. In a worktree, `.git` is a file (gitlink), not a directory. Use git itself:
      
      ```bash
      exclude=$(git -C "$repo" rev-parse --git-path info/exclude)
      ```
      
      ### Idempotent append
      
      ```bash
      line="/.my-tool/"
      mkdir -p "$(dirname "$exclude")"
      grep -qxF "$line" "$exclude" 2>/dev/null || printf '\n# my-tool\n%s\n' "$line" >> "$exclude"
      ```
      
      `grep -qxF` matches the exact line with no regex surprises.
      
      ### When to break the rule
      
      Only when the artifact is genuinely team-shared and belongs in the repo (build outputs used in CI, generated files, vendored dependencies). If in doubt, ask: "would another contributor benefit from this rule?" If no, exclude locally.
      
    • troubleshooting.md 4.1 KB
      # Troubleshooting & Technical Details
      
      ## Troubleshooting
      
      ### "Worktree already exists"
      
      The script offers to print its path. This does not transfer ownership or change the caller's working directory.
      
      ### "Cannot remove worktree: it is the current worktree"
      
      Run cleanup with the main checkout as workdir. Obtain its path from `list`; `git rev-parse --show-toplevel` inside a linked checkout returns that linked checkout, not the main one.
      
      ```bash
      env -C "$main_checkout" bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh cleanup feature-name
      ```
      
      ### Lost in a worktree?
      
      See where you are:
      
      ```bash
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh list
      ```
      
      ### .env files missing in worktree?
      
      If a worktree was created without .env files (e.g., via raw `git worktree add`), copy them:
      
      ```bash
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh copy-env feature-name
      ```
      
      Use the main path shown by `list` for commands aimed at the main checkout:
      
      ```bash
      env -C "$main_checkout" git status --short
      ```
      
      ### Cleanup refuses a tree
      
      Supply explicit names and the `WORKTREE_SESSION_ID` set before creation. Do not reuse another session's token. Preserve any dirty, untracked, or ignored files, including copied environment files and dependencies; arrange their disposition under user authority before retrying. A locked tree remains protected by Git. A clean tree from this session can be removed after confirming no process uses it.
      
      ---
      
      ## Branch from a fresh remote base (manager-script behavior)
      
      Do not run these steps manually; the script runs them. Read only when debugging why `create` branched from `origin/<base>` instead of a local branch, or why it fell back to a local ref.
      
      When creating a worktree's branch from the default branch (`main`/`master`), the local base may be ahead of `origin/<base>` due to another session, worktree, or background task. Branching from local HEAD silently carries those unrelated commits into the new feature branch and the eventual PR. Checking out `<base>` in the caller's working tree to update it first is worse: it silently switches the user's active branch out from under them, which is why the script never does that.
      
      The script's actual sequence (fetch-only, never checks out the caller's branch):
      
      ```bash
      GIT_TERMINAL_PROMPT=0 git fetch --no-tags origin <base>
      if [ $? -eq 0 ]; then
        base_ref="origin/<base>"
      else
        base_ref="<base>"   # offline fallback: branch from the local ref
      fi
      git worktree add .worktrees/<name> -b <branch> "$base_ref"
      ```
      
      A narrow `remote.origin.fetch` refspec makes `git fetch origin` silently partial. When the config maps only one branch, every other remote-tracking ref stays frozen, and `git log origin/<other>` or `git merge-base --is-ancestor` return stale answers with no error. Check `git config --get-all remote.origin.fetch`, and pass an explicit refspec before making any claim about another branch.
      
      Known gap: the script does not distinguish "stale-base contamination" (another session advanced local `<base>` past `origin/<base>` with unrelated commits) from "forgot-to-branch" (the user's own unpushed commits on local `<base>` that were meant for a feature branch); it always prefers `origin/<base>` when the fetch succeeds. To carry unpushed local commits on `<base>` forward into the new branch instead, branch manually: `git worktree add <path> -b <branch> <base>`.
      
      ---
      
      ## Technical Details
      
      ### Directory Structure
      
      ```
      .worktrees/
      ├── feature-login/          # Worktree 1
      │   ├── .git
      │   ├── app/
      │   └── ...
      ├── feature-notifications/  # Worktree 2
      │   ├── .git
      │   ├── app/
      │   └── ...
      └── ...
      
      .gitignore (updated to include .worktrees)
      ```
      
      ### How It Works
      
      - Uses `git worktree add` for isolated environments
      - Each worktree has its own branch
      - Changes in one worktree don't affect others
      - Share git history with main repo
      - Can push from any worktree
      
      ### Performance
      
      - Worktrees are lightweight (just file system links)
      - No repository duplication
      - Shared git objects for efficiency
      - Much faster than cloning or stashing/switching
      
    • workflow-examples.md 1.7 KB
      # Workflow Examples
      
      ## Code Review with Worktree
      
      ```bash
      # Claude Code recognizes you're not on the PR branch
      # Offers: "Use worktree for isolated review? (y/n)"
      
      # You respond: yes
      export WORKTREE_SESSION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
      # Script runs (copies .env files automatically):
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh create pr-123-feature-name
      
      # Apply the returned path to each review command's workdir:
      target=$(bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh switch pr-123-feature-name)
      env -C "$target" git status --short
      
      # After integrating work and disposing of generated/ignored files with authorization:
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh cleanup pr-123-feature-name
      ```
      
      ## Parallel Feature Development
      
      ```bash
      # For first feature (copies .env files):
      export WORKTREE_SESSION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh create feature-login
      
      # Later, start second feature (also copies .env files):
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh create feature-notifications
      
      # List what you have:
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh list
      
      # Switch between them as needed:
      target=$(bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh switch feature-login)
      env -C "$target" git status --short
      
      # With both trees clean, integrated, and no processes using them:
      bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh cleanup feature-login feature-notifications
      ```
      
    • worktree-integration.md 3.9 KB
      # Worktree integration
      
      ## Dependency Provenance
      
      Never satisfy a worktree's gitignored dependency directory with a symlink to another checkout's. Generated autoloaders and module resolvers compute the application base directory from the *real* location of their own files, so the link resolves back into the donor tree and every first-party class or module loads from there: your worktree's edits never execute, new files appear as "not found", and config comes from the other tree's `.env`. Give the worktree a real directory: `cp -al <donor>/vendor "$WT/vendor"` (hard links: same inodes, near-zero disk, correct base dir) for a read-only harness, or a full dereferencing copy / real install whenever anything will write into it, because with hard links a package-manager write edits the donor too. Assert it once rather than assuming: print the resolved file path of one first-party symbol and confirm it names the worktree.
      
      
      ## Environment Detection
      
      Before creating worktrees, detect the execution context:
      
      1. **Codex/sandbox environment?** If `$CODEX_SANDBOX` is set or the repo is at a non-standard path (e.g., `/tmp/`, `/workspace/`), worktrees may not be supported. Fall back to regular branch switching.
      2. **Bare repo?** If `git rev-parse --is-bare-repository` returns true, worktrees are the only way to have a working directory. Adjust paths accordingly.
      
      Adapt the workflow to the detected context rather than failing with a generic error.
      
      
      ## Integration with Workflows
      
      ### Code review (`/ia-review` in Claude Code)
      
      1. Check current branch
      2. If ALREADY on target branch -> stay there, no worktree needed
      3. If DIFFERENT branch -> Ask via AskUserQuestion (Claude Code; load with ToolSearch `select:AskUserQuestion` if not loaded) or request_user_input (Codex); fall back to numbered options in chat. Options: 1) review in a new worktree 2) switch branch in place
      
      ### Plan execution (`/ia-work` in Claude Code)
      
      Always offer choice:
      1. New branch on current worktree (live work)
      2. Worktree (parallel work)
      
      
      ## Branch Completion
      
      When work in a worktree is done, verify tests pass, then present exactly 3 options. Ask via AskUserQuestion (Claude Code; load with ToolSearch `select:AskUserQuestion` if not loaded) or request_user_input (Codex); fall back to numbered options in chat.
      
      1. **Merge locally**: merge into base branch, delete worktree branch, clean up worktree
      2. **Push + PR**: push branch, create PR with `gh pr create`, keep worktree until merged
      3. **Keep as-is**: leave branch and worktree for later
      Discarding is never offered as an option. Delete the branch and worktree only when the user asks for it explicitly, and require typing "discard" to confirm first. No silent discards.
      
      
      ## Change Summary
      
      When completing work in a worktree (before merge or PR), output a structured summary:
      
      ```
      CHANGES MADE:
      - src/routes/tasks.ts: Added validation middleware
      
      THINGS I DIDN'T TOUCH (intentionally):
      - src/routes/auth.ts: Has similar validation gap but out of scope
      
      POTENTIAL CONCERNS:
      - The Zod schema is strict -- rejects extra fields. Confirm this is desired.
      ```
      
      The "DIDN'T TOUCH" section prevents reviewers from wondering whether adjacent issues were missed or intentionally deferred.
      
      
      ## Hooks and Local Excludes
      
      Before writing any git hook, check `git config core.hooksPath`; Husky repos ignore `.git/hooks/` entirely. Personal tooling excludes go in `$(git rev-parse --git-path info/exclude)`, never the tracked `.gitignore`. Details: [hooks-and-excludes.md](./hooks-and-excludes.md)
      
      In a linked worktree `.git` is a file, so every `.git/<state-file>` test is wrong: `test -f .git/MERGE_HEAD` reports "no merge in progress" in the middle of a conflict, because per-worktree state lives in the common dir under `worktrees/<name>/`. Ask the plumbing instead: `git rev-parse -q --verify MERGE_HEAD` for the state, `git rev-parse --git-path <file>` for the path. Same for `REBASE_HEAD`, `CHERRY_PICK_HEAD`, and hook paths.
      
  • scripts
    • worktree-manager.sh 9.2 KB
      #!/bin/bash
      
      # Git Worktree Manager
      # Handles creating, listing, switching, and cleaning up Git worktrees
      
      set -e
      
      RED='\033[0;31m'
      GREEN='\033[0;32m'
      YELLOW='\033[1;33m'
      BLUE='\033[0;34m'
      NC='\033[0m' # No Color
      
      CURRENT_ROOT=$(git rev-parse --show-toplevel)
      COMMON_DIR=$(realpath -- "$(git rev-parse --git-common-dir)")
      CURRENT_GIT_DIR=$(realpath -- "$(git rev-parse --git-dir)")
      if [[ "$CURRENT_GIT_DIR" == "$COMMON_DIR" ]]; then
        GIT_ROOT="$CURRENT_ROOT"
      elif [[ -f "$COMMON_DIR/whetstone-main-root" ]]; then
        IFS= read -r GIT_ROOT < "$COMMON_DIR/whetstone-main-root"
      else
        IFS= read -r main_record < <(git worktree list --porcelain)
        GIT_ROOT=${main_record#worktree }
      fi
      [[ "$(git -C "$GIT_ROOT" rev-parse --show-toplevel)" == "$GIT_ROOT" ]] || {
        echo "Error: main checkout unavailable; run create from the main checkout first" >&2
        exit 1
      }
      ROOT_COMMON_DIR=$(git -C "$GIT_ROOT" rev-parse --git-common-dir)
      if [[ "$ROOT_COMMON_DIR" != /* ]]; then
        ROOT_COMMON_DIR="$GIT_ROOT/$ROOT_COMMON_DIR"
      fi
      [[ "$(realpath -- "$ROOT_COMMON_DIR")" == "$COMMON_DIR" ]] || {
        echo "Error: cannot resolve the main worktree safely" >&2
        exit 1
      }
      WORKTREE_DIR="$GIT_ROOT/.worktrees"
      
      resolve_worktree() {
        local name="$1" candidate registered
        git check-ref-format --branch "$name" >/dev/null 2>&1 || return 1
        candidate=$(realpath -e -- "$WORKTREE_DIR/$name") || return 1
        [[ "$candidate" == "$WORKTREE_DIR/"* ]] || return 1
        while IFS= read -r registered; do
          if [[ "$registered" == "worktree $candidate" ]]; then
            printf '%s\n' "$candidate"
            return 0
          fi
        done < <(git worktree list --porcelain)
        return 1
      }
      
      ensure_gitignore() {
        if ! grep -q "^\.worktrees$" "$GIT_ROOT/.gitignore" 2>/dev/null; then
          echo ".worktrees" >> "$GIT_ROOT/.gitignore"
        fi
      }
      
      copy_env_files() {
        local worktree_path="$1"
      
        echo -e "${BLUE}Copying environment files...${NC}"
      
        local env_files=()
        for f in "$GIT_ROOT"/.env*; do
          if [[ -f "$f" ]]; then
            local basename
            basename=$(basename "$f")
            if [[ "$basename" != ".env.example" ]]; then
              env_files+=("$basename")
            fi
          fi
        done
      
        if [[ ${#env_files[@]} -eq 0 ]]; then
          echo -e "  ${YELLOW}ℹ️  No .env files found in main repository${NC}"
          return
        fi
      
        local copied=0
        for env_file in "${env_files[@]}"; do
          local source="$GIT_ROOT/$env_file"
          local dest="$worktree_path/$env_file"
      
          if [[ -f "$dest" ]]; then
            echo -e "  ${YELLOW}⚠️  $env_file already exists, backing up to ${env_file}.backup${NC}"
            cp "$dest" "${dest}.backup"
          fi
      
          cp "$source" "$dest"
          echo -e "  ${GREEN}✓ Copied $env_file${NC}"
          copied=$((copied + 1))
        done
      
        echo -e "  ${GREEN}✓ Copied $copied environment file(s)${NC}"
      }
      
      create_worktree() {
        local branch_name="$1"
        local from_branch="${2:-main}"
      
        if [[ -z "$branch_name" ]]; then
          echo -e "${RED}Error: Branch name required${NC}"
          exit 1
        fi
      
        git check-ref-format --branch "$branch_name" >/dev/null || return 1
        if [[ -L "$WORKTREE_DIR" ]]; then
          echo "Error: refusing a symlinked worktree directory" >&2
          return 1
        fi
      
        local worktree_path="$WORKTREE_DIR/$branch_name"
        if [[ "$(realpath -m -- "$worktree_path")" != "$worktree_path" ]]; then
          echo "Error: refusing redirected worktree path: $worktree_path" >&2
          return 1
        fi
      
        if [[ -d "$worktree_path" ]]; then
          echo -e "${YELLOW}Worktree already exists at: $worktree_path${NC}"
          echo -e "Switch to it instead? (y/n)"
          # No stdin (agent/CI): read fails, set -e would abort. Empty answer = decline.
          read -r response || response=""
          if [[ "$response" == "y" ]]; then
            switch_worktree "$branch_name"
          fi
          return
        fi
      
        echo -e "${BLUE}Creating worktree: $branch_name${NC}"
        echo "  From: $from_branch"
        echo "  Path: $worktree_path"
      
        # Fetch a fresh remote base without touching the caller's checkout.
        echo -e "${BLUE}Fetching $from_branch from origin...${NC}"
        local base_ref="origin/$from_branch"
        # GIT_TERMINAL_PROMPT=0: fail fast instead of hanging on a credential
        # prompt blocking on an inherited tty. A non-zero exit (offline, no
        # remote) falls back to the local branch ref below.
        if ! GIT_TERMINAL_PROMPT=0 git fetch --no-tags origin "$from_branch"; then
          echo -e "${YELLOW}Fetch failed; branching from local $from_branch instead${NC}"
          base_ref="$from_branch"
        fi
      
        mkdir -p "$WORKTREE_DIR"
        ensure_gitignore
        printf '%s\n' "$GIT_ROOT" > "$COMMON_DIR/whetstone-main-root"
      
        echo -e "${BLUE}Creating worktree...${NC}"
        git worktree add -b "$branch_name" "$worktree_path" "$base_ref"
      
        if [[ -n "${WORKTREE_SESSION_ID:-}" ]]; then
          printf '%s\n' "$WORKTREE_SESSION_ID" > "$(git -C "$worktree_path" rev-parse --git-path whetstone-owner)"
        else
          echo "Set WORKTREE_SESSION_ID before create to enable session-owned cleanup." >&2
        fi
      
        copy_env_files "$worktree_path"
      
        echo -e "${GREEN}✓ Worktree created successfully!${NC}"
        echo ""
        echo "Run commands with this worktree as their workdir:"
        printf 'env -C %q <command>\n' "$worktree_path"
        echo ""
      }
      
      list_worktrees() {
        git worktree list
      }
      
      switch_worktree() {
        local worktree_name="$1"
      
        if [[ -z "$worktree_name" ]]; then
          list_worktrees >&2
          echo "Return the path of which worktree? (enter name)" >&2
          read -r worktree_name || worktree_name=""
        fi
      
        resolve_worktree "$worktree_name" || {
          echo "Error: registered worktree not found: $worktree_name" >&2
          return 1
        }
      }
      
      copy_env_to_worktree() {
        local worktree_name="$1"
        local worktree_path
      
        if [[ -z "$worktree_name" ]]; then
          local current_dir="$CURRENT_ROOT"
          if [[ "$current_dir" == "$WORKTREE_DIR"/* ]]; then
            worktree_path="$current_dir"
            worktree_name=$(basename "$worktree_path")
            echo -e "${BLUE}Detected current worktree: $worktree_name${NC}"
          else
            echo -e "${YELLOW}Usage: worktree-manager.sh copy-env [worktree-name]${NC}"
            echo "Or run from within a worktree to copy to current directory"
            list_worktrees
            return 1
          fi
        else
          if ! worktree_path=$(resolve_worktree "$worktree_name"); then
            echo -e "${RED}Error: Worktree not found: $worktree_name${NC}"
            list_worktrees
            return 1
          fi
        fi
      
        copy_env_files "$worktree_path"
        echo ""
      }
      
      cleanup_worktrees() {
        if [[ $# -eq 0 || -z "${WORKTREE_SESSION_ID:-}" ]]; then
          echo "Usage: set WORKTREE_SESSION_ID before create; cleanup <owned-name> [owned-name...]" >&2
          return 1
        fi
        local name worktree_path owner_file status response
        local to_remove=()
        for name in "$@"; do
          worktree_path=$(resolve_worktree "$name") || return 1
          owner_file=$(git -C "$worktree_path" rev-parse --git-path whetstone-owner)
          if [[ "$CURRENT_ROOT" == "$worktree_path" || ! -f "$owner_file" || "$(cat "$owner_file")" != "$WORKTREE_SESSION_ID" ]]; then
            echo "Refusing current or unowned worktree: $worktree_path" >&2
            return 1
          fi
          status=$(git -C "$worktree_path" status --porcelain --untracked-files=all --ignored=matching) || return 1
          if [[ -n "$status" ]]; then
            echo "Refusing worktree with tracked, untracked, or ignored changes: $worktree_path" >&2
            return 1
          fi
          to_remove+=("$worktree_path")
        done
        printf 'Remove these session-owned clean worktrees?\n'
        printf '  %s\n' "${to_remove[@]}"
        echo "Confirm no process is using them. Remove? (y/n)"
        read -r response || response=""
        [[ "$response" == y ]] || return 0
        for worktree_path in "${to_remove[@]}"; do
          git worktree remove "$worktree_path" || return 1
          printf 'Removed: %s\n' "$worktree_path"
        done
      }
      
      main() {
        local command="${1:-list}"
      
        case "$command" in
          create)
            create_worktree "${2:-}" "${3:-main}"
            ;;
          list|ls)
            list_worktrees
            ;;
          switch|go)
            switch_worktree "${2:-}"
            ;;
          copy-env|env)
            copy_env_to_worktree "${2:-}"
            ;;
          cleanup|clean)
            shift
            cleanup_worktrees "$@"
            ;;
          help)
            show_help
            ;;
          *)
            echo -e "${RED}Unknown command: $command${NC}"
            echo ""
            show_help
            exit 1
            ;;
        esac
      }
      
      show_help() {
        cat << EOF
      Git Worktree Manager
      
      Usage: worktree-manager.sh <command> [options]
      
      Commands:
        create <branch-name> [from-branch]  Create new worktree (copies .env files automatically)
                                            (from-branch defaults to main)
        list | ls                           List all worktrees
        switch | go [name]                  Print registered worktree path for caller workdir
        copy-env | env [name]               Copy .env files from main repo to worktree
                                            (if name omitted, uses current worktree)
        cleanup | clean <name> [...]        Remove named clean worktrees owned by WORKTREE_SESSION_ID
        help                                Show this help message
      
      Environment Files:
        - Automatically copies .env, .env.local, .env.test, etc. on create
        - Skips .env.example (should be in git)
        - Creates .backup files if destination already exists
        - Use 'copy-env' to refresh env files after main repo changes
      
      Examples:
        worktree-manager.sh create feature-login
        worktree-manager.sh create feature-auth develop
        worktree-manager.sh switch feature-login
        worktree-manager.sh copy-env feature-login
        worktree-manager.sh copy-env                   # copies to current worktree
        worktree-manager.sh cleanup feature-login
        worktree-manager.sh list
      
      EOF
      }
      
      main "$@"
      
  • SKILL.md 8.8 KB
    ---
    name: ia-git-worktree
    class: tool
    description: >-
      Manage Git worktrees for isolated parallel development. Use when creating,
      listing, switching, or cleaning up git worktrees, or when needing isolated
      branches for concurrent reviews or feature work.
    ---
    
    # Git worktree manager
    
    **GATE: If the task runs inside an existing worktree (a worktree path is given and no create/remove/switch is requested), none of the creation flow applies: work in place and skip this skill.** To check: `git rev-parse --show-toplevel` appears as a linked entry in `git worktree list`.
    
    ## Working rules
    
    - Work in place when given an existing worktree without a creation/removal request.
    - Keep one writer per worktree and preserve other sessions' branches, files, and staged work.
    - Verify dependencies resolve first-party code from the intended worktree.
    - Keep merge, push, and cleanup within user authorization; report the exact exercised checkout and resulting commit.
    
    ## Always use the manager script
    
    Never call `git worktree add` directly; always use the `worktree-manager.sh` script.
    
    The script handles critical setup that raw git commands don't:
    1. Copies `.env`, `.env.local`, `.env.test`, etc. from main repo
    2. Ensures `.worktrees` is in `.gitignore`
    3. Creates consistent directory structure
    4. After creation, install dependencies if detected: `package.json` → `npm install`, `composer.json` → `composer install`, `pyproject.toml` → `pip install -e .`, `go.mod` → `go mod download`
    
    All commands use: `bash ${CLAUDE_PLUGIN_ROOT}/skills/ia-git-worktree/scripts/worktree-manager.sh <command>`. If `CLAUDE_PLUGIN_ROOT` is unset (non-Claude-Code harness), resolve the script relative to this skill's own directory.
    
    Before creating worktrees, export a unique `WORKTREE_SESSION_ID` and retain that same value for this session's later manager calls. For example, `export WORKTREE_SESSION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"`. The manager records ownership in each new worktree's Git metadata. Creation without a session ID remains available, but manager cleanup then refuses that tree; never adopt a previous session's ID to bypass ownership.
    
    The manager script fetches `origin/<base>` fresh and branches from it; it never checks out `<base>` in the caller's working tree. If the fetch fails (offline, no remote), it falls back to the local `<base>` ref. Details: [troubleshooting.md](./references/troubleshooting.md).
    
    
    ## Commands
    
    | Command | Description | Example |
    |---------|-------------|---------|
    | `create <branch> [from]` | Create worktree + branch (default: from main) | `...worktree-manager.sh create feature-login` |
    | `list` / `ls` | List all worktrees with status | `...worktree-manager.sh list` |
    | `switch <name>` / `go` | Print the registered worktree's absolute path; the caller applies it as workdir | `target=$(...worktree-manager.sh switch feature-login)` |
    | `copy-env <name>` | Copy .env files to existing worktree | `...worktree-manager.sh copy-env feature-login` |
    | `cleanup <name> [...]` / `clean` | Confirm removal of named, session-owned, clean worktrees | `...worktree-manager.sh cleanup feature-login` |
    
    Run commands with `env -C "$target" <command>` or the harness workdir argument. A child script cannot change the caller's working directory. Listing and resolving names work from the main checkout, linked checkouts, and their subdirectories.
    
    Cleanup refuses the current checkout, another session's tree, and tracked, untracked, or ignored files. Auto-copied `.env` files and installed dependencies therefore require explicit user-managed disposition before cleanup. Confirm no process uses the named trees; the manager cannot detect every external reader. Git's normal removal safeguards remain enabled, including locked-tree refusal. Do not force deletion or suppress failures to finish cleanup.
    
    
    ## Safety Verification
    
    Before creating a worktree, verify the worktree directory is gitignored:
    
    ```bash
    # Verify .worktrees is ignored (should output ".worktrees")
    git check-ignore .worktrees || echo "WARNING: .worktrees not in .gitignore"
    ```
    
    If not ignored, add it to `.gitignore` before proceeding.
    
    After creating a worktree, run the project's test suite (or the fastest relevant subset if the full suite exceeds a few minutes) to establish a clean baseline. Catch pre-existing failures in the worktree before starting new work, not mid-implementation.
    
    
    ## Ownership
    
    - One writer per worktree. Treat every `git worktree list` entry not created **in the current session** as read-only; a tree left from a previous round is not the current session's either. Reuse is most tempting exactly where it is most dangerous: an existing tree already has dependencies and env wired up, and another session may be running a suite in it.
    - Do not mutate a tree while the current session's suite runs there. Test runners load source files as they reach them, so a mid-run edit produces a mass-failure result that looks exactly like a real regression.
    - A failure burst that contradicts a claim is a harness **hypothesis**, not a conclusion. Do not record or report the self-inflicted attribution until a re-run on a tree just asserted clean (`git status --short` empty) has returned.
    - When a mutation is unavoidable, assert the restore (grep the token back to its original count, plus `git status --short`) rather than trusting `git checkout --`.
    - One checkout has one index, so staging explicit paths does not scope a commit: `git add <mine> && git commit` also commits whatever a peer staged, under the current session's message. The protection is a pathspec on the commit itself (`git commit -- <paths>`), which takes those paths from the working tree and ignores the index; new files still need `git add`. It constrains that commit, not a peer's, so the residual control is latency between writing and committing. Read `git show --stat HEAD` afterwards and confirm only the intended files are there.
    - `git -C <repo> push <remote> HEAD:<branch>` resolves `HEAD` in **that** repo, not in the worktree that was edited. Edits made in a linked worktree and pushed with `-C` at the main checkout publish the main checkout's commit onto the feature branch, and `--force-with-lease` does not catch it because the lease checks the branch's old value, not what `HEAD` names. Never spell `HEAD:` in a `-C` push; resolve the SHA in the worktree and push it explicitly, then confirm with `git ls-remote`. Two branches "updated" to one SHA, or a pushed subject unrelated to the work, is the tell.
    - Linked worktrees share one stash stack. `git stash` writes to the common git directory, so a red/green cycle in one worktree can pop and drop a stash another worktree pushed in between. Never stash for red/green here: `git diff > /tmp/red.patch`, `git checkout -- <files>` (worktree-local) for the red run, `git apply /tmp/red.patch` for green. A dropped stash is still recoverable while its commit survives: `git stash store -m <message> <sha>` re-registers the SHA that `Dropped refs/stash@{0} (<sha>)` printed.
    - A worktree's HEAD is shared mutable state, so answer branch questions from refs. Any other session can check something else out there, which makes `git -C <worktree> rev-parse HEAD` describe a different branch and report a correct push as a mismatch. Refs are shared across every worktree: ask any one of them about the branch by name (`rev-parse <branch>`, `rev-list --count origin/<branch>..<branch>`, `reflog <branch>`).
    
    Use `env -C <worktree> <cmd>` for every command, never `cd`. A shell's cwd persists across calls, so one `cd <repo-root>` for an unrelated reason silently relocates every later command: probe files get written into the shared main tree and run against its bytes, and the tidy-up reflex `git checkout -- <path>` becomes a **write** aimed at the wrong tree. The `git -C` habit does not generalize: interpreters, test runners, linters, and a heredoc `cat >` all take the cwd. Have any probe print the tree it ran in.
    
    
    ## Verify
    
    - `git worktree list` shows the new entry
    - `.worktrees` directory confirmed in `.gitignore`
    - Dependencies installed in the worktree
    - Baseline test suite passes in the worktree
    
    
    ## References
    
    - [workflow-examples.md](./references/workflow-examples.md) - Code review and parallel development workflows
    - [troubleshooting.md](./references/troubleshooting.md) - Common issues, fresh-remote-base behavior, directory structure, how it works
    - [hooks-and-excludes.md](./references/hooks-and-excludes.md) - Hook safety under Husky, .git/info/exclude vs .gitignore
    - [worktree-manager.sh](./scripts/worktree-manager.sh) - The manager script
    
    ## Task-specific references
    
    Read the relevant reference before implementing or reviewing the matching behavior:
    
    - For dependency provenance, environment adaptation, review/work workflows, branch completion, or hooks: [worktree-integration.md](./references/worktree-integration.md).
    
    Existing specialized references, when the corresponding topic applies:
    
  • SPEC.md 4.3 KB
    # ia-git-worktree Specification
    
    ## Intent
    
    `ia-git-worktree` is a `tool`-class skill (a narrow utility scoped to a single capability). Manage Git worktrees for isolated parallel development. Use when creating, listing, switching, or cleaning up git worktrees, or when needing isolated branches for concurrent reviews or feature work.
    
    ## Scope
    
    In scope:
    - Behaviors described in `SKILL.md` and routed via the should_trigger phrasings in `distillery/tests/fixtures/triggers/ia-git-worktree.jsonl`.
    - Updates to runtime behavior, structure, trigger precision, references, and validation.
    
    Out of scope:
    - Acting as the runtime instructions themselves (those live in `SKILL.md`).
    - Trigger phrasings already covered by adjacent `ia-*` skills (`validate-plugin` flags >70% description overlap as DUPLICATE_TRIGGER).
    - <!-- to fill in: domain-specific exclusions when the skill drifts -->
    
    ## Trigger Context
    
    - Class: `tool`
    - Hook regex: `plugins/whetstone/hooks/skill-patterns.sh` -> `SKILL_PATTERNS[ia-git-worktree]`
    - Common requests (from fixture should_trigger):
      - "create a worktree for the feature branch so I can review in parallel"
      - "set up parallel development branches using worktrees"
      - "set up an isolated worktree to review this PR"
    - Should not trigger for (from fixture should_not_trigger):
      - "write an Eloquent model for the invoices table"
      - "add rate limiting to the public API"
      - "merge the PR into main"
    
    ## Source And Evidence Model
    
    Authoritative sources:
    
    - `SKILL.md`: runtime instructions and reference routing.
    - `references/*.md`: bundled supplementary content (4 file(s)).
    - `distillery/tests/fixtures/triggers/ia-git-worktree.jsonl`: positive and negative trigger phrasings under regression test.
    - `plugins/whetstone/hooks/skill-patterns.sh`: regex pattern that fires this skill.
    - `distillery/.eval-data/ia-git-worktree/`: harvested session examples (when present).
    
    Data that must not be stored in this skill or its references:
    
    - Secrets, credentials, tokens.
    - Machine-specific filesystem paths (`/home/...`, `/Users/...`, `~/ai/...`). The validator (`MACHINE_PATH_LEAK`) flags these as HIGH.
    - Private URLs, customer data, or unredacted personal information.
    
    ### Coverage matrix
    
    | Dimension | Status | Evidence |
    |---|---|---|
    | Trigger fixtures | complete | distillery/tests/fixtures/triggers/ia-git-worktree.jsonl (>=5 should_trigger, >=5 should_not_trigger) |
    | Hook regex pattern | complete | plugins/whetstone/hooks/skill-patterns.sh (`SKILL_PATTERNS[ia-git-worktree]`) |
    | Reference architecture | complete | 4 file(s) under references/ |
    | Real-usage signal | <!-- populated by harvest-sessions when sessions exist --> | distillery/.eval-data/ia-git-worktree/ (created by harvest-sessions) |
    
    ## Evaluation
    
    Lightweight (run on every change):
    
    ```bash
    python3 distillery/scripts/distiller.py validate-plugin --component ia-git-worktree
    python3 distillery/scripts/distiller.py test-triggers --skill ia-git-worktree
    ```
    
    Deeper (when behavior risk warrants):
    
    ```bash
    python3 distillery/scripts/distiller.py dspy-eval ia-git-worktree
    python3 distillery/scripts/distiller.py diagnose-negatives ia-git-worktree
    ```
    
    Acceptance gates:
    - `validate-plugin --component ia-git-worktree` returns 0 HIGH findings.
    - `test-triggers --skill ia-git-worktree` returns F1 = 1.0 with floors of 5 should_trigger and 5 should_not_trigger.
    - For dspy-eval, the composite score does not regress against the most recent saved baseline (see `distillery/.eval-data/ia-git-worktree/history.json`).
    
    ## Known Limitations
    
    <!-- to fill in over time as drift surfaces. Default rule: any time diagnose-negatives
         surfaces a recurring failure pattern, document it here so future maintainers
         understand the trade-off the current implementation accepts. -->
    
    ## Maintenance Notes
    
    - Update `SKILL.md` when the runtime workflow, branch conditions, or output contract changes.
    - Update this `SPEC.md` when intent, scope, evidence model, evaluation gates, or maintenance expectations change.
    - Update the trigger fixture when adding new positive phrasings, removing stale ones, or expanding scope (the 5/5 floor is a hard validator gate).
    - Update the hook regex in `skill-patterns.sh` whenever fixture positives expose a missed phrasing; verify F1 = 1.0 with `eval-triggers` before committing.
    - Run the full release pipeline via `/release`; never bump versions or update CHANGELOG.md from a per-skill edit.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related