Skip to content
Back to skills

Worktree

ASecurity

When the built-in EnterWorktree or ExitWorktree tool resolves in this session, prefer ExitWorktree with action keep to leave a worktree; this skill to create one, to enter an existing one (claim check, then EnterWorktree by path), and to inventory, clean up, or audit worktrees. Manage git worktree lifecycle for parallel-session isolation: create (external root, then enter), status (PR + staleness inventory), cleanup (file-lock-aware removal), audit (infrastructure health). Use when: 'create w...

  • 13 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 2, 2026
ai-agentsgoshellbashgitapi

Works with

  • claude code
  • terminal
  • cli
  • api
  • mcp

Security analysis

A100/100

Pro scans all 11 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add melodic-software/claude-code-plugins --skill worktree --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Worktree?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Worktree
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/melodic-software-worktree/badge)](https://www.skillsdirectory.com/skills/melodic-software-worktree)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: "When the built-in EnterWorktree or ExitWorktree tool resolves in this session, prefer ExitWorktree with action keep to leave a worktree; this skill to create one, to enter an existing one (claim check, then EnterWorktree by path), and to inventory, clean up, or audit worktrees. Manage git worktree lifecycle for parallel-session isolation: create (external root, then enter), status (PR + staleness inventory), cleanup (file-lock-aware removal), audit (infrastructure health). Use when: 'create worktree', 'worktree status', 'clean up worktrees', 'orphaned worktrees', or proactively when on main before writing code, not for PR lifecycle (use /pull-request)."
user-invocable: true
disable-model-invocation: false
argument-hint: "[create|status|cleanup|audit] [args]"
metadata:
  workflow-stage: session
  summary: Create, inspect, and clean git worktrees for parallel sessions
---

**Arguments.** `[create|status|cleanup|audit] [args]`. e.g., /worktree create feat/my-feature, /worktree status, /worktree cleanup, /worktree audit

## Repository context. Gather first

Collect these with **individual** Bash calls, one command per call, never combined into a single
invocation:

- Current branch, `git branch --show-current`
- Worktree inventory, `git worktree list | head -30`
- Git dir. `git rev-parse --git-dir`
- Git common dir (differs from the git dir when in a linked worktree).
  `git rev-parse --git-common-dir`

The pipe is the bound and belongs in the command. A read-time cap ("read only the first 30
entries") bounds nothing: the Bash tool returns the command's complete output into context before
there is anything to decide about. Treat a failure (not a repository, git unavailable) as an unknown
value and carry on. Keep these as separate body calls rather than pre-compute: the harness composes
a pre-computed block into one shell invocation, and a worktree-isolated agent refuses a git-bearing
compound command, which would make this skill uninvocable from inside a worktree. The dated record
for the composition half is
[reference/gather-block.md](reference/gather-block.md), "The pre-compute block runs as one shell
invocation".

That refusal is documented behavior, not a quirk of one release, so the constraint is durable. Per
[worktrees](https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation) (fetched
2026-09-27, quoted with link markup removed), "Claude Code applies four checks": file edits into the
main checkout, a command whose **working directory** resolves there, a **git redirect** into it
("The redirect can come through `git -C`, `--git-dir`, a `GIT_DIR` or `GIT_WORK_TREE` variable, or a
`cd` into the main checkout before running git."), and the **command shape**. The last is the one
that bites a compound command, and it fails closed: "Claude Code blocks a Bash or Monitor command
when it can't verify from the command text that any git the command runs stays inside the
worktree." A block is therefore not evidence the command *would* have reached the main checkout, an
unverifiable one is refused on the same footing, which is exactly what a multi-command shell
invocation looks like. The page's own remedy is the one this skill takes: "Claude Code tells Claude
how to rewrite the refused command, such as splitting it into plain, separate commands." The same
page adds two facts worth holding: "The same enforcement covers every subagent Claude spawns from
the isolated session. It applies whether the session is interactive or runs in the background.", so
a delegated worker inherits it rather than escaping it; and "For PowerShell commands, Claude Code
applies only the working-directory check", so PowerShell is narrower coverage, never a sanctioned
route around the git-redirect or command-shape check.

## Purpose

Orchestrate git worktree lifecycle from creation through cleanup. **Front-half** of the development workflow. Gets you into a worktree and keeps them healthy. `/source-control:pull-request` is the **back-half**. Handles prep, PR creation, monitoring, merge.

**Why this exists:** worktrees are the isolation mechanism for parallel code changes. Multiple Claude Code sessions on different tasks without stepping on each other. In repos where branch protection blocks direct commits to main, every feature, fix, or refactor starts with a worktree or branch; this skill makes that seamless.

This skill is the canonical owner of the parallel-session worktree convention **for this plugin fleet**, and [§ The nesting invariant, dated measurement](#the-nesting-invariant-dated-measurement) is the one site that states the mechanism, every other surface in this plugin points here instead of restating it. That is an ownership claim, not a census: consumer docs outside this repository also describe worktree placement, at least one of them written more recently than the as-of date below. So the ownership comes with an inbound channel, **a consumer that measures something contradicting that section should open an issue on this plugin's tracker so the owner is corrected here.** Canonical ownership with no back-channel just makes the owner the last to know.

Worktrees live at an external `worktree_root` (`<root>/<owner>-<repo>-<slug>`, outside every repository), defaulting to `<plugin-data-dir>/worktrees` when the key is unset, and never nested inside any repository's tree, that nesting invariant is what creation enforces, on the dated measured basis recorded below rather than as a standing absolute. Keeping `worktree_root` clear of repository-discovery roots (such as a ghq root) is convention, not machine-checked: creation rejects only paths inside an existing repository, so a root you point at a discovery tree still pollutes repository enumeration. `ghq list` reports each worktree as a repository of its own, and a leading dot does not hide it. Choose a configured root accordingly. The default is already clear of this: the plugin data directory is harness state, never a checkout and never inside a discovery tree, which a checkout-relative default would be under a layout like `<root>/github.com/<owner>/<repo>`.

## Adapting to your environment (graceful degrade)

This skill is self-contained, every action runs on plain `git`, plus `gh` for PR cross-referencing where available. Where it mentions an adjacent capability (an issue tracker, a build/lint verifier, a session-start setup hook), treat it as optional: use it when your environment provides it, proceed without it otherwise. Project-specific conventions, branch naming, worktree layout, which gitignored files a fresh worktree needs, come from the consuming project's own `CLAUDE.md`, rules, and hooks; read them before creating or removing anything.

## Arguments

`$ARGUMENTS`. Action selector. Parse first token as action, remainder as arguments.

| Action | Entry point | Use case |
|--------|-------------|----------|
| *(empty)* | Smart default | Detect current state, suggest appropriate action |
| `create [name]` | Create worktree | Validate name, explain setup, call EnterWorktree |
| `status` | Inventory | List all worktrees with PR status and staleness |
| `cleanup [--dry-run]` | Remove stale | Prune orphans, detect merged PRs and stale locks, remove with confirmation |
| `audit` | Health check | Run status + verify configuration health |

---

## Action: Smart Default (empty args)

Detect current state and guide user to the right action.

1. **Check git repo**: `git rev-parse --is-inside-work-tree`. If not in a repo → "Not in a git repository."

2. **Detect current branch**: `git rev-parse --abbrev-ref HEAD`

3. **Check if in a worktree**: `git rev-parse --git-dir` differs from `git rev-parse --git-common-dir` in any linked worktree. Common layouts: a `.worktrees/` directory sibling to the repo's `.claude/`, Claude Code's default `.claude/worktrees/`, or a bare-clone hub (`git rev-parse --git-common-dir` ends in `.bare` and worktrees are siblings of `.bare/`).

4. **Branch-based guidance**:

   - **On the default branch** → "You're on `<default-branch>`. Create a branch (`git checkout -b <type>/<description>`) or use `/source-control:worktree create` if you need parallel session isolation."
   - **In a worktree** → Show current worktree info: branch name, last commit, associated PR (via `gh pr list --head <branch> --json number,title,state`). If a PR exists, suggest the next `/source-control:pull-request` phase.
   - **On a feature branch (not worktree)** → Show branch info and any associated PR.

5. **Check for stale/prunable worktrees**: Run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/lib/worktree-facts.sh" list <repo>` and read its `prunable` column. If any worktrees are prunable or branches have merged PRs → suggest `/source-control:worktree cleanup`.

6. **Otherwise** → Show brief status summary (worktree count, any needing attention).

---

## Action: `create [name]`

Create a new worktree with guided naming and setup verification. Full procedure. Pre-flight guards (already-in-worktree, mid-session transition), name validation (EnterWorktree schema constraints), base-ref notes, the explain-before-create block, directory-rename caveats, and post-create setup checks: [context/create.md](context/create.md).

**Safety invariants create MUST honor** (full detail in context/create.md):

- **Create via the shared helper, not `EnterWorktree(name:)`.** `${user_config.worktree_root}` substitution into skill content is raw text, not shell-escaped, so it must never reach a shell parser, a value containing `'`, `$`, or a backtick breaks a quoted `--root` literal, and one containing the delimiter line ends a heredoc early. Instead write it to a temp file with the `Write` tool (JSON string parameter, never shell-parsed) and hand the file to the helper's `--fallback-root-file` flag (plugin-option rung, below `worktreeroot.path`); full render in [context/create.md § Create the worktree](context/create.md#create-the-worktree). The helper places the worktree at the external root (`<root>/<owner>-<repo>-<slug>`), copies `.worktreeinclude` files, and prints the path. `EnterWorktree(name:)` lands in the in-repo `.claude/worktrees/`, the placement [§ The nesting invariant, dated measurement](#the-nesting-invariant-dated-measurement) exists to avoid.
- **On a non-zero helper exit, STOP, never fall back to `EnterWorktree(name:)`.** An unset `worktree_root` is not an error when another rung resolves: the helper may use `worktreeroot.path` or fall back to `<plugin-data-dir>/worktrees` from the value below and notes it on stderr, still exiting 0. Exit 3 means no usable root, neither configured nor supplied, a resolved root the containment guard rejects for landing inside a repository, **or** (on Windows) a root on a different drive from the repo, including the unconfigured plugin-data-dir default. Surface the helper's guidance and stop; a silent in-repo fallback is the nesting regression this closes.
- **Scripts directory (resolved): `${CLAUDE_PLUGIN_ROOT}/scripts`.** The `context/` files call it `<scripts-dir>`; substitute this resolved absolute path before any command from them reaches Bash. Claim: `${CLAUDE_PLUGIN_ROOT}` expands in this file but stays literal in a `context/` file read via `Read`, and the Bash tool's environment does not carry it (a command with the literal token exits 127 from `/scripts/...`). Basis: two headless `claude -p` runs on a disposable `--plugin-dir` plugin, which quoted the token back from the skill body and from a context file, then ran the context file's Bash line. As-of: 2026-09-29, Claude Code 2.1.284. Recheck trigger: a Claude Code release note that changes plugin-variable substitution or the Bash tool environment, or a run of the same probe showing the token expanded in a context file.
- **Plugin data directory: `${CLAUDE_PLUGIN_DATA}`**, THIS FILE is the only surface where that token expands (a `context/` file is read as raw bytes and would carry it literally, and a Bash-tool subprocess's environment copy is not per-plugin). Carry the resolved path and hand it to the helper's `--data-root-file` flag through the same `Write`-tool temp file channel as the root above, so an unconfigured `worktree_root` still resolves to a location outside every repository. If the token ever arrives unexpanded, the helper detects it and refuses rather than creating a literally-named directory.
- **Enter with `EnterWorktree(path: "<printed-path>")` as the final action**, working directory changes and session state transitions on that call, so nothing may execute after it. The out-of-`.claude/worktrees/` path prompts for approval (not suppressible outside `bypassPermissions`).

**Orchestrated (autonomous) provisioning does not use this action.** An autonomous orchestrator that must stay resident to keep dispatching, e.g. `/work-items:work`, cannot invoke `create`: the `EnterWorktree` terminal above would transition the orchestrator's own session and end its ability to orchestrate. Such a run provisions **non-interactively** instead, the dispatched worker runs the shared `worktree-create.sh` helper directly (its output contract prints the path; the caller simply omits the `EnterWorktree` step) or a plain `git worktree add` followed by `scripts/worktree-claim.sh claim <path>` (the PostToolUse hook does this for Bash-tool adds, claiming only the parsed target), then works the worktree via `git -C <path>` **without entering it**. When that direct invocation runs from outside the repository, add `--repo-dir <repo-toplevel>` to the flags the interactive path already passes (`--name`, `--fallback-root-file`, `--data-root-file`; see context/create.md). `--repo-dir` alone is not a complete command: with `worktreeroot.path` unset, the helper obtains the plugin-data default only through `--data-root-file` and otherwise exits 3. Also pass `--base-ref head` when the effective `worktree.baseRef` setting is `head`; an omitted `--base-ref` defaults to `fresh`. The dispatching orchestrator (`/work-items:work`) owns that end-to-end worker-side lifecycle.

**Pass this session's id on every direct helper invocation.** `${CLAUDE_SESSION_ID}` expands in this file. The helper's lock is this session's claim only when `--session-id` carries that resolved value, because the reason then contains `session <id> since`, the token `check-enter` matches. If the token is still literal, stop and do not call the helper: the helper treats an unexpanded value as a usage error and creates nothing. [context/create.md](context/create.md) receives the resolved value, not the token, because a context file is read as raw bytes.

**Before writing in an existing worktree, run the claim gate.** A helper-created tree already carries a lock reason; a plain `git worktree add` may not. Before `git -C <path>` writes, or before `EnterWorktree(path:)` into a tree this session did not just create:

```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/worktree-claim.sh" check-enter <path> --session-id "${CLAUDE_SESSION_ID}"
```

`${CLAUDE_SESSION_ID}` is the skill-markdown substitution (same token as `plugins/context-guard/reference/reader-contract.md`). Pass it explicitly: the PostToolUse hook records the payload `session_id` in the lock, and the Bash environment does not independently export `CLAUDE_SESSION_ID`. Without `--session-id`, `check-enter` cannot prove ownership and treats even this session's claim as foreign. If the literal `${CLAUDE_SESSION_ID}` survives unexpanded, do not guess an id. Surface that and stop.

Exit 4 prints `FOREIGN CLAIM: <reason>`. Stop; another session holds a live claim. Exit 3 prints `UNCLAIMED`. Claim it (`claim <path> --session-id "${CLAUDE_SESSION_ID}"`) or leave it. Exit 0 is this session's claim, or a path that is not a linked worktree. `report` lists every linked worktree that still has no reason. The lock does not block writes; the reason is the claim other agents can read.

---

## Action: `status`

Inventory all worktrees with PR association, staleness detection, and a **stranded-work axis**. Collect Tier-0 facts with `bash "${CLAUDE_PLUGIN_ROOT}/scripts/lib/worktree-facts.sh" list <repo>` (do not parse porcelain by hand) plus one batched `gh pr list` and last-commit dates, and one run of `${CLAUDE_PLUGIN_ROOT}/scripts/landed-work.sh` per repository. Then apply the two-axis classification, staleness threshold (14-day default; the configured override is `${user_config.worktree_stale_days}`), the reap-age hours `cleanup` reads (48 default; `${user_config.worktree_reap_after_hours}`), and presentation schema per [context/status.md](context/status.md). `audit` Step 1 invokes this logic internally.

The **Work** axis answers a question age and PR state cannot: whether removing a worktree would destroy a commit. It is classified first and outranks the rest, so a worktree holding unpushed unlanded commits is `stranded`, never merely `stale`. An unprovable verdict reports `unknown` and is treated exactly as `stranded`, the engine reports `?` rather than `no` so that an ambiguity is never read as safe.

---

## Action: `cleanup [--dry-run]`

Remove stale worktrees, orphaned metadata, branches from merged PRs, and the project-scope plugin install records the worktree leaves behind. Full 5-step procedure. Prune orphaned metadata → identify candidates (detection reasons: orphaned dir / prunable / PR-merged / stale / stale lock / reap age, the last proposing an already-safe worktree older than `${user_config.worktree_reap_after_hours}` hours, 48 by default) → present → execute (4a release file locks, 4b guards → reap records → remove, 4c emit branch deletion for the user) → verify physical deletion: [context/cleanup.md](context/cleanup.md). `--dry-run` reports candidates and takes no action.

**Safety invariants cleanup MUST honor** (full detail in context/cleanup.md):

- **Never remove a worktree, and never emit its `git branch -D`, while its work is stranded or unproven**, a worktree whose unpushed commits are not already on the base loses them, and `unknown` is treated exactly as `stranded`. Both sites are guarded because removal itself is recoverable (the branch ref survives) while the branch deletion one step later is not; a detached-HEAD worktree is the exception where removal alone is already terminal. The override is `--acknowledge-stranded`, per worktree, never a bare `--force`, which answers git's dirty-tree check, a different question. Offer `git -C <path> push -u origin HEAD` first: it makes the commits durable without anyone judging whether the work matters.
- **Release OS file locks BEFORE `git worktree remove --force`** (Step 4a), on Windows `--force` unregisters the worktree from git but leaves a husk on disk if a process holds a file handle. Stop build servers (`dotnet build-server shutdown`, Gradle `--stop`, or your stack's equivalent) and worktree-rooted daemons/MCP servers first; stop ONLY those (never another live worktree's processes).
- **Reap the worktree's project-scope plugin install records BEFORE removing its directory, and only ever on a teardown this action is performing** (Step 4b). Claude Code keys a project-scope install to a literal `projectPath` and never reaps it when that path goes away, so a torn-down worktree leaves one record per installed plugin behind forever. Run `${CLAUDE_PLUGIN_ROOT}/scripts/reap-project-plugin-records.sh --worktree-path <path>` **from inside** the candidate, after the stranded-work and carried-file guards clear: `claude plugin uninstall -s project` has no path flag and resolves strictly against the current directory. **Never trigger a reap on bare path non-resolution.** A record for a live repository on an unmounted share or a detached volume looks identical to a dead worktree, and destroying it is unrecoverable. An **orphaned-directory** candidate is the strictest case, because it is the only class with no stranded-work row to read: it must pass every test for its class. An `empty` row needs *not a symlink*, *not a work tree*, *no `.git` entry*, and *empty*. A `husk` row needs *not a symlink*, *not a work tree*, a `.git` file naming `<common>/worktrees/<name>` whose admin dir is gone while `<common>` is still a repository, and *no entry but that file*. The `.git` test is the one that matters and the first does not imply it. A live worktree whose main clone was moved, deleted, or unmounted still carries its `.git` file while `rev-parse` fails, and has no `<common>` to show, and the external root is shared across repositories, so such a directory is another lane's live work: it is `unknown`, never a husk. Pre-existing orphans are `audit`'s to report, not this action's to remove.
- **A worktree's lock is its owner's claim, and reap age never overrides it.** Every helper-created worktree is locked when created. A locked worktree past the reap age is proposed only when `worktree-claim.sh stale <path>` exits 0, the same proof a **Stale lock** needs; otherwise it stays locked and is reported. Unlock and removal wait for the Step 4 confirmation, never `--force --force`.
- **A reap's non-zero exit is a no-op to report, never an escalation.** The CLI's failure text for an id with no project-scope record here suggests `--scope user`, which would uninstall the plugin fleet-wide. Never run `-s user`, never `--prune`, and never hand-edit `installed_plugins.json` (Claude Code's internal state, not a published contract).
- **Never swallow removal stderr** (`2>/dev/null`), a failed removal must surface so Step 5 reports husks honestly rather than counting one as removed.
- **Emit `git branch -D` + self-worktree removal for the USER to run, never inline** (Step 4c). Deleting a branch is destructive (and the consuming project's hooks may block it mid-session); a worktree can't delete itself (the running Claude Code session holds its handle). `-D` (not `-d`) is needed because squash-merge changes the SHA.

---

## Action: `audit`

Periodic health check for worktree infrastructure. Suitable as a recurring work item in your tracker. **Step 1:** run the `status` action internally, flagging any worktree whose Work axis is any value other than `safe` (see [context/status.md](context/status.md) for the closed mapping, do not re-enumerate here) and any whose Status shows an issue (stale, merged-not-cleaned, prunable, locked). Stranded work leads the findings, it is the only class where doing nothing is safer than acting. The Step 2 configuration-health checklist (`delete_branch_on_merge`, the `worktreeroot.path` conformance doctor, gitignored-file propagation), the Step 2b orphaned-plugin-install-record report, the Step 2c root scan of unregistered directories under the worktree root (`${CLAUDE_PLUGIN_ROOT}/scripts/worktree-root-scan.sh`; `empty` and a `husk` holding only its `.git` file go to `cleanup`, the other classes are reported only), and the Step 3 findings presentation: [context/audit.md](context/audit.md).

**Step 2b reports; it never reaps.** Records left by worktrees removed without the reap step (by hand, by another tool, or by an older cleanup) are not reachable by that step, so `audit` makes them visible, in four buckets: *live here*, *live elsewhere*, *candidate orphan*, and *other project records* (listed for information only with no remedy, because this plugin owns worktree lifecycle and nothing else). **The *live elsewhere* bucket is load-bearing and is the one an implementation drops:** the worktree root is shared: one root at `<root>/<owner>-<repo>-<slug>` serving every repository, so "not in *this* repository's `git worktree list`" is true of every other repository's live worktree under it. Registration is scoped to one repository; a liveness test (`git -C <path> rev-parse --is-inside-work-tree`) is not, and both are required before anything is called an orphan. Removing an orphan needs its directory recreated first, which is a deliberate user act; the audit emits the commands and stops. Reaping on bare path non-resolution is exactly what an unmounted volume looks like, and is never done.

---

### The nesting invariant, dated measurement

**This section is the sole owner of the mechanism claim.** Every other statement of it in this plugin is a pointer here. It is a *dated measurement*, not a standing fact. Read the expiry below before relying on it.

The eager double-load this invariant was originally written against, CLAUDE.md, commands, agents, and rules all loading twice from a nested worktree, was fixed upstream in Claude Code v2.1.69, so that basis no longer holds. What replaces it, measured on 2.1.224: from a session inside a nested worktree, a read matching a `paths:` glob emits one `path_glob_match` naming the **parent** checkout's rule file, loading it alongside the worktree's own copy, both charged at roughly their own size. The same read from an externally-placed worktree emits zero such events. **That 2.1.224 measurement is disputed, not refuted:** a counter-reproduction on 2.1.227 did not observe the leak, and neither original run disclosed its fixture. An authenticated run of `fixtures/nesting-invariant-probe.sh` on **2.1.285** (2026-09-30) adjudicated the current CLI: the leak **does not reproduce**. From a dot-nested and from a plain-nested worktree, the read emits no `path_glob_match` for the owning parent checkout's rule file, and the external control emits none, with 5 `InstructionsLoaded` events on each arm (no fixture failure). Whether the leak was real on 2.1.224 cannot be re-run on that version and stays disputed, not refuted. See `fixtures/README.md`.

**Three control arms narrow what the invariant rests on.** On 2.1.285 the owning-parent leak is absent on both the dot-nested and the plain-nested placement, so placement relative to the worktree's own parent no longer changes what loads. A worktree nested inside an **unrelated** repository still picks up that repository's rules: its scoped rule loaded via `path_glob_match`, and on every arm the enclosing checkout's `CLAUDE.md` and unconditional rules loaded at `session_start`, including the external arm, whose workdir sat inside an unrelated checkout. Session-start ancestor traversal therefore reaches a **different** repository, which is the surface the placement convention now rests on, so a change to it is a recheck trigger in its own right. **Arm-by-arm status**, so a fix to one arm does not silently weaken another:

| Arm | Status |
|---|---|
| the owning-parent leak exists at all | **not reproduced on 2.1.285** (dot-nested, plain-nested and external all show no owning-parent rule); the 2.1.224 claim stays disputed, not refuted |
| not specific to `.claude/worktrees/` | **moot on 2.1.285**: dot-nested and plain-nested behave identically (no owning-parent leak) |
| **nested in an unrelated repo is worse** | **supported for scoped rules on 2.1.285**: the enclosing repository's scoped rule loaded via `path_glob_match`. The `session_start` half is not isolated by that run: the fixture's unrelated repository had no `CLAUDE.md` or unconditional rule |
| lazy, not eager (no triggering read → no load) | untested |

Basis: an `InstructionsLoaded` hook trace, which names loaded files rather than inferring them from token deltas, passed via `claude -p --settings <file>` because project-scope hooks in an unapproved `settings.json` do not run headlessly. **The fixture is the adjudicator**. Creation mechanism, launch mode, the exact `paths:` glob and its anchoring root, and whether the parent's rule file was committed all change the outcome, and none of them was recorded for either original run. `fixtures/nesting-invariant-probe.sh` fixes all of them and is the recheck procedure; `fixtures/README.md` records what it has and has not established, run by run.

On hook registration: use the `args`-array **exec form** here, per <https://code.claude.com/docs/en/hooks> (raw markdown, fetched 2026-08-11). "Set `args` whenever the hook references a path placeholder, since each element is passed as one argument with no quoting." Both forms are documented with no event-specific carve-out, and this plugin's own `hooks/hooks.json` registers its hooks in the single-string form and they fire. Still unprobed, and therefore stated as unknown: whether the single-string form fires for an `InstructionsLoaded` hook supplied via `claude -p --settings <file>`.

Upstream coverage: [#16600](https://github.com/anthropics/claude-code/issues/16600) is the live issue. OPEN, labeled `enhancement` and `memory`, asking that memory traversal respect worktree boundaries. It concerns **memory files**; the same trace found those handled correctly on 2.1.224, so the surface still leaking is path-scoped rules, which no open upstream issue covers. That "handled correctly" is a **null result from this same trace**, not a release-note fact, no 2.1.224 changelog line covers memory, worktree, or rule loading, and that changelog scan is packet-sourced and has not been re-run.

**Verification stamp** ([upstream-drift convention](https://raw.githubusercontent.com/melodic-software/claude-code-plugins/main/docs/conventions/upstream-drift/README.md)), as-of **2026-09-30**, last adjudicated measurement on **2.1.285**, from an authenticated `fixtures/nesting-invariant-probe.sh` run (Linux, first-party login):

- **Recheck triggers (event).** A Claude Code release note naming worktree rule-file loading or path-scoped rule resolution; `#16600` changing state; or the suppression rule above changing, since the placement convention rests on it.
- **Unconditional expiry.** **2.1.305, or 2026-12-29. Whichever comes first.** Both event triggers are known to be incapable of firing on their own: `#16600` has not changed state since well before this as-of date, and an opaque release stanza ("Bug fixes and reliability improvements", 2.1.226) cannot fire an event-keyed trigger at all. An expiry is the only trigger that fires without upstream cooperation. On expiry, run `fixtures/nesting-invariant-probe.sh` under an **authenticated** CLI and refresh this stamp with the outcome. Drift or no drift. A zero-event run is a fixture failure, not a null.

## Boundary, the built-in `EnterWorktree` and `ExitWorktree` tools

When built-in worktree tools resolve in this session, a request to "make a worktree" can reach
either a tool or this skill.

- **`EnterWorktree` (built-in tool)**: creates a worktree by name in the in-repo
  `.claude/worktrees/` and switches the session into it, or, given a `path`, switches into an
  existing worktree. The model invokes it; the person does not.
- **`ExitWorktree` (built-in tool)**: leaves a worktree session and restores the original
  directory. `action: "keep"` leaves the worktree and its branch on disk; `action: "remove"`
  deletes both, but the tool will not remove a worktree that was entered by `path`.
- **This skill (marketplace plugin).** Creates the worktree at an external root through the
  shared helper, then enters it with `EnterWorktree(path:)`; checks the session claim before
  entering an existing worktree; owns `status`, `cleanup`, and `audit`.

**Routing.** When `ExitWorktree` resolves in this session, use it with `action: "keep"` to leave a
worktree. Use this skill to create a worktree, and never `EnterWorktree(name:)`, which places it
inside the repository (see the nesting invariant above). Entering a worktree that already exists
also goes through this skill: run the claim gate above (`check-enter`), then, when
`EnterWorktree` resolves in this session, call `EnterWorktree(path:)` as the final action.
Removing a worktree is `cleanup`: this skill enters by `path`, and `ExitWorktree` will not remove
a worktree entered that way.

**Mutation gate.** `EnterWorktree` and `ExitWorktree` change the session's working directory, so
each is the final action of its step. `ExitWorktree` with `action: "remove"` deletes a worktree
and its branch, so this skill never passes it. Only `cleanup` deletes a worktree, under its own
confirmation.

**Availability is never assumed.** This section states what to do when a tool resolves in this
session, never that it is present. The four-part records live in
[reference/native-worktree.md](reference/native-worktree.md).

## What this skill does NOT do

- **Does not push, create, merge, or close PRs**, `/source-control:pull-request` owns the back-half (prep, create, monitor, merge).
- **Does not commit or stage code**, staging and committing stay user-controlled; `/source-control:commit` owns the commit mechanic.
- **Does not run CI, build, test, or lint**, use your project's build/test/lint tooling or skills.
- **Does not manage remote branches**, GitHub's `delete_branch_on_merge` handles remote cleanup on merge (when enabled); local `git branch -D` is emitted for the user, never run inline.
- **Does not uninstall plugins, and does not touch user or local scope.** `cleanup`'s reap removes only the *project-scope install records keyed to the worktree it is tearing down*, through `claude plugin uninstall -s project`. It never runs `-s user` or `-s local`, never passes `--prune`, and never edits `installed_plugins.json`. Fleet-wide plugin state is a plugin-management concern, not a worktree one.
- **Does not enforce branch naming**, the consuming project's hooks and CI are the gates. This skill only surfaces the project's convention (read it from the project's `CLAUDE.md` / rules; default suggestion: `<type>/<kebab-description>` with a Conventional Commits type prefix).

## Integration Points

This skill complements other workflow components. It does not duplicate their logic.

| Component | Relationship |
|-----------|-------------|
| `/source-control:pull-request merge` (Phase 4) | Handles post-merge cleanup as part of PR lifecycle. `/source-control:worktree cleanup` is the standalone version for ad-hoc or batch cleanup |
| `/source-control:pull-request create` (Phase 2.1) | Detects default-branch checkout and suggests `/source-control:worktree create` |
| Project session-start hooks (if any) | May warn on main or auto-configure fresh worktrees; this skill verifies setup ran per context/create.md's post-create checks |
| Recurring maintenance tracker items | Can invoke `/source-control:worktree audit` periodically |

## Graceful Degradation

- **`gh` CLI unavailable or fails**: `status` and `cleanup` work with git-only data. PR cross-reference and the `delete_branch_on_merge` check are skipped with note: "GitHub API unavailable. PR status unknown."
- **Not in a git repo**: All actions exit immediately with "Not in a git repository."
- **`worktree_stale_days` invalid or unexpanded**: Falls back to 14-day default silently (treat a literal `${user_config.worktree_stale_days}` token as unset).
- **`worktree_reap_after_hours` invalid or unexpanded**: Falls back to 48 hours silently (treat a literal `${user_config.worktree_reap_after_hours}` token as unset).
- **No worktrees exist**: `status` reports "No linked worktrees found." `cleanup` reports "Nothing to clean up."

Files in this skill

  • SKILL.md28.8 KB
  • context/audit.md11.7 KB
  • context/cleanup.md24.8 KB
  • context/create.md17 KB
  • context/status.md7.5 KB
  • evals/evals.json15.1 KB
  • fixtures/README.md15.6 KB
  • fixtures/nesting-invariant-probe.sh8.8 KB
  • fixtures/project-scope-reap-probe.sh5.8 KB
  • fixtures/worktree-create-hook-probe.sh3.9 KB
  • nesting-invariant-ssot.test.sh8.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…