Playbook: Shared Checkout Worktree Isolation Enforcement

Created: 2026-07-08 Incident: Two stray commits from unrelated sessions landed on this orchestrating session’s checked-out branch within a single working session Issue: #3230


Trigger

Multiple concurrent Claude Code sessions/agents (main session, background Agent() calls, other human/agent sessions on the same machine) all operate against the same primary checkout (C:\code_2026\p24-infra) instead of isolated tmp/wt-{issue} worktrees mandated by CLAUDE.md §Agent Workflow — Worktree Branch Isolation.

Whichever session runs git checkout <branch> last determines what’s checked out. If a second session then runs git commit without first switching to its own branch, the commit lands on whatever branch the first session left checked out — silently, with no error.


Symptoms

  • git log --oneline -3 shows an unexpected commit authored by a different task/topic than what you’re working on
  • git branch --show-current shows a branch name unrelated to your current task (e.g., security/3216-jwt-rotation or rc/3223-atrax-n8n-bms4 when you expected main)
  • A commit you just made doesn’t appear where expected, or an extra commit appears above yours in git log

Root Cause

This is not a bug in any single session’s logic — every session correctly runs git commit in “the current directory.” The problem is that the current directory is shared, mutable state across concurrent sessions, and nothing enforced the documented worktree-isolation policy technically — it was documentation-only, easy to forget under task pressure.

Observed twice in one session on 2026-07-08:

  1. A commit intended for docs/is-3218-... landed on security/3216-jwt-rotation (a JWT rotation branch from an unrelated concurrent session)
  2. Minutes later, the working directory had switched again to rc/3223-atrax-n8n-bms4 (an ATRAX n8n branch from yet another concurrent session)

Immediate Recovery (if you discover a stray commit)

Do NOT discard it — it may be someone else’s real, unmerged work.

# 1. Identify the stray commit(s) on top of / mixed into your branch
git log --oneline -5
 
# 2. Rescue it onto its own branch before touching anything
git branch rescue/<topic>-<short-desc> <stray-commit-sha>
git push origin rescue/<topic>-<short-desc>
 
# 3. Reset your own branch back to remove the stray commit
#    (only if it hasn't been pushed anywhere with the stray commit included)
git reset --hard origin/<your-branch>    # or origin/main if branch is new
 
# 4. Redo your own work cleanly in an isolated worktree (see Prevention below)
 
# 5. File an issue documenting the rescued branch so a human/session can
#    properly re-file it under the correct issue number and review it

Permanent Fix — Technical Enforcement (applied 2026-07-08)

Documentation alone (CLAUDE.md §Worktree Branch Isolation) was not sufficient — it relies on every session remembering to opt in. Added a technical guard to both safety hooks:

  • .claude/hooks/pre-bash-safety.sh (Bash tool)
  • .claude/hooks/pre-bash-safety-windows.ps1 (PowerShell tool)

Detection logic: a linked git worktree’s --git-dir points to .git/worktrees/<name>, while the primary checkout’s --git-dir equals its --git-common-dir. Any git commit, git checkout -b, git switch -c, or git branch -m/-M run where these are equal (i.e., in the primary checkout) is now blocked with a message directing to git worktree add.

# This now fails in the primary checkout:
cd C:\code_2026\p24-infra
git commit -m "..."
# BLOCKED: git commit / branch-create in the PRIMARY checkout, not an isolated worktree (#3230)...
 
# This still works — inside any linked worktree:
cd C:\code_2026\p24-infra\tmp\wt-1234
git commit -m "..."
# proceeds normally

What is NOT blocked (intentionally — these are read-only/navigational, needed even in the primary checkout): git checkout <existing-branch> (no -b), git pull, git fetch, git status, git log, git worktree add itself.


Standard Workflow (unchanged, now technically enforced)

# 1. Create an isolated worktree for this issue
git worktree add tmp/wt-{issue} -b {branch-name} origin/main
 
# 2. Do ALL commit/branch work inside the worktree
cd tmp/wt-{issue}
# ... edits, git add, git commit — all fine here ...
git push -u origin {branch-name}
 
# 3. Open PR, review, merge as usual
 
# 4. Clean up — remove, then GUARANTEE the dir is gone (git may leave it behind), then prune
cd ../..
git worktree remove --force tmp/wt-{issue}
rm -rf tmp/wt-{issue}                 # leftover unregistered dir silently falls through to the parent repo — #6006
git worktree prune -v

A worktree dir left behind after git worktree remove (Windows file-lock, stale checkout) makes git silently operate on the parent repo’s current branch. See worktree-remove-fallthrough-guard.md for the root cause and the mandatory pre-write git rev-parse --show-toplevel guard.


Limitations

  • This guard only protects sessions running through Claude Code’s hook system on this machine. It does not protect against: manual human git commands run outside Claude Code, or other machines/CI runners operating on a clone of this repo.
  • It cannot undo damage from before 2026-07-08 — only prevents new occurrences going forward.
  • Background Agent() calls with isolation: "worktree" already get their own isolated worktree automatically (unrelated mechanism) — this hook guards the orchestrating session and any Agent() calls made without that isolation flag that end up sharing the primary checkout.

  • CLAUDE.md §Agent Workflow — Worktree Branch Isolation (the pre-existing documented policy this hook now enforces technically)
  • concurrent-sops-write-worktree-race.md — the sibling filesystem-level race (#5242): a SOPS write is not a git commit, so the hook guard here never fires on it; that playbook adds a pre-flight guard for shared-file/SOPS writes
  • Issue #3230
  • Rescued branch example: rescue/3161-test-net-v42 (re-filed as issue #3233, PR #3234)