claude-multisession
TLDR/BLUF
Run several Claude Code sessions at once on one repository without them overwriting each
other. Each session gets its own git worktree and branch; hooks refuse the edits, commits and pushes
that would collide. No daemon, no service, no dependencies beyond pwsh, git and a python.
PowerShell 7, Windows-first, MIT. Cloning installs nothing.
The one thing to know before you start:
When something here breaks, it produces exactly the same output as when it works – byte-identical to success. A hook that is installed but reaches nothing exits 0 and prints nothing, which is what a healthy hook does when there is no other session to find. A gate whose helper never loaded lets the edit through, which is what a gate does when it checks and finds nothing wrong. You cannot tell the two apart from the outside, because the difference was never written into the output.
That is why ccx doctor exists, why you run it before installing anything as well as after, and
why it fires each control on purpose instead of reading a settings file. A green report you did not
baseline is not evidence.
Go: Quickstart - Limits - What ships - Full docs
Or have Claude Code evaluate it for you. Feed it this page and it will read your repository and tell you which of these collisions you actually have – usually faster than deciding from the docs.
The problem
Point several agents at one working directory and they contend for the git index and overwrite each other’s files. The sharp version is documented upstream in anthropics/claude-code#76590, with a field report of roughly fourteen sessions handed the same directory as their working directory.
One agent runs an ordinary git checkout -B <branch> origin/main. Git allows it – that branch is
not checked out anywhere. The shared working tree force-switches, swapping every file under whichever
session was mid-task and dragging its uncommitted work onto the wrong branch. It is invisible while
it happens, because each session believes it owns its directory.
That is the loudest collision, not the only one. Six more – same file, same work in different files, same reserved number, same config lock, same shared list, same agent memory – are tabulated with their measurements in the README’s collision table, which is the place to start if you are still deciding whether you have this problem.
What you get
A worktree per session. Every session works in its own checkout on its own branch, while the repository history – and the coordination state keyed to it – stays shared. Worktrees
Edits that collide are refused, not merged. A PreToolUse collision gate refuses an edit to a
file another live session is already changing. Around it sit atomic claims, a cross-session lock,
and atomic sequence-number allocation, so two sessions cannot mint the same decision-record number.
Claims are advisory by design – they cannot stop a session that refuses to look, which is why the
commit-time gate sits behind them. Coordination
Guardrails that hold whether the agent cooperates or not. Two git hooks: commit-msg runs the
claim gate, pre-push refuses a direct push to a protected ref. A worktree gate stops sessions
building in the shared primary checkout. Hooks
Sessions that can reach each other. Announce tells peers what you are about to touch before you start, rather than at the merge conflict afterwards. Steering changes a running session’s course mid-task. Presence, occupancy and overlap answer – separately – who is live, which worktree each occupies, and what each is changing right now. Coordination, Steering
Cleanup that refuses to guess. A liveness registry tracks which sessions are actually alive; the reaper prunes worktrees that are merged and clean and unoccupied, and declines when it cannot tell which of those a worktree is. Pruning
Work too large for one context. A coordinated set of sessions can cover a codebase at once, which extends to compliance work – an OWASP ASVS 5.0 assessment runs to several hundred requirements, more than one session can hold. The write-up is candid that the obvious split is the wrong one: a session per chapter is a scheduling answer, and the collision that actually costs you is not two agents editing the same row but two agents applying different unwritten rules, producing verdicts that cannot be reconciled afterwards because neither recorded which rule it applied. Running a large assessment
Which part defends against #76590
Three mechanisms touch that failure. Only the first prevents it:
| Script | What it does | |
|---|---|---|
| Prevention | scripts/hooks/worktree_gate.ps1 |
A PreToolUse hook. Refuses the git verbs that would swap or discard the shared primary checkout’s tree – that is the tree #76590 flips, and the rule that stops it. A separate rule refuses a git checkout/git switch that would hijack another session’s linked worktree. Either way the tool call does not run. |
| Repair | scripts/worktree/worktree-selfheal.ps1 |
Restores the shared primary checkout when its HEAD has drifted and the tree is clean. On a dirty tree it declines, says so, and touches nothing. |
| Detection | the home-branch record | Recorded in each worktree’s private git directory, where a checkout cannot move it. A later session finding the worktree elsewhere warns and offers the restore command. Warn-only; it is wrong by design, so treat a warning as a prompt to read the reflog, not as proof. |
Limits, read before installing
Session discovery rests on a vendor surface this project does not own. That single fact is the
source of every limit below, and it is worth understanding once rather than meeting three times.
Everything answering “who is live, and where” reads <config-root>/sessions/<pid>.json – a record
the client writes, whose shape, location and lifetime belong to the client. Three consequences:
- Announce needs the desktop client. It delivers through the
ccd_session_mgmtMCP server, which the desktop client provides and a plain CLI install does not. The hook does not send anything itself; it resolves peers and asks the model to send. On a CLI-only host it still fires, still finds peers, then instructs the model to call tools it does not have. Nothing is delivered and the model says so. If you are CLI-only, leave that one hook uninstalled – nothing else depends on it. - The desktop app’s own session list is incomplete. Its
list_sessionsenumerates an in-memory map of sessions that app itself spawned. An editor-extension session is never entered into it – not filtered out, never registered – so it is invisible there and cannot be messaged. Treatlist_sessionsas authoritative for who can be messaged, and the on-disk records as the only registry for who exists. - A schema change degrades to “cannot tell”, not to a wrong answer. If a future client renames a
field or changes
startedAt’s unit, every fence here reports that it cannot tell. That is designed for inscripts/coord/session-registry.ps1, and the doctor prints how many records it read and placed, so a schema change surfaces as a count going to zero rather than a silent all-clear.
These are guardrails against the accidental action, not security boundaries. The PreToolUse
gates inspect tool arguments, so a file written by a shell command is invisible to them, and any
agent-authored script defeats a command-string rule outright. git commit --no-verify and
git push --no-verify bypass the git hooks. No CI-side enforcement ships.
Requirements
| Need | Without it |
|---|---|
PowerShell 7.3+ (pwsh) |
Nothing installs. Most scripts carry #Requires -Version 7.3. |
| git | Nothing installs. Everything is keyed on the git common directory. |
python on PATH (or CCX_PYTHON) |
The installed git gates are OFF and say so on stderr. Needed by the three git-hook checkers and the leak gate. |
ccx.config.json at the target repo root |
User-scope hooks stay inert in that repo. It is both the knob file and the opt-in marker. |
Runs on PowerShell 7 for Linux and macOS, but the Windows paths are the exercised ones; self-marking
in the roster and path case-folding degrade elsewhere. There is no ccx executable on PATH –
where these docs say ccx doctor, that is shorthand for
pwsh -NoProfile -File <this-checkout>/bin/ccx-doctor.ps1. MIT licensed
(LICENSE).
Quickstart
Two directories are involved, and every command says which it means:
| tooling | This checkout. Nothing you install governs it; it is where scripts are copied from and hashed against. |
| target | The repository you want governed. It gets the config file, the git hooks, and its primary checkout in the gate’s allowlist. |
Use the vendored layout – copy scripts/, bin/ and ccx.config.json into the target and
commit them, so tooling is target. It is the only layout in which the doctor can reach exit 0. The
separate-checkouts layout works for the worktree gate, both git hooks and the backstop, but the three
coordination hooks are shims that resolve their script inside whatever repository the session is
running in – so a target that does not carry those files gets three hooks that are wired and resolve
nothing.
Run all of this from a plain terminal. All four installers refuse when $env:CLAUDECODE is 1,
because a session that can install these controls can remove them.
$tooling = "<path-to-this-checkout>"
$target = "<path-to-the-repo-you-want-governed>"
Set-Location $target # the doctor reports what it resolves FROM HERE, so stand in the target
# 1. Opt the target in, and give it the scripts the coordination hooks resolve.
Copy-Item "$tooling/ccx.config.json" "$target/ccx.config.json" # then edit it
Copy-Item "$tooling/scripts" $target -Recurse
Copy-Item "$tooling/bin" $target -Recurse
# ...then commit them, so every worktree of the target gets them. After vendoring there are two
# copies on disk: install and audit from ONE of them. Installing from one and hashing against the
# other is exactly the drift the doctor calls STALE.
# 2. Baseline BEFORE installing anything. Expect a wall of OFF and exit 1 -- that is correct, and it
# is the only way to tell an installed guardrail from a decorative one afterwards.
pwsh -NoProfile -File "$tooling/bin/ccx-doctor.ps1" -Repo $target
# 3. Coordination hooks: session banner, collision gate, announce. Takes NO repository -- it writes
# ONE settings file whose hooks resolve their repo per session at run time.
pwsh -NoProfile -File "$tooling/scripts/coord/install-coordination.ps1"
# 4. The commit-msg claim gate and pre-push guard, into the TARGET clone's shared .git/hooks, where
# one copy governs every worktree of that clone at once.
pwsh -NoProfile -File "$tooling/scripts/coord/install-git-hooks.ps1" -RepoRoot $target
# 5. The worktree gate. -Repo names the PRIMARY checkout to allowlist (several allowed:
# -Repo <path-a>,<path-b>). The allowlist is the kill switch.
pwsh -NoProfile -File "$tooling/scripts/worktree/install-gate.ps1" -Repo $target
# 6. The SessionStart backstop. ONE config root per run -- run it again for each root the doctor
# lists under "config roots". An unwired root is OFF, and OFF is exit 1.
pwsh -NoProfile -File "$tooling/scripts/worktree/install-selfheal.ps1" -ConfigDir ~/.claude
# 7. Prove it.
pwsh -NoProfile -File "$tooling/bin/ccx-doctor.ps1" -Repo $target
Then run two sessions
pwsh -NoProfile -File "$tooling/scripts/worktree/spawn.ps1" -Name alerts
pwsh -NoProfile -File "$tooling/scripts/worktree/spawn.ps1" -Name parser
spawn.ps1 creates an isolated worktree on its own branch and opens an editor window in it; new.ps1
does the same without the editor. Neither takes a target flag – they act on the primary of the
repository you are standing in, so keep standing in the target. Now start a Claude Code session in
each window and confirm the coordination is live:
pwsh -NoProfile -File "$tooling/scripts/coord/presence.ps1" # both sessions listed
pwsh -NoProfile -File "$tooling/scripts/coord/overlap.ps1" # what each is changing
Ask both sessions to edit the same file and the second one’s edit is refused. That refusal is the whole product.
Two things the quickstart does not cover
Prove it governed the repository you meant. The doctor’s default target is the current directory, so a run started in the wrong place produces a long, plausible, mostly-green report about the wrong clone:
pwsh -NoProfile -File "$tooling/bin/ccx-doctor.ps1" -Repo $target |
Select-String 'repo examined|tooling checkout|gate: allowlist|LIVE allowlist'
Give the sessions a working agreement. Copy
CLAUDE.md.template
into the target as CLAUDE.md and edit it down to what is true there. The gates stop what they can
see; this file is where you write down what they cannot. Keep it short enough that it stays true –
an unmaintained working agreement is worse than none, because the next session acts on it anyway.
INSTALL.md is the record of record for the installers: the annotated version of these steps, and how to prove each one is live rather than merely merged.
What ships
Paths are relative to this checkout; browse them in the repository.
Start here
| Script | Does | Doc |
|---|---|---|
bin/ccx-doctor.ps1 |
Prove – by receipt and by attack – that each control is installed, wired, and refuses what it can be made to refuse. It never infers, always prints WHAT WAS SCANNED and BLIND SPOTS ON THIS RUN, and a skip is never a pass (exit 2). At least one deny path is not self-testable: the collision gate’s needs a live peer worktree holding an uncommitted change to the same file, so the doctor proves only that the gate refuses to go silent, and prints that as a blind spot every run | INSTALL.md |
To run sessions
| Script | Does | Doc |
|---|---|---|
scripts/worktree/new.ps1 |
Create an isolated worktree on its own branch, off the fetched remote tip, serialised against concurrent adds | Worktrees |
scripts/worktree/spawn.ps1 |
new.ps1 plus an editor window (-Editor, else CCX_EDITOR, else EDITOR, else code) |
Worktrees |
scripts/coord/presence.ps1 |
Who is actually live in this repo right now, across every surface. Read-only | Coordination |
scripts/coord/overlap.ps1 |
What everyone else is changing – files and stated work. -File <path>, -Json; cached, so the gate’s common case is a cache read |
Coordination |
scripts/coord/claim.ps1 |
Take, release or list an atomic claim on a piece of work, so a session finds out before the work rather than at merge. Advisory: a claim cannot stop a session that refuses to look. Claims do not expire and releasing is manual, on purpose | Coordination |
scripts/coord/alloc.ps1 |
Allocate the next number in a shared sequence atomically, so two sessions can never be handed the same one; -ShowFloor inspects without spending one. seq_check.py is the other half – neither is sufficient alone |
Sequence allocation |
bin/ccx-steer.ps1 |
Queue a steering note from a second terminal while a session is mid-task | Steering |
To clean up
| Script | Does | Doc |
|---|---|---|
scripts/worktree/remove.ps1 |
Remove one worktree, referencing its tip before anything is removed, and writing a keep-ref when -DeleteBranch is used |
Pruning |
scripts/worktree/prune-merged.ps1 |
The reaper: prune = merged and clean and unoccupied. Dry-run by default, -Apply to act. Carries a second, non-cwd signal and prints its blind spots |
Pruning |
scripts/worktree/rescue.ps1 |
Move uncommitted work out of the shared primary into a fresh worktree – the companion to the gate that stops you writing there | Worktrees |
scripts/worktree/restore-primary.ps1 |
Re-attach the primary to its home branch after a session left it detached or on the wrong branch. Refuses on a dirty tree | Worktrees |
scripts/worktree/sessions.ps1 |
Find sessions for this repo across every login, including ones a relocation made invisible; -Rehome puts a transcript back |
Worktrees |
scripts/security/scan_forbidden.py |
The leak gate: refuse identifying content before a private repo goes public. --path DIR, --require-tokens, --show-context. Nothing wires it |
Leak gate |
Controls that run without you
Installed once, then invoked by the harness or by git.
| Script | Event | Does | Doc |
|---|---|---|---|
scripts/hooks/worktree_gate.ps1 |
PreToolUse |
Denies writes whose target path is inside a governed primary, dispatch from the primary, and the git verbs that swap or discard its tree. Fails open, but loudly | Hooks |
scripts/hooks/collision_gate.ps1 |
PreToolUse |
Refuses an edit to a file a live session is already changing. Fails open – never silently | Coordination |
scripts/hooks/announce-session.ps1 |
UserPromptSubmit |
Tells peers you exist and what you intend, on the first prompt at which a messageable peer exists. Always exits 0; delivery needs the desktop client | Coordination |
scripts/worktree/session-context.ps1 |
SessionStart |
Banner: your project’s working defaults plus a live coordination block. Its stdout is the chat’s starting context, so it never fails loudly | Hooks |
scripts/worktree/worktree-selfheal.ps1 |
SessionStart |
Repairs a primary whose HEAD drifted, when the tree is clean – the most privileged control here, whose only safety property is that it refuses a dirty tree | Worktrees |
scripts/hooks/claim_check.py |
commit-msg |
Refuses a code-touching commit whose subject claims an item this worktree does not hold. Fail-closed | Coordination |
scripts/hooks/push_guard.py |
pre-push |
Refuses a direct push of a protected ref. An explicitly empty protectedRefs list disables it with a message on stderr |
PRs and merges |
scripts/hooks/seq_check.py |
pre-commit |
Refuses a colliding, unallocated, or unindexed sequence number; --ci re-runs the collision rules against a freshly fetched trunk. No installer wires it |
Sequence allocation |
scripts/hooks/block-blanket-git-stage.ps1 |
PreToolUse |
Opt-in. Denies git add -A/--all/-u/. and git commit -a/-am. Fails open |
Hooks |
scripts/hooks/steer-inject.ps1 |
PreToolUse |
Opt-in per worktree. Delivers a queued steering note at the next tool-call boundary rather than at the end of the turn | Steering |
Internals and installers
| Script | Does | Doc |
|---|---|---|
scripts/coord/session-registry.ps1 |
The liveness fence: reads the client’s session registry and decides whether a session is alive. Liveness may only VETO, never PERMIT – DEAD/STALE/absent is the absence of a veto, not a permission | Concepts |
scripts/coord/occupancy.ps1 |
The one cwd-to-worktree matcher, returning a receipt alongside its rows (roots examined, records examined, records that could not be placed) and setting Available only when there was something to examine |
Concepts |
scripts/coord/lock.ps1 |
The short-lived cross-session mutex, dot-sourced rather than run: . lock.ps1 then Enter-CcxLock / Exit-CcxLock. No TTLs anywhere: locks retry and never steal, and on timeout fail loudly and name the holder |
Concepts |
scripts/coord/install-coordination.ps1 |
Wires the banner, collision gate and announce at user scope as shims that re-resolve at run time; writes exactly one settings file per run (-SettingsPath) |
INSTALL.md |
scripts/coord/install-git-hooks.ps1 |
Installs commit-msg + pre-push into one clone’s shared .git/hooks (-RepoRoot), refuses to overwrite a foreign hook, and never writes pre-commit at all |
INSTALL.md |
scripts/worktree/install-gate.ps1 |
Installs the worktree gate as a copy outside every working tree, into every config root it finds, plus the allowlist that is its kill switch (-Repo, -ConfigDir) |
INSTALL.md |
scripts/worktree/install-selfheal.ps1 |
Wires the SessionStart backstop into ONE config root per run; -ConfigDir is mandatory, and it governs whatever the gate’s allowlist already names |
INSTALL.md |
Where to go next
The model: Concepts (worktree per session, one shared state root, a liveness fence
that may only veto, exclusive-create over read-modify-write, no TTLs, the six knobs in
ccx.config.json) then Hooks (harness versus git hooks, every control mapped to its
event and fail-open or fail-closed posture).
Running sessions: start at Running multiple sessions – it is the entry point to the group, and it covers the three things no other page owns (which surface to run sessions on, the channels they have for reaching each other, and using one session as a coordinator). Then, in the order the work happens: Worktrees - Coordination - Steering - Sequence allocation - PRs and merges - Pruning.
Safety, in descending order of how much actually ships: Leak gate (a scanner you can run today, plus the blind spot no scanner can close) - Usage awareness (a design; ships no hook) - Session mail (a design for reaching the peers announce cannot; ships nothing, and is most useful as the list of ways the obvious implementations fail).
In practice: Tips and tricks (ordered by when each item bites) - Drift audit case study (a method, not a finding list).
The standards are a separate project now. secure-development-standards holds the set that used to live in this repository: a bar for code an agent wrote and a small team has to stand behind, plus the CI discipline and the assessment method that went with it. They ship no code and confer no certification. Their index is over there and is not mirrored here, because two copies of one list is the drift both projects are about.
At the repository root: INSTALL.md (record of record for the installers) and CLAUDE.md.template (a working agreement to drop into your own repository).