# Worktree

> Worktree — isolate parallel work without clobbering

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

---


# Worktree — isolate parallel work without clobbering

A worktree is a second checkout sharing the same `.git/` history but with its
own `HEAD`, branch, and working files. Edits in one **never** touch another —
that isolation is the whole point, and the source of every gotcha below. The
`description` above is the always-loaded *when*; this body is the *how*. The
user should never have to instruct either.

> Re-verified against the live Claude Code worktree docs + empirical repo tests
> on 2026-06-14. Docs change under us — see §5; treat the live docs as truth
> over this file.

## 0. Is a worktree actually warranted?

Make one when work could collide on shared files — a second concurrent session,
parallel independent tasks, or file-mutating agents running beside other work.
**Do not** wrap a solo single-task session: a worktree you don't need just buys
untracked noise and a merge-back tax. When unsure whether two efforts can
collide, make it.

## 1. Create / enter

- **This session into its own worktree:** call `EnterWorktree` with a short
  `name`. It creates `.claude/worktrees/<name>/` at the repo root, on a new
  branch **`worktree-<name>`**, and switches the session's working dir into it.
  From inside a worktree you cannot nest another — only switch into an existing
  one via `path`, and that path must be under `.claude/worktrees/`.
- **`EnterWorktree` is UNAVAILABLE under a pinned cwd / from a subagent** — you can't move yourself in; spawn an `isolation:worktree` Agent for file-mutating steps instead (full mechanics in §6, *When EnterWorktree is blocked*).
- **A separate parallel session:** the user runs `claude --worktree <name>` in
  another terminal, or `git worktree add ../<dir> -b <branch>` then `claude`
  there.
- **Base ref:** new worktrees branch from **`origin/HEAD`** by default — a clean
  tree matching the remote, NOT your local uncommitted work. To carry unpushed
  local commits instead, set `worktree.baseRef: "head"` (project-level
  `.claude/settings.json`, or your account config). This repo's account config sets
  `worktree.baseRef: "fresh"` (the default — branch from `origin/<default-branch>`).

## 2. The gotchas (this is why the skill exists)

- **Results do NOT auto-merge.** Changes live on `worktree-<name>`, isolated.
  Reaching `main` is deliberate: review the diff, open a PR (ONE TRUNK, kernel
  prime directive 6). Never assume worktree work has reached `main`.
- **`state/` is gitignored and per-checkout, but the `harness` CLI resolves to the
  MAIN ledger.** `state/*` is not copied into a worktree. As of follow-up 1d30be,
  `bin/harness` resolves `state/` to the MAIN checkout via `_resolve_state_dir()`
  (git `--git-common-dir`), so `./bin/harness predict|outcome|corrections|followup|gc`
  run *inside* a worktree write to the ONE canonical ledger — no longer the
  worktree's throwaway `state/`. **Caveat (residual):** the enforcement HOOKS
  (`log_skill_use`, `session_start`/`session_end`) still root state at their OWN tree,
  so skill-usage and similar logged *during a worktree session* write tree-local and
  vanish on cleanup until that locked half is fixed (tracked with 3939d8/d72eec). The
  ledger is the kernel's self-knowledge; a split is silent prediction/correction debt.
- **`memory/` is committed → it rides in automatically.** It's part of the
  checkout, so every worktree has the team memory natively. No junction needed.
- **Shared runtime is NOT isolated.** Same DB, ports, services across worktrees.
  Worktrees isolate *files*, not *runtime* — use separate
  schemas/containers/ports when running migrations or binding a port.
- **The enforcement guard protects the TRUNK, not your worktree's own copies.**
  The active guard hook is wired by **absolute path to the trunk** copy (silo
  `settings.json`), so it blocks edits to the trunk's
  `hooks/lint/evals/autonomy.json` no matter which worktree you're in — but it
  does NOT fire on a worktree's OWN copies of those files (verified: editing
  `<worktree>/hooks/…` exits 0). What keeps enforcement safe is **ONE TRUNK**: a
  worktree edit can't reach `main` without a PR + human review. Route
  enforcement/config changes via `/harness-pr`; never treat the guard as a
  backstop for a worktree-local edit.
- **`node_modules`, build artifacts, gitignored config** (`.env`,
  `settings.local.json`) are NOT copied into a fresh worktree. **Plugin
  enablement is a case of this:** `/plugin` writes a plugin's *code* to the
  machine-global account cache (present everywhere), but records its *enable
  flag* in `.claude/settings.local.json` — gitignored, so a Claude-created
  worktree starts with installed plugins OFF. This repo ships a root
  **`.worktreeinclude`** (gitignore-syntax; only files that are *also*
  gitignored get copied) listing `.claude/settings.local.json`, so plugin
  enablement *and* the project permission allowlist ride into every new
  worktree. (That propagates already-granted `permissions.allow` entries
  without re-prompting — intentional here, since worktrees are
  same-user/same-machine on one repo; drop the include line if you'd rather
  re-consent per worktree.) Add lines there as a product grows gitignored
  config it needs.
  Caveat: a custom `WorktreeCreate` hook replaces git creation and **skips**
  `.worktreeinclude` (this repo configures none). (Verified 2026-06-18 — live
  docs + an empirical subagent-worktree test: the copied `settings.local.json`
  arrived with its `enabledPlugins` block intact.)
  `worktree.symlinkDirectories` exists for heavy dirs but has **known bugs**
  (cleanup can silently fail; a write can replace the symlink with a regular
  file) — prefer `.worktreeinclude`, use symlinks only knowingly.
- **Untracked files in a worktree are not automatically THIS repo's work.** A
  harness worktree can accumulate strays from a *different* project — you ran a
  sibling project's task here, or a trial dropped its output in. Before
  `git add`/committing an untracked dir, resolve its home first: is the same dir
  tracked, and newer, in a sibling repo (`ls` the projects dir;
  `git -C <sibling> log -- <dir>`)? If so, the copy here is a stray — remove it,
  don't commit it into the trunk. (2026-06-17: a 162-file
  `skills/yc-venture-foundry/` was committed into the harness before we caught it
  lived, newer, as `yc-venture-foundry/` in the sibling `yc-foundry-experiment`;
  had to `git reset` + `rm`.)

## 3. Cleanup — and where it bites

Two paths with two different bars — don't conflate them:

- **`ExitWorktree` (interactive):** auto-removes the worktree and its branch
  **only when pristine** — no uncommitted changes, no untracked files, and **no
  new commits** (*any* commit counts, pushed or not). Otherwise it prompts
  **keep** or **remove**. A **named session** also prompts (so you can resume
  the worktree later) rather than auto-removing.
- **Background sweep (`cleanupPeriodDays`):** a looser bar — auto-removes aged
  subagent- and background-session worktrees with no uncommitted changes, no
  untracked files, and **no _unpushed_ commits**. `--worktree` user sessions are
  **never** swept.
- **`claude --worktree` and `-p` non-interactive runs are NOT auto-cleaned.**
  Manual: `git worktree remove <path>` (`--force` to discard changes), then
  `git worktree prune`.
- **Return to trunk from a worktree with `git switch --detach origin/main`, never a
  bare `git switch main`.** Inside a LINKED worktree `git switch main` checks `main`
  OUT into that worktree and leaves the PRIMARY checkout detached on an old commit,
  so `main` migrates between worktrees and the next session's `git switch main` fails
  ("already used by worktree X"). `--detach origin/main` lands on trunk's commit
  without moving the `main` ref. The `post_merge_return_to_trunk` hook now emits the
  `--detach` form when it detects a linked worktree (follow-up 1c9cea); do the same by
  hand. Detail in `references/cleanup.md`.
- **Before removing, reconcile this worktree's gitignored `state/`** (see §2) —
  cleanup discards it.
- **Pruning a BATCH is rule-driven, not list-driven.** State is non-stationary
  while a peer session is live (it can swap a branch, push, or open a PR mid-pass),
  so: re-read the `git worktree list` / `git branch -vv` SNAPSHOT before each
  destructive batch; drive each delete off a RULE ("merged into `main` AND not
  pinned by a worktree AND no open PR"), never a memorized name list; and lean on
  git's own refusals (`git worktree remove` without `--force` refuses a dirty tree;
  `git branch -d` not `-D` refuses an unmerged branch). Run each `git worktree
  remove "<literal path>"` as its OWN command — a `for … do git worktree remove …`
  loop is BLOCKED (Guard A's exemption matches the segment START, and the loop body
  leads with `do`). Spot live peers by `.jsonl` mtimes under `projects/*<repo>*`.
- **A `locked` worktree may be held by THIS session's own long-lived host, not a
  dead peer** — locks leak from `isolation:worktree` agents and outlive them. Before
  reaping one or killing the holder pid, walk the current shell's process-ancestry
  (PowerShell: `Win32_Process.ParentProcessId` from `$PID` up): if the holder is an
  ANCESTOR it's your own session, and killing it ends the conversation. Reclaim
  self-held locks losslessly with `git worktree unlock "<path>"` → `git worktree
  remove "<path>"` (never `--force`/kill), or let them free on the next CLI restart.
- Full batch-GC empirics — the snapshot/rule/refusal discipline, the Guard-A loop
  trap, `git branch -d`'s merged-into-HEAD pitfall, and lock self-vs-peer
  diagnostics (with provenance) — live in `references/cleanup.md`.

## 4. Windows / this repo's housekeeping

- Paths with spaces (`D:\GitHub Projects\...`) must be quoted in shell commands.
- **Keep `.claude/worktrees/` in `.gitignore`.** The docs recommend it, and it's
  confirmed here: a worktree created under `.claude/worktrees/` otherwise shows
  up as `?? .claude/` in the main checkout's `git status`. (Added to this repo's
  `.gitignore` alongside this skill.)
- Windows symlink behavior is finicky — see the `worktree.symlinkDirectories`
  caveat in §2; prefer `.worktreeinclude`.

## 5. Verify against live docs — don't trust this file blindly

Claude Code changes under us, and stale worktree knowledge is dangerous. So:

- Before anything non-trivial, or the moment behavior surprises you, `WebFetch`
  the canonical page: **https://code.claude.com/docs/en/worktrees** (and
  `/sub-agents`, `/settings`, `/tools-reference`). Treat the live docs as truth
  over this file.
- If the live docs contradict a step here, follow the docs and **update this
  skill in the same motion** (branch + PR) so the next session inherits the
  correction. A skill that silently drifts is worse than no skill.

## 6. Sessions: launch, resume, and the guard hatches

Terse rules below; recipes, exact error strings, and provenance live in
`references/sessions.md` (re-verify against live docs if behavior surprises you).

- **Launching the harness against a foreign repo** needs
  `CLAUDE_CONFIG_DIR=<harness>/.claude-private/accounts/<name> claude` from inside
  that repo — a plain `claude` loads the global config, not the harness, and `/cd`
  cannot relocate a live session (ADR 0004). Recipe in `references/sessions.md`.
- **A session "missing" from `/resume` is almost never data loss** — it is either
  still open in a live process (withheld from the picker) or just re-titled.
  `claude --resume <session-id>` opens it regardless; prove integrity with the
  `.jsonl` byte-count, not prose. Detail in `references/sessions.md`.
- **Guard A allows cross-worktree READS; only writes are gated.** Read/Glob/Grep a
  sibling worktree directly; for a cross-worktree WRITE the env hatch can't be set
  mid-session, so lead the command with `HARNESS_ALLOW_CROSS_WORKTREE=1 <cmd>`
  (powershell form + Guard B's launch-only hatch in `references/sessions.md`).
- **Before reimplementing a trunk fix, reconcile against ALL in-flight work, not
  just merged history.** `git fetch`, then scan merged AND open PRs
  (`gh pr list --state all`) AND live sibling worktrees (`git worktree list`) — a
  near-complete rebuild can already sit in an OPEN PR or peer worktree, not yet on
  `main`. (retro-backlog 2026-06-19 b7488db6+dc1c3470; extended 2026-06-23 d1917edc
  — re-derived an SDD phase open PR #99 already carried.)
- **Two sessions sharing one checkout race on `.git/HEAD`.** Prevent with separate
  worktrees (Guard C's lease blocks a stale-HEAD mutate on main); to commit
  mid-race, build from git objects (`write-tree`→`commit-tree`→`push
  <oid>:refs/heads/…`) off a TEMP `GIT_INDEX_FILE`, never `checkout`/`reset` a HEAD
  a peer holds. (sessions b7488db6 + dc1c3470.)
- **A finished `isolation:worktree` agent can leave the session's cwd inside its
  now-empty worktree.** Confirm `pwd` is the PRIMARY checkout before any
  `bin/harness` op: the CLI resolves `state/` to the main ledger regardless of cwd
  (§2), but the enforcement HOOKS still log tree-local, so a drifted cwd silently
  drops skill-usage/session logs. (session b3314a63, 2026-06-23.)
- **When EnterWorktree is blocked** (subagent / pinned cwd — see §1), don't fight
  the guards inline: spawn an `isolation:worktree` Agent to do the Write + commit
  in its own clean worktree, then `git push` its branch from the primary checkout.
  Full create-time and amend-time recipes in `references/sessions.md`.

## Rules

- The user never asks for a worktree and never recites the gotchas — that's this
  skill's job.
- A worktree is single-agent, single-branch, temporary. Integration happens via
  PR to `main`, never a merge *inside* it.
- Worktree changes are not on `main` until a PR merges them. Say so plainly;
  never imply otherwise.
- Enforcement-layer / config changes (settings keys, `.worktreeinclude`, hooks)
  go via `/harness-pr` — and the trunk guard won't catch a worktree-local edit,
  so don't rely on it as a backstop.

