Skip to content
Back to skills

Working In Worktrees

ASecurity

Decides when agent work needs an isolated git worktree and how to create, enter, and clean one up safely. Use when starting code changes in a checkout that may be shared with another agent, dispatching a Linear task, executing a spec, or running any parallel work that mutates tracked files.

  • 10 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 1, 2026
ai-agentsrustgobashnodetestinggitapi

Works with

  • claude code
  • cursor
  • cli
  • api
  • mcp

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 27, 2026

npx -y skills add dork-labs/dorkos --skill working-in-worktrees --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Working In Worktrees?

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

Security grade badge for Working In Worktrees
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dork-labs-working-in-worktrees/badge)](https://www.skillsdirectory.com/skills/dork-labs-working-in-worktrees)

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
---
name: working-in-worktrees
description: Decides when agent work needs an isolated git worktree and how to create, enter, and clean one up safely. Use when starting code changes in a checkout that may be shared with another agent, dispatching a Linear task, executing a spec, or running any parallel work that mutates tracked files.
---

# Working in Worktrees

## Overview

This skill governs **workspace isolation** for code work in DorkOS — a repo that is routinely worked by several agents and sessions at once. It teaches the one decision rule (_one checkout, one writer_), the concrete failure mode that makes isolation non-optional, the exact mechanics for creating, entering, and cleaning up a worktree without losing anyone's work — and the half that isolation does **not** buy you, because worktrees separate working trees but share every ref.

The repo-wide rule lives in `AGENTS.md` → **Worktrees**. This skill is the mechanics and the _why_.

## When to Use

- You are about to make a code change and the checkout **may be shared** with another agent or session.
- You are running the `/flow:execute` stage (the workspace-choice phase of the flow plugin's `executing-specs` skill) — the unified `/flow` execution gate.
- You are running parallel work that mutates tracked files.
- You are comparing your branch against `main` across **more than one command** — a conflict investigation, a red-before drill, a changelog gate, a "what did `main` change" question.
- You need to create, enter, exit, or remove a worktree and want the safe procedure.
- You are _unsure_ whether to isolate — the default answer for code work in this repo is **yes**.

## Key Concepts

### The rule: one checkout, one writer

`main` is the **clean integration tree**, not a shared scratchpad. Code changes default to an **isolated worktree**; `main` stays clean and is where branches merge back.

**Default to a worktree for any code change.** Stay in `main` only when _all three_ hold:

1. You are **certainly the sole writer** in this checkout, **and**
2. The work is **non-code** (`research/`, `specs/`, tracker, docs prose) **or** a single commit you land immediately, **and**
3. **No long-running dev server** in this checkout needs to stay undisturbed.

Create a worktree when **any** trigger fires:

- 🔴 **Another agent/session may be active here** — the DorkOS default. You usually cannot prove you are alone, so assume you are not.
- 🔴 **Multi-commit / long-lived code work** — a feature, refactor, or spec implementation.
- 🟡 The checkout is already **dirty or on an unrelated topic** branch.
- 🟡 A **dev server or build** must run undisturbed (port isolation).

### Why this is non-negotiable: the auto-checkpoint race

The `Stop` hook `.claude/hooks/create-checkpoint.sh` runs on **every turn** and does `git add -A` → `git stash create` → `git reset`. In a checkout that a **second writer** is touching, that index churn races concurrent git operations:

- It can fire between another agent's `git add` and its commit write, **unstaging that agent's files** → an **empty-tree ("no-op") commit**.
- It can **sweep another agent's uncommitted changes** into your working tree.

This is not theoretical — it happened while dispatching `DOR-101` (an empty-tree commit that had to be recovered via `--amend`). Your own research documents the **identical** industry failure: Cursor "silently ran `git stash` + `git reset HEAD` mid-session"; Claude Code auto-cleanup deleted 10 days of uncommitted work (#46444). See `research/20260611_workspace_strategy_runtimes_symphony.md`. A worktree gives each agent its own tree, so each checkpoint only ever touches that agent's own work — the race cannot happen.

The hook also self-defends: it **bails when a git operation is in progress** (`index.lock`, rebase/merge/cherry-pick state). That narrows the window but does **not** replace isolation — worktrees are the structural fix.

### Two readers, one ref namespace

The hazard above is about two **writers** sharing one working tree. There is a second, quieter one: two **readers** sharing one set of refs. A worktree isolates the working tree. It does **not** isolate the refs or the object store — those live in the common git dir, shared by every worktree of this repo:

```bash
git rev-parse --git-dir --git-common-dir              # differ ⇒ you are in a secondary worktree
git rev-parse --git-path refs/remotes/origin/main     # …yet this resolves under the COMMON dir
```

So you can hold a perfectly isolated tree and still share one `origin/main` with every other session on the machine. When any of them runs `git fetch`, that ref moves **for you too** — including between two commands of your own investigation.

**Nothing errors.** Every command exits `0`. Two `git` invocations seconds apart simply answer against different trees, and the inconsistency surfaces only as a conclusion that does not match the code. On 2026-09-01, with two sessions in one checkout, an agent resolving a merge conflict listed the files `main` had touched, reasoned about that list, and concluded `main` had modified client model-picker files it had never touched — a concurrent session had fetched mid-investigation. It recovered by re-deriving everything against a pinned SHA, but only because the answer had looked implausible. A wrong answer that looks reasonable does not get caught.

**The rule: pin the base once, then never name the moving ref again.**

```bash
BASE=$(git rev-parse origin/main)          # once, at the start of the comparison
git diff --name-only "$BASE"...HEAD
git log --oneline "$(git merge-base "$BASE" HEAD)"..HEAD
```

- Use `$BASE` for every step of a multi-step comparison — **never write `origin/main` twice** in one line of reasoning.
- Prefer `git merge-base "$BASE" HEAD` over re-reading the branch name. A merge base recomputed against a ref that moved is a _different_ merge base, silently.
- **Treat a surprising file list as evidence the ref moved, not as data.** A diff naming files nobody on your branch went near is a symptom, not a discovery — re-derive it against a pinned SHA before you reason one step further.
- **Hand people SHAs, not ref names.** "`main` touched 8 files" is unfalsifiable an hour later; "`2a8fb9c9b` touched 8 files" is checkable forever. `git show --name-only --format='' 2a8fb9c9b` returns the same eight paths on every machine, at every hour, regardless of who fetched — which is the whole property a moving ref lacks.

The same shape bites outside `git`. PR #1413 reported `mergeable: MERGEABLE` from the GitHub API while its own merge-queue entry read `UNMERGEABLE`, and a local test-merge disagreed with both — three answers to one question, each read at a slightly different moment. Any answer you did not pin is a snapshot, not a fact.

### Non-code phases stay in `main`

The `/flow` intent stages — `/flow:ideate`, `/flow:specify`, `/flow:decompose` — write **only `specs/` markdown** (plus tracker breadcrumbs). They do not mutate code, so they run in `main` without a worktree. Isolation begins at **execution** — the `/flow:execute` stage, the workspace-choice phase of the flow plugin's `executing-specs` skill.

## Step-by-Step Approach

1. **Detect whether you are already in a worktree.**

   ```bash
   git rev-parse --git-dir --git-common-dir
   ```

   The two paths are **equal only in the main worktree**. If they differ, you are already in a secondary worktree — **work here, do not nest**. Never create a worktree from inside one.

2. **Judge "am I alone?"** You usually can't prove it. Heuristics, weakest to strongest:
   - Did _you_ start this checkout, or were you handed it mid-state? Handed-in ⇒ assume shared.
   - `git status` shows changes you did not make ⇒ another writer is here.
   - `git worktree list` shows siblings ⇒ multi-worktree work is already underway.
   - **Default for this repo: assume shared.** When in doubt, isolate.

3. **Create the worktree** (keyed by unit of work — `spec-<slug>`, `DOR-123`):

   ```
   /worktree:create <branch-name>            # from origin/main (default)
   /worktree:create <branch-name> --from-current
   ```

   **Do not key the branch by ticket id unless the branch will _complete_ that ticket.** Linear closes an issue whose identifier appears in the merged PR's branch name, so `DOR-123` on a branch that only partly delivers DOR-123 closes it anyway. Use a descriptive slug for partial work; see `creating-pull-requests` → **A merged PR closes the ticket it names**.

   This provisions everything via `.gtrconfig`: copies `.env`/`.mcp.json`/`.vercel`, runs `pnpm install`, generates fumadocs types, and patches **unique `DORKOS_PORT`/`VITE_PORT`/`SITE_PORT`** values (`worktree-setup.sh`) so parallel `pnpm dev` instances never collide. Worktrees live under `gtr.worktrees.dir` — `~/.dork/workspaces/<project>/<branch>/`, committed in `.gtrconfig` but overridable per machine by an untracked `.git/config`, so resolve it rather than assuming (`git config --get gtr.worktrees.dir`, or `git gtr go <branch>` for the full path).

   **Port isolation only works for dev scripts that read their port from one of those env vars.** A hardcoded port in any package's dev script collides with the main checkout, and one `EADDRINUSE` kills the entire `turbo dev` run (persistent tasks take their siblings down). If you add a dev script that listens on a port: take the port from an env var, patch that var in `worktree-setup.sh`, and add it to `globalPassThroughEnv` in `turbo.json` — Turbo's strict env mode silently strips undeclared vars before they reach the task process.

   **Lighter dev runs:** when you only need the app (e.g. testing a server/client change), skip the site and plugin builds entirely: `pnpm exec dotenv -- turbo dev --filter=@dorkos/server --filter=@dorkos/client`.

4. **Verify it exists before you rely on it — `gtr` reports success either way.** (`/worktree:create` runs `git gtr new` underneath, so this applies however you created it.)

   On 2026-07-28 `git gtr new … --yes` printed `[OK] Worktree created: <path>` **four times for paths that did not exist** when an agent went to use them; `git worktree list` had no record of them either. Worktrees that were **never written to** did not survive; every one an author actually committed in did. The mechanism is unknown, so treat that success line as a claim, not a fact:

   ```bash
   git gtr new <branch> --from origin/main --yes     # only if creating by hand
   W=$(git gtr go <branch> | tail -1)                 # resolves the folder: slashes become hyphens
   if [ -d "$W" ]; then
     git -C "$W" log --oneline -1
     git -C "$W" rev-list --count HEAD..origin/main   # 0 == based on origin's tip
   else
     echo "MISSING — recreate it"
   fi
   ```

   **Every line that touches `$W` must sit inside the guard.** `git gtr go` prints nothing to stdout when the worktree is missing, and `git -C ""` falls back to the _current_ directory — so a `git -C "$W" …` left outside the `if` reports on wherever you happen to be standing, exits `0`, and manufactures the success you were trying to disprove.

   Notice how far that defect travelled: `gtr` reported a creation that had not happened, the first check written against it could not fail, and the fix for _that_ check left its second line unguarded — three layers of the same mistake in one thread. A check that cannot fail is worse than no check, because it produces confidence instead of an answer.

   **Always pass `--from origin/main`.** A bare `git gtr new` bases on the _local_ default branch, which drifts behind; local `main` was three commits behind `origin/main` when this was written. An author who quietly recreated a missing worktree got exactly that, and caught it only just before writing code.

   **Then commit something before you hand the path to anyone.** An empty worktree is the kind that vanishes, and a path in a briefing is a promise someone else acts on. Equally, **if you were handed a path, verify it before you start and say so if it is wrong.** Of three agents briefed against paths that did not exist, two silently recreated or relocated them and only the third reported it — which is the only reason this was found at all. Adapting quietly costs the orchestrator more than failing loudly.

5. **Enter without restarting** — move the running session in with the **EnterWorktree** tool, passing `path` = the new worktree's absolute location. **`EnterWorktree` accepts gtr worktrees** — it works for any path that appears in `git worktree list`, which gtr's `~/.dork/workspaces/…` worktrees do. The session cwd switches with no CLI restart and the SDK session continues. Do _not_ believe the stale claim that a gtr worktree must be re-created under `.claude/worktrees/` before it can be entered — that older limitation no longer holds (re-confirmed 2026-06-27). (`claude -w <name>` instead starts a _fresh_ session already inside one.)

6. **Do the work**, commit, push, open the PR from the worktree branch.

7. **Exit** with **ExitWorktree** (`keep` to leave it on disk, `remove` to delete) before cleanup, or `cd` back to the main checkout.

8. **Clean up after merge** — for the one worktree you know about, `/worktree:remove <branch> --delete-branch`. But the merge usually lands after your session is over (auto-merge and the merge queue both run on their own clock), so the more reliable habit is to sweep at the **start** of a session rather than the end of one:

   ```
   /worktree:prune          # what would go, and why
   /worktree:prune --fix    # remove it
   ```

   That collects everything that merged while you were away, not just the branch you happen to be thinking about. It refuses anything it cannot prove is safe — uncommitted, unpushed, still open, or unaskable — and names the reason for each. `/flow:done` (`closing-work`) also offers cleanup for a worktree recorded in `04-implementation.md`, but only when a session is alive to accept the offer, which is exactly the case that keeps not happening.

   **Do not skip this because the worktree is "just" a review checkout.** Review checkouts were 47 of the 107 worktrees the 2026-08-01 sweep removed — the largest single category — at ~3.5 GB each (`research/20260801_worktree-and-branch-sweep.md`).

## Landing Work from a Shared or Diverged Checkout

When you've already committed on a shared `main` that has diverged from `origin/main` (another agent's merge landed upstream while you worked, so a plain `git push` is rejected), **do not rebase the shared checkout** — that churns the working tree and can yank the branch out from under the co-tenant agent. Land your commit through an isolated worktree instead:

1. `git fetch origin` — refs only; never touches the working tree.
2. `git worktree add <path> -b <branch> origin/main` — a fresh worktree at origin's tip.
3. `git -C <path> cherry-pick <your-sha>` — re-apply just your commit. It carries your changelog fragment (a uniquely-named file under `changelog/unreleased/`), which cannot conflict with anyone else's fragment — the shared `[Unreleased]` conflict is gone (ADR 260707-231641).
4. **Watch for a stray fragment on cherry-pick.** The `post-commit` hook re-derives a fragment from the commit subject; it dedupes by entry line, so a replay normally writes nothing. If a redundant fragment does appear, delete it and `git commit --amend` with the hook suppressed: `touch <path>/.claude/.changelog-populator.lock` first, `rm` it after (`--no-verify` does **not** skip a post-commit hook). See `.claude/git-hooks/changelog-populator.py`.
5. `git -C <path> push -u origin <branch> --no-verify` — a native worktree has no `node_modules`, so the lefthook pre-push can't run; the commit already passed pre-commit lint in the source checkout, and CI is the backstop.
6. Open the PR, merge, then `git worktree remove <path>` + delete the branch.
7. **Reconcile the shared checkout** once its working tree is clean: `git fetch && git reset --hard origin/main` drops the now-redundant local commit (recoverable via reflog) so local `main` matches origin. Re-check `git status` is clean immediately before resetting.

Better still: start the work in a worktree from the outset (the steps above, minus the cherry-pick) so the divergence never happens.

**Local `main` keeps itself level.** When a session starts (not on resume, `/clear` or compaction), `.claude/hooks/sync-main-checkout.sh` fast-forwards the main checkout's `main` to `origin/main` when it is behind and has no tracked changes, and prints one `[Harness]` line instead when it is dirty (move that work to a worktree) or diverged (land it with the steps above). It never moves a tree with tracked changes, and untracked files it would overwrite stop the fast-forward. Pause it per clone with `git config dorkos.mainSync warn` (report only) or `off`; `git config --unset dorkos.mainSync` restores it.

## Best Practices

- **Key by unit of work, not session.** `spec-<slug>` or `DOR-123` — a workspace outlives any one session and can be reattached. Use `DOR-123` only when the branch will **complete** that ticket; merging a branch whose name carries the id closes the issue (step 3).
- **Prefer gtr worktrees** (`/worktree:create`) over native `claude -w`/`.claude/worktrees/` for anything that runs lint/typecheck hooks or a dev server — gtr ones are fully provisioned; native ones are instant but unprovisioned (fine for docs-only).
- **`main` is the merge target, not the workbench.** Land branches into it; don't accumulate ad-hoc code edits there.
- **Record the worktree** in `04-implementation.md` (specs) so completion and the `/flow:done` stage can offer cleanup.

## Common Pitfalls

- ❌ Starting code work in a shared checkout "because it's a small change" — the auto-checkpoint race does not care how small your change is.
- ❌ Creating a worktree from inside a worktree (always run the two-path `rev-parse` detection first).
- ❌ Trusting `gtr`'s `[OK] Worktree created` line — check the path exists and commit in it before handing it to anyone.
- ❌ Silently recreating or relocating a worktree path you were handed. Say it is wrong; the orchestrator cannot see what you quietly fixed, and the replacement may be based on a stale local `main`.
- ❌ Auto-removing a worktree with **uncommitted, untracked, or unpushed** work — refuse and confirm first. This is where Claude Code and Cursor both shipped data-loss bugs.
- ❌ Forcing the `/flow` intent stages (`/flow:ideate`, `/flow:specify`, `/flow:decompose`) into worktrees — they only write `specs/` markdown; stay in `main`.
- ❌ Reading `.env` directly to learn a worktree's ports (the file-guard hook denies it) — use `/worktree:list`.
- ❌ Naming `origin/main` twice in one investigation. Pin it once (`BASE=$(git rev-parse origin/main)`) and compare against `$BASE` — another session's `git fetch` moves the shared ref between your commands, and every command still exits `0`.
- ❌ Believing a file list that surprised you. In a shared checkout that is first evidence the ref moved, not a finding to reason from.

## References

- Repo rule: `AGENTS.md` → **Worktrees**
- Commands: `/worktree:create`, `/worktree:list`, `/worktree:remove`
- Execution gate: the `/flow:execute` stage, the workspace-choice phase of the flow plugin's `executing-specs` skill
- Cleanup: the `/flow:done` stage (`closing-work` skill)
- Strategy + industry failure modes: `research/20260611_workspace_strategy_runtimes_symphony.md`
- Parallel-vs-isolation tradeoffs: `contributing/parallel-execution.md`

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…