# Git Worktrees

> Use when starting feature work that needs an isolated workspace or worktree separate from the current branch, or before executing an implementation plan that should not disturb the current checkout, or when finishing with a worktree - deleting one leaves its per-topic build cache behind outside the checkout, so the disk stays full and nothing lists what to reclaim

- Skill: `bitranox/git-worktrees` (Agent Skill, multi-file: 11 files)
- Install (CLI): `npx skillmds@latest add bitranox/git-worktrees`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bitranox/git-worktrees/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: bitranox (https://skillmd.com/u/bitranox)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bitranox/git-worktrees

---


# Git Worktrees

> Adapted from the superpowers plugin (MIT).

## Overview

Ensure work happens in an isolated workspace. Prefer your platform's native worktree tools. Fall back to manual git worktrees only when no native tool is available.

**Core principle:** Detect existing isolation first. Then use native tools. Then fall back to git. Never fight the harness.

**Announce at start:** "I'm using the git-worktrees skill to set up an isolated workspace."

## Step 0: Detect Existing Isolation

**Before creating anything, check if you are already in an isolated workspace.**

```bash
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
BRANCH=$(git branch --show-current)
```

**Submodule note:** a plain submodule does NOT trip this test - measured with these exact commands,
`GIT_DIR` and `GIT_COMMON` both resolve to `<super>/.git/modules/<name>`, so they compare equal and
the submodule reads as a normal checkout, which is how you want to treat it. The pair differs only
in a linked worktree. Run this if you want it stated explicitly, or when a submodule may itself
have a worktree attached:

```bash
# Returns a path when you are inside a submodule; empty otherwise
git rev-parse --show-superproject-working-tree 2>/dev/null
```

**If `GIT_DIR != GIT_COMMON` (and not a submodule):** You are already in a linked worktree. Skip to Step 2 (Project Setup). Do NOT create another worktree.

Report with branch state:
- On a branch: "Already in isolated workspace at `<path>` on branch `<name>`."
- Detached HEAD: "Already in isolated workspace at `<path>` (detached HEAD, externally managed). Branch creation needed at finish time."

**If `GIT_DIR == GIT_COMMON` (or in a submodule):** You are in a normal repo checkout.

Has the user already indicated their worktree preference in your instructions? If not, ask for consent before creating a worktree:

> "Would you like me to set up an isolated worktree? It protects your current branch from changes."

Honor any existing declared preference without asking. If the user declines consent, work in place and skip to Step 2.

## Step 1: Create Isolated Workspace

**You have two mechanisms. Try them in this order.**

### 1a. Native Worktree Tools (preferred)

The user has asked for an isolated workspace (Step 0 consent). Do you already have a way to create a worktree? It might be a tool with a name like `EnterWorktree`, `WorktreeCreate`, a `/worktree` command, or a `--worktree` flag. If you do, use it and skip to Step 2.

Native tools handle directory placement, branch creation, and cleanup automatically. Using `git worktree add` when you have a native tool creates phantom state your harness can't see or manage.

Only proceed to Step 1b if you have no native worktree tool available.

### 1b. Git Worktree Fallback

**Only use this if Step 1a does not apply** - you have no native worktree tool available. Create a worktree manually using git.

#### Directory Selection

Follow this priority order. Explicit user preference always beats observed filesystem state.

1. **Check your instructions for a declared worktree directory preference.** If the user has already specified one, use it without asking.

2. **Check for an existing project-local worktree directory:**
   ```bash
   ls -d .worktrees 2>/dev/null     # Preferred (hidden)
   ls -d worktrees 2>/dev/null      # Alternative
   ```
   If found, use it. If both exist, `.worktrees` wins.

3. **If there is no other guidance available**, default to `.worktrees/` at the project root.

4. **Bind the outcome**, because every step below refers to the directory you just chose:
   ```bash
   LOCATION=$(ls -d .worktrees 2>/dev/null || ls -d worktrees 2>/dev/null || echo .worktrees)
   BRANCH_NAME=<your-topic-branch>
   ```

#### Safety Verification (project-local directories only)

**MUST verify directory is ignored before creating worktree:**

```bash
# Check the directory you CHOSE, not both names. `git check-ignore` matches a PATHNAME and
# does not care whether it exists, so testing both with `||` reports "ignored" whenever
# EITHER name is listed - including the one you are not about to use.
git check-ignore -q "$LOCATION" 2>/dev/null
```

**If NOT ignored:** Add to .gitignore, commit the change, then proceed.

**Why critical:** Prevents accidentally committing worktree contents to repository.

#### Create the Worktree

```bash
# $LOCATION and $BRANCH_NAME were bound in Directory Selection above. Unset, this
# expands to "/<branch>" at the filesystem root, and the failure looks like a sandbox denial.
path="$LOCATION/$BRANCH_NAME"

git worktree add "$path" -b "$BRANCH_NAME"
cd "$path"
```

**Sandbox fallback:** If `git worktree add` fails with a permission error (sandbox denial), tell the user the sandbox blocked worktree creation and you're working in the current directory instead. Then run setup and baseline tests in place.

## Step 2: Project Setup

Auto-detect and run appropriate setup:

```bash
# Node.js
if [ -f package.json ]; then npm install; fi

# Rust
if [ -f Cargo.toml ]; then cargo build; fi

# Python (prefer uv)
if [ -f pyproject.toml ]; then uv sync 2>/dev/null || uv pip install -e .; fi
if [ -f requirements.txt ]; then uv pip install -r requirements.txt 2>/dev/null || pip install -r requirements.txt; fi

# Go
if [ -f go.mod ]; then go mod download; fi
```

## Step 3: Verify Clean Baseline

Run tests to ensure workspace starts clean:

```bash
# Use project-appropriate command
npm test / cargo test / pytest / go test ./...
```

**If tests fail:** Report failures, ask whether to proceed or investigate.

**If tests pass:** Report ready.

### Report

```
Worktree ready at <full-path>
Tests passing (<N> tests, 0 failures)
Ready to implement <feature-name>
```

## Step 4: Finishing - Reclaim the Per-Topic Build Cache (wtclean)

`git worktree remove` deletes the checkout and nothing else. The per-worktree build cache from
Step 2 lives OUTSIDE the checkout on purpose (that is what stops several worktrees fighting over
one `CARGO_TARGET_DIR`), so it survives the removal and piles up invisibly - usually noticed only
when the disk fills.

`scripts/wtclean.py` removes the worktree AND those caches together, and shows what it will take
before it takes anything:

```
uv run scripts/wtclean.py my-feature                 # the plan, with sizes - deletes nothing
uv run scripts/wtclean.py my-feature --apply         # remove exactly what the plan listed
uv run scripts/wtclean.py .worktrees/my-feature --cache-dir ~/.cache/targets/my-feature --apply
```

**Cache locations are a convention, not a discovery.** Git cannot be asked where your build cache
lives, so the default candidates are `<base>/wt-<topic>-target` and `<base>/wt-<topic>-clippy`,
with `<base>` your home directory. If yours live somewhere else, name them with `--cache-dir`
(repeatable) or adjust `--base` / `--prefix` / `--cache-suffix`. A run that matches nothing says
which paths it checked rather than reporting an empty plan as though you had no caches.

**The worktree is found the same way, and the search is wider than the cache one.** A bare
topic resolves to the first of these that exists: `<base>/wt-<topic>`, then `.worktrees/<topic>`,
`worktrees/<topic>` and `.claude/worktrees/<topic>` under the directory you run it from, which
covers both layouts Step 1b creates and the one the native worktree tool creates. Naming
`--base` confines the search to that base. Any other layout is reached by passing the worktree
path outright (`wtclean.py .config/wt/my-feature`) instead of the bare name.

Two consequences worth knowing before you trust a bare name:

- **The three project-local candidates are relative to the directory you run from**, not to the
  repository root, so running it from a subdirectory checks paths that do not exist and leaves
  `<base>` as the only real candidate. Run it from the directory that holds `.worktrees/` or
  `.claude/worktrees/`.
- **A bare topic matching more than one candidate is REFUSED**, naming each one, and no flag
  overrides it - `--discard-uncommitted` answers "delete it anyway", not "pick one for me".
  The refusal covers the CACHES too, unlike every other worktree refusal: cache candidates are
  built from the topic alone, so two checkouts sharing a name share them, and a run that could
  not resolve the name must not delete anything derived from it. A dirty or symlinked worktree
  is a RESOLVED one, so its caches are unambiguously its own and still go.
  A stale `<base>/wt-<topic>` beside a real project-local checkout is a question, and this is
  a delete with no undo, so answer it by passing the path you mean. When exactly one matches,
  the plan names it on its `would remove` line.

A search that finds nothing names every path it tried, which is the half that was missing when a
bare name reaching only `<base>` reported `nothing to remove` for a 513 MB checkout sitting in
`.claude/worktrees/`.

What it refuses, because a delete is not undoable. Every refusal below is decided at PLAN time,
so the dry run lists them as `REFUSED` rather than leaving them for `--apply` to discover. An
`--apply` can still report a `FAILED` on top of those - a removal that git or the filesystem
rejects is not knowable until it is attempted:

- **It is a dry run until `--apply`**, and `--apply` removes exactly what the plan listed - it
  does not re-scan, so a directory created after you read the plan is not swept up with it.
- **A worktree holding uncommitted or untracked work** is refused. `--discard-uncommitted`
  overrides that and forwards `--force` to `git worktree remove`, which DISCARDS the work.
- **A target that is a symbolic link** is refused: removing through a link can destroy data
  outside the directory you named. On Windows this does not cover a directory JUNCTION, which is
  not reported as a symbolic link; the "resolves outside the base" refusal is what covers that.
- **A parent reference anywhere in the argument** (`../../etc`, `wt-a/../../etc`) is refused
  outright, never normalised - taking the basename first would turn `../../etc` into the
  innocent-looking name `etc`. A path WITHOUT a parent reference is not refused: it names the
  worktree directly (relative to where you run from, like any path you type), which is what
  the third example above does. What has to be a bare name is the TOPIC, and the topic is the
  argument's last path segment with the prefix stripped - it is what the cache candidates are
  built from, so `.worktrees/wt-my-feature` still looks for `<base>/wt-my-feature-target`.

Exit codes: 0 = nothing blocked, 1 = something was refused or could not be removed, 2 = usage
error. `--json` emits the machine-readable envelope; warnings go to stderr.

## Quick Reference

| Situation                         | Action                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Already in linked worktree        | Skip creation (Step 0)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| In a submodule                    | Treat as normal repo (Step 0 guard)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Native worktree tool available    | Use it (Step 1a)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| No native tool                    | Git worktree fallback (Step 1b)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `.worktrees/` exists              | Use it (verify ignored)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `worktrees/` exists               | Use it (verify ignored)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Both exist                        | Use `.worktrees/`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| Neither exists                    | Check instruction file, then default `.worktrees/`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| Directory not ignored             | Add to .gitignore + commit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| Permission error on create        | Sandbox fallback, work in place                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| Tests fail during baseline        | Report failures + ask                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| No package.json/Cargo.toml        | Skip dependency install                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Returning to an OLD worktree      | `git status --porcelain` FIRST - a long-lived worktree can hold an abandoned prior op's dirty state; `git stash push -u` it. Never `commit -a` over it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| Sharing a build cache dir         | Give each worktree its OWN `CARGO_TARGET_DIR` (or equivalent). One shared incremental cache across trees with different sources serializes builds on the target lock, and makes the compiler link the OTHER worktree's crate and emit phantom errors that name real symbols - so it reads as a defect in the code in front of you and sends you editing correct code. THE TELL: grep your own worktree for the name the compiler SUGGESTS; zero hits means you are reading a sibling's build. (Measured: nine errors of the form `no method named X found, help: there is a method X_ns with a similar name`, where the suggested name existed in no file of the tree being built.) Use a compiler cache (sccache) for cross-tree reuse instead. |
| Worktree deleted, disk still full | Its per-topic build cache is still there - `git worktree remove` never touches it. `uv run scripts/wtclean.py <topic> [--apply]` (Step 4)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

## Common Mistakes

### Fighting the harness

- **Problem:** Using `git worktree add` when the platform already provides isolation
- **Fix:** Step 0 detects existing isolation. Step 1a defers to native tools.

### Skipping detection

- **Problem:** Creating a nested worktree inside an existing one
- **Fix:** Always run Step 0 before creating anything

### Skipping ignore verification

- **Problem:** Worktree contents get tracked, pollute git status
- **Fix:** Always use `git check-ignore` before creating project-local worktree

### Assuming directory location

- **Problem:** Creates inconsistency, violates project conventions
- **Fix:** Follow priority: explicit instructions > existing project-local directory > default

### Proceeding with failing tests

- **Problem:** Can't distinguish new bugs from pre-existing issues
- **Fix:** Report failures, get explicit permission to proceed

## Red Flags

**Never:**
- Create a worktree when Step 0 detects existing isolation
- Use `git worktree add` when you have a native worktree tool (e.g., `EnterWorktree`). This is the #1 mistake - if you have it, use it.
- Skip Step 1a by jumping straight to Step 1b's git commands
- Create worktree without verifying it's ignored (project-local)
- Skip baseline test verification
- Proceed with failing tests without asking

**Always:**
- Run Step 0 detection first
- Prefer native tools over git fallback
- Follow directory priority: explicit instructions > existing project-local directory > default
- Verify directory is ignored for project-local
- Auto-detect and run project setup
- Verify clean test baseline

