working-in-worktrees
Skill11 starsChanged 50 days agoMITView on GitHub
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.
- Reads credentials
- Deletes or force-pushes
- Commits and pushes
In this repository44 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- auditing-uiSkill
- browser-testingSkill
- capturing-product-mediaSkill
- ci-local-exportSkill
- ci-pulseSkill
- ci-quarantineSkill
- ci-statusSkill
- creating-pull-requestsSkill
- debugging-systematicallySkill
- debugging-test-failuresSkill
- debugging-typescript-errorsSkill
- deps-weeklySkill
- designing-frontendSkill
- opensrcSkill
- organizing-fsd-architectureSkill
- runtime-watchSkill
- stewarding-ci-pipelineSkill
- syncing-agent-skillsSkill
- verification-before-completionSkill
- vision-drift-checkSkill
- visual-companionSkill
- writing-adrsSkill
- writing-app-copySkill
- writing-blog-postsSkill
- writing-changelogsSkill
- writing-developer-guidesSkill
- writing-for-humansSkill
- writing-to-usersSkill
- adding-config-fieldsSkill
- clarifying-requirementsSkill
- maintaining-dependenciesSkill
- maintaining-dev-playgroundSkill
- managing-specsSkill
- marketplace-devSkill
- orchestrating-parallel-workSkill
- reading-session-transcriptsSkill
- receiving-code-reviewSkill
- requesting-code-reviewSkill
- styling-with-tailwind-shadcnSkill
- test-driven-developmentSkill
- upgrading-runtime-dependenciesSkill
- dorkbot-triageSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 35
Create a worktree when any trigger fires:
- Very specific: line 74
Use $BASE for every step of a multi-step comparison — never write origin/main twice in one line of reasoning.
- Very specific: line 75
Prefer git merge-base "$BASE" HEAD over re-reading the branch name.
- Distinctive: line 99
Default for this repo: assume shared: When in doubt, isolate.
- Very specific: line 114
Lighter dev runs: when you only need the app (e.g.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
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:executestage (the workspace-choice phase of the flow plugin'sexecuting-specsskill) — the unified/flowexecution gate. - You are running parallel work that mutates tracked files.
- You are comparing your branch against
mainacross more than one command — a conflict investigation, a red-before drill, a changelog gate, a "what didmainchange" 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:
- You are certainly the sole writer in this checkout, and
- The work is non-code (
research/,specs/, tracker, docs prose) or a single commit you land immediately, and - 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 addand 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:
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 dirSo 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.
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
$BASEfor every step of a multi-step comparison — never writeorigin/maintwice in one line of reasoning. - Prefer
git merge-base "$BASE" HEADover 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. "
maintouched 8 files" is unfalsifiable an hour later; "2a8fb9c9btouched 8 files" is checkable forever.git show --name-only --format='' 2a8fb9c9breturns 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
Detect whether you are already in a worktree.
git rev-parse --git-dir --git-common-dirThe 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.
- 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 statusshows changes you did not make ⇒ another writer is here.git worktree listshows siblings ⇒ multi-worktree work is already underway.- Default for this repo: assume shared. When in doubt, isolate.
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-currentDo 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-123on a branch that only partly delivers DOR-123 closes it anyway. Use a descriptive slug for partial work; seecreating-pull-requests→ A merged PR closes the ticket it names.This provisions everything via
.gtrconfig: copies.env/.mcp.json/.vercel, runspnpm install, generates fumadocs types, and patches uniqueDORKOS_PORT/VITE_PORT/SITE_PORTvalues (worktree-setup.sh) so parallelpnpm devinstances never collide. Worktrees live undergtr.worktrees.dir—~/.dork/workspaces/<project>/<branch>/, committed in.gtrconfigbut overridable per machine by an untracked.git/config, so resolve it rather than assuming (git config --get gtr.worktrees.dir, orgit 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
EADDRINUSEkills the entireturbo devrun (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 inworktree-setup.sh, and add it toglobalPassThroughEnvinturbo.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.Verify it exists before you rely on it —
gtrreports success either way. (/worktree:createrunsgit gtr newunderneath, so this applies however you created it.)On 2026-07-28
git gtr new … --yesprinted[OK] Worktree created: <path>four times for paths that did not exist when an agent went to use them;git worktree listhad 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: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" fiEvery line that touches
$Wmust sit inside the guard.git gtr goprints nothing to stdout when the worktree is missing, andgit -C ""falls back to the current directory — so agit -C "$W" …left outside theifreports on wherever you happen to be standing, exits0, and manufactures the success you were trying to disprove.Notice how far that defect travelled:
gtrreported 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 baregit gtr newbases on the local default branch, which drifts behind; localmainwas three commits behindorigin/mainwhen 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.
- Enter without restarting — move the running session in with the EnterWorktree tool, passing
path= the new worktree's absolute location.EnterWorktreeaccepts gtr worktrees — it works for any path that appears ingit 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.) - Do the work, commit, push, open the PR from the worktree branch.
- Exit with ExitWorktree (
keepto leave it on disk,removeto delete) before cleanup, orcdback to the main checkout. 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 itThat 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 in04-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:
git fetch origin— refs only; never touches the working tree.git worktree add <path> -b <branch> origin/main— a fresh worktree at origin's tip.git -C <path> cherry-pick <your-sha>— re-apply just your commit. It carries your changelog fragment (a uniquely-named file underchangelog/unreleased/), which cannot conflict with anyone else's fragment — the shared[Unreleased]conflict is gone (ADR 260707-231641).- Watch for a stray fragment on cherry-pick. The
post-commithook 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 andgit commit --amendwith the hook suppressed:touch <path>/.claude/.changelog-populator.lockfirst,rmit after (--no-verifydoes not skip a post-commit hook). See.claude/git-hooks/changelog-populator.py. git -C <path> push -u origin <branch> --no-verify— a native worktree has nonode_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.- Open the PR, merge, then
git worktree remove <path>+ delete the branch. - Reconcile the shared checkout once its working tree is clean:
git fetch && git reset --hard origin/maindrops the now-redundant local commit (recoverable via reflog) so localmainmatches origin. Re-checkgit statusis 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>orDOR-123— a workspace outlives any one session and can be reattached. UseDOR-123only 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 nativeclaude -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). mainis 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:donestage 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-parsedetection first). - ❌ Trusting
gtr's[OK] Worktree createdline — 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
/flowintent stages (/flow:ideate,/flow:specify,/flow:decompose) into worktrees — they only writespecs/markdown; stay inmain. - ❌ Reading
.envdirectly to learn a worktree's ports (the file-guard hook denies it) — use/worktree:list. - ❌ Naming
origin/maintwice in one investigation. Pin it once (BASE=$(git rev-parse origin/main)) and compare against$BASE— another session'sgit fetchmoves the shared ref between your commands, and every command still exits0. - ❌ 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:executestage, the workspace-choice phase of the flow plugin'sexecuting-specsskill - Cleanup: the
/flow:donestage (closing-workskill) - Strategy + industry failure modes:
research/20260611_workspace_strategy_runtimes_symphony.md - Parallel-vs-isolation tradeoffs: the
orchestrating-parallel-workskill → "Worktrees vs Subagents"
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

