- name
- git-worktree
- description
- This skill should be used when managing Git worktrees for isolated parallel development. It handles creating, listing, switching, and cleaning up worktrees with a simple interactive interface.
<!-- grok-harness-invoke:start -->
**Grok Build (`plugins/soleur/lib/harness.ts` `invokeSkill()`):** Read this SKILL.md in this process and run it to completion. A one-segment `soleur:<name>` in this document names a SKILL — on Grok Build, Read `plugins/soleur/skills/<name>/SKILL.md` in this process; it is not a nested tool_use. A multi-segment id such as `soleur:<domain>:<name>` names an AGENT: spawn it, never Read it, and on Grok Build spawn_subagent takes the id with its colons replaced by hyphens (`agentIdToGrokSubagentType`). **Claude Code:** Skill tool for a skill (`soleur:<name>`), Task tool with `subagent_type` for an agent. Forbidden is executing a subset, not the Read.
<!-- grok-harness-invoke:end -->
# Git Worktree Manager
This skill provides a unified interface for managing Git worktrees across your development workflow. Whether you're reviewing PRs in isolation or working on features in parallel, this skill handles all the complexity.
## What This Skill Does
- **Create worktrees** from main branch with clear branch names
- **List worktrees** with current status
- **Switch between worktrees** for parallel work
- **Clean up completed worktrees** automatically
- **Interactive confirmations** at each step
- **Automatic .gitignore management** for worktree directory
- **Automatic .env file copying** from main repo to new worktrees
- **Write guard enforcement** via PreToolUse hook (`.claude/hooks/worktree-write-guard.sh`) -- blocks Write/Edit to main checkout when worktrees exist
## CRITICAL: 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. Detects bare repos (`core.bare = true`) and derives `GIT_ROOT` via `--absolute-git-dir` instead of `--show-toplevel`
5. Sources the shared `plugins/soleur/scripts/resolve-git-root.sh` helper -- all scripts that need `GIT_ROOT` should source this helper instead of inlining their own detection logic
**After creating a worktree**, run `npm install` if the project has a `package.json` — worktrees do not share `node_modules/` with the main working tree, and build commands (`npx @11ty/eleventy`, etc.) will silently hang instead of erroring.
```bash
# ✅ CORRECT - Always use the script
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" create feature-name
# ❌ WRONG - Never do this directly
git worktree add .worktrees/feature-name -b feature-name main
```
## When to Use This Skill
Use this skill in these scenarios:
1. **Code Review (`soleur:review`)**: If NOT already on the target branch (PR branch or requested branch), offer worktree for isolated review
2. **Feature Work (`soleur:work`)**: Always ask if user wants parallel worktree or live branch work
3. **Parallel Development**: When working on multiple features simultaneously
4. **Cleanup**: After completing work in a worktree
## How to Use
### In Claude Code Workflows
The skill is automatically called from the `soleur:review` and `soleur:work` skills:
```
# For review: offers worktree if not on PR branch
# For work: always asks - new branch or worktree?
```
### Manual Usage
You can also invoke the skill directly from bash:
```bash
# Create a new worktree (copies .env files automatically)
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" create feature-login
# List all worktrees
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" list
# Switch to a worktree
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" switch feature-login
# Copy .env files to an existing worktree (if they weren't copied)
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" copy-env feature-login
# Clean up completed worktrees
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" cleanup
```
## Commands
### `create <branch-name> [from-branch]`
Creates a new worktree with the given branch name.
**Options:**
- `branch-name` (required): The name for the new branch. The worktree DIRECTORY is the slug of it — every `/` becomes `-`, so `ci/rule-metrics` creates branch `ci/rule-metrics` in `.worktrees/ci-rule-metrics`. Identical for any name without a slash. Read the path the script prints; do not construct `.worktrees/<branch-name>` yourself. `create` refuses if the target directory already holds a different branch (`SOLEUR_WORKTREE_SLUG_COLLISION`) — the transform is many-to-one, so `ci/foo` and `ci-foo` would otherwise share one directory.
- `from-branch` (optional): Base branch to create from (defaults to `main`)
**Example:**
```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" create feature-login
```
**What happens:**
1. Checks if worktree already exists
2. Fetches `refs/remotes/origin/<from-branch>` (the local `<from-branch>` ref is NOT touched — this lets `create` succeed even when a sibling worktree has `<from-branch>` checked out; see #3741)
3. Creates the new worktree from `origin/<from-branch>` with `--no-track` (preserves the pre-fix upstream-unset state so downstream `git push -u origin <branch>` flows are unchanged)
4. **Copies all .env files from main repo** (.env, .env.local, .env.test, etc.)
5. Shows path for cd-ing to the worktree
**Opt-in: also update local `<from-branch>`**
Pass `--update-local-main` (as a global flag, before `create`) to additionally fast-forward the local `<from-branch>` ref. Default behavior leaves the local ref untouched.
```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" --update-local-main create feature-login
```
**Dependency install (bounded, skippable):**
After copying `.env` files, `create`/`feature` install dependencies at the root and for each `apps/*/` package (bun/npm/yarn by lockfile). Two guards keep this from hanging a pipeline on a restricted host:
- **Opt-out:** `--no-install` (global flag) or `SOLEUR_WORKTREE_SKIP_INSTALL=1` skips every install arm and prints `SOLEUR_WORKTREE_INSTALL_SKIPPED reason=opt-out`. The worktree is still created and usable; run installs inside it later.
- **Reachability preflight + timeout:** each arm probes its resolved registry endpoint (`curl`, bounded, on the scheme the registry actually uses — `http://` private registries included) and skips fast with `SOLEUR_WORKTREE_INSTALL_SKIPPED reason=registry-unreachable host=<host> endpoint=<endpoint>` when unreachable; a stalled install is killed after `SOLEUR_WORKTREE_INSTALL_TIMEOUT_SECS` (default 300) and reported as `reason=timeout`. Probe bounds: `SOLEUR_WORKTREE_REGISTRY_PROBE_SECS` (connect, default 5) / `SOLEUR_WORKTREE_REGISTRY_PROBE_MAX_SECS` (total, default 8).
- **Other non-success arms also mark stdout:** `reason=failed rc=<n>` (ordinary nonzero install), `reason=tool-missing runtime=<rt>` (lockfile present but package manager absent), `reason=no-lockfile` (package.json with no recognized lockfile). When the host lacks `timeout`/`gtimeout`, installs run unbounded and `SOLEUR_WORKTREE_INSTALL_UNBOUNDED` is printed once.
All skips and failures warn-and-continue: worktree creation exits 0 and the worktree stays on disk.
### `list` or `ls`
Lists all available worktrees with their branches and current status.
**Example:**
```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" list
```
**Output shows:**
- Worktree name
- Branch name
- Which is current (marked with ✓)
- Main repo status
### `switch <name>` or `go <name>`
Switches to an existing worktree and cd's into it.
**Example:**
```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" switch feature-login
```
**Optional:**
- If name not provided, lists available worktrees and prompts for selection
### `cleanup` or `clean`
Interactively cleans up inactive worktrees with confirmation.
**Example:**
```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" cleanup
```
**What happens:**
1. Lists all inactive worktrees
2. Asks for confirmation
3. Removes selected worktrees
4. Cleans up empty directories
### `cleanup-merged` and `space-report`
`cleanup-merged` runs at every session start: it removes worktrees whose branch merged, sweeps dead-owner scratch under `/tmp` and `/var/tmp` (ADR-250), and prints what it freed. `space-report` prints the same `SOLEUR_CLEANUP_SPACE` state line without running any cleanup (read-only).
| Variable | Default | Effect |
|---|---|---|
| `SOLEUR_QUARANTINE_DRAIN=1` | off (only the exact value `1` enables it) | Also delete scratch-quarantine entries past their TTL, inside the sweep's lock. Opt-in: a terminal delete on your machine. |
| `SOLEUR_SWEEP_QUAR_SCRATCH_TTL_MIN` / `SOLEUR_SWEEP_QUAR_WT_TTL_MIN` | 10080 (7 d) / 43200 (30 d) | Per-class TTL, minutes. |
| `SOLEUR_SWEEP_QUAR_TTL_FLOOR_MIN` | 1440 | Floor under both TTLs, so an exported `0` cannot empty a fresh quarantine. |
| `SOLEUR_SWEEP_DRAIN_TIMEBOX_S` / `SOLEUR_SWEEP_DRAIN_MAX_ENTRIES` | 5 / 200 | Checked between entries, per base. One very large entry can overrun them. A non-numeric value falls back to the default; a max of `0` means 200. |
| `SOLEUR_DOCKER_PRUNE=1` / `apply` | off | After a run that removed a worktree, prune Docker build cache and dangling images older than 24 h (`1` is a dry run, an upper bound). Never `-a`, volumes or containers. Any other value (including `0`) prints `skipped reason=invalid-value`. `SOLEUR_DOCKER_TIMEOUT_S` (60) bounds each call, so `apply` can take about 140 s. |
Markers (stdout): `SOLEUR_TMP_SWEEP` (adds `drained`, `drained_bytes`, `map_s`), `SOLEUR_DOCKER_PRUNE` and `SOLEUR_CLEANUP_SPACE logical_bytes=… df_delta_bytes=… fstype=… docker_prune=… snapshots=…`. A `SOLEUR_DOCKER_PRUNE skipped reason=` names every no-op: `invalid-value`, `no-worktree-removed` (the common one), `docker-missing`, `no-timeout`, `remote-daemon` (a `tcp://` or `ssh://` `DOCKER_HOST` is never pruned), `daemon-unreachable`, `timeout`, `df-failed rc=N` (dry run), `prune-failed rc=N`. Docker's own `Total:` text is embedded verbatim and is opaque. `logical_bytes` counts drained bytes only; `-` means no baseline was taken (`space-report`). `df_delta_bytes` is the signed change in free space on `/var/tmp` and can be smaller, or negative, because other processes write too and, on btrfs with snapper, snapshots keep deleted blocks pinned (`snapshots=snapper`; Soleur never deletes snapshots). `snapshots=none` is reported for any non-btrfs filesystem and for btrfs without a snapper config named `root`, so it also covers a differently named config or Timeshift.
The markers stay on your machine when you run the plugin yourself. On the Soleur web platform the same lines are mirrored to server logs like every other `SOLEUR_*` marker. The sweep needs `flock`, so on stock macOS the sweep and drain skip with `flock-missing`; the Docker step needs `timeout` or `gtimeout` and otherwise prints `skipped reason=no-timeout`, and without `findmnt` the report prints `fstype=unknown snapshots=unknown`. Set `SOLEUR_DOCKER_PRUNE` in your shell profile or the session environment.
### `sync-bare-files` or `sync`
Syncs stale on-disk files from git HEAD in a bare repo. Only needed when the repo uses `core.bare=true` — on-disk files at the bare root become stale after merges since git never updates them. Auto-called after `cleanup-merged` cleans branches in bare repo context.
**Example:**
```bash
bash "${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh" sync-bare-files
```
**What it syncs: EVERY tracked file at HEAD — there is no allowlist.**
The implementation builds a throwaway index from `HEAD` and runs
`git checkout-index -a -f` against the bare root, so every path git tracks is
materialized. That pass is **additive** — it never removes anything — so a
separate prune step afterwards deletes on-disk leftovers for paths that were
tracked once and are absent from HEAD. The two together are what make the bare
root equal HEAD exactly. It then re-applies execute bits for the scripts and
hooks the plugin loader and SessionStart hooks exec.
The prune is locale-independent (every `sed`/`sort`/`comm` stage runs under `LC_ALL=C`; a `comm`
in the user's locale once deleted tracked files, including `.claude-plugin/marketplace.json`) and
runs against the **bare root's** HEAD, pinned once, even when `sync` is started from a worktree on
another branch. Each candidate is checked against that commit right before removal, and a path under
a symlinked directory, a `.`/`..`/`.git` path, a quoted spelling, and (when the root is the git dir)
a top-level name git owns are skipped (best effort for that legacy `*.git` layout).
A candidate that HEAD still tracks aborts the **whole** prune with `ERROR: tracked path in prune set`
(nothing is removed; the first one found is named). A prune larger than `SOLEUR_SYNC_PRUNE_MAX` (default
5000, counted on paths about to be removed; a leading zero is read as decimal, an invalid value falls
back to 5000 with a `WARN`, `0` skips every prune) is skipped with a `WARN`. There are three skip
paths (tracked hit, ceiling, could not compute the candidate set), all exit 0 so `cleanup-merged`
completes, and the last line then reads `Synced on-disk files from git HEAD (prune skipped ...)`.
Nothing was deleted in that case; do not re-run, and report the `ERROR` path if one was printed.
A deleted file whose name holds a tab, newline, quote or backslash is never pruned (git quotes it).
> **Corrected 2026-08-10 (#7409).** This section previously enumerated a
> seven-entry list (`AGENTS.md`, `plugins/soleur/hooks/*`, `.claude/hooks/*.sh`,
> `resolve-git-root.sh`, …) and told readers that "any file Claude Code executes
> at runtime from the bare repo root **must be added to the sync list**". Both
> halves were wrong: the enumeration described a whitelist the code has not
> had, and the instruction sent readers to register files in a list that does
> not exist. Worth stating precisely rather than deleting, because the
> difference is load-bearing — when #7409 moved the session-state lock/lease
> library to `plugins/soleur/scripts/lib/`, a genuine whitelist would have left
> the bare root's repointed `.claude/hooks/*` sources pointing at a file the
> mirror never copied, and they degrade **silently** (`|| true`). The full
> mirror is what makes that a non-event.
**Still important:** run `sync-bare-files` after any merge that moves or adds a
file executed from the bare root. The mirror is complete, but it is not
automatic — on-disk files at a `core.bare=true` root are never updated by git
itself, so a stale copy survives until this runs.
If `sync-bare-files` fails with `checkout-index from HEAD failed` or `the bare root's HEAD does not
resolve (<ref>)`, check the bare root's `HEAD`: a PR merge that deleted the branch `HEAD` named leaves
it dangling, and `git -C <bare-root> symbolic-ref HEAD refs/heads/main` repairs it (name the root with
`-C`: run bare from a worktree it would re-point that worktree). The Claude Code and Devin SessionStart loader does this
automatically (`.claude/hooks/README.md`, "Bare-root HEAD self-heal"); other harnesses do not.
## 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
Ver en GitHub