Field report: concurrent sessions sharing one git clone — measured collisions, three userland mitigations, and the residue only the harness can fix

Status Open
Maintainer reply None cached
Activity 4 comments · opened Aug 22, 2026

Concurrent Claude Code sessions on one machine: a shared-clone collision problem

Written 2026-08-13, updated 2026-08-21 for sharing with Anthropic. A field report from a team running
3–6 concurrent Claude Code sessions on one workstation against one git working directory. It documents a
reproducible failure, how often the conditions for it arise, three mitigations we built, and what each
one measurably does and does not fix. All three are working code you are welcome to take, improve, or
replace.

The headline is not "concurrent sessions collide." It is that two of our three incidents happened
to sessions that had read the mitigation and followed it correctly
, and one of those had deliberately
excluded the contended file from its own commit and still lost its work to another session. That is what
makes this a tooling gap rather than a discipline problem.

And the 2026-08-21 update sharpens it. Our third mitigation removes the collision structurally
sessions never edit the shared file at all — and over seven days it drove file-bundling on our
highest-contention file from 55% of commits to zero, across 23 fragments from 20 sessions. What remained
was not a design gap. It was that three long-running sessions forgot the convention and edited the file
anyway
, despite the rule being in the document every session reads at startup, a session-start check,
and a tool that refuses bad input. A tool can only refuse the calls it receives. That residue is the part
we cannot reach from userland, and it is what we would most like to see addressed at the harness level.

---

TL;DR — We run 3–6 concurrent Claude Code sessions on one workstation against one git clone. Sessions commit their own work. Three times, one session's uncommitted edits were swept into a different session's commit; content always survived, attribution never did, and two were pushed before anyone noticed. We measured the exposure (37% of all commits touched a single shared file, ~8/day, 55% of those bundled with unrelated work), then built three mitigations and measured each. The third one works: sessions stop editing the shared file entirely and queue fragments instead. Over 7 days it drove file-bundling on that file from 55% of commits to 0, across 23 fragments from 20 sessions, with zero losses. The residue is the reason we're filing this. Three sessions edited the file directly anyway — about one every two days — despite the rule living in the document every session reads at startup, a session-start check, and a tool that refuses bad input. A tool can only refuse the calls it receives. The harness is the only layer that sees the write itself, and a soft prompt there ("3 sessions have this file queued in an inbox — edit it directly?") would have caught all three. Two related asks, both cheap: a warning at git add/git commit time when another session has uncommitted edits to files in your index (this alone would have prevented all three incidents), and some way for concurrent sessions on one machine to know about each other at all. Working code for both userland tools is offered below.

---

Environment

  • One developer workstation, Windows.
  • One git clone of a shared documentation repo at ~/.claude/team-docs, plus several ServiceNow

workspace directories.

  • 3–6 Claude Code sessions open at once, each working a different project, all against those same

directories. This is normal for us, not an edge case — different sessions own different products and
releases.

  • Sessions commit to git themselves (with developer approval); the developer pushes.

The essential property: every session shares one working tree and one git index. There are no
per-session checkouts. A file written by one session is visible to all of them the instant it is saved.

---

The failure, three times

Content was never lost in any incident. Attribution was, and two of the three were pushed before
anyone noticed, so the history is public and unrewritable.

1. 4def7c1 — 2026-05-12

A session ran git add / git commit while a parallel session had staged files it had not yet
committed. git commit commits the entire index, not the paths you added, so the other session's
work rode along. The commit is titled "2.53 Phase 2 SHIPPED" and its diff also contains an unrelated
1.24 audit.

Our response: a written protocol. Run git status --short before staging; read both columns (index
vs working tree); un-stage foreign entries; bundle add + commit + verify into one shell invocation to
narrow the race; verify HEAD afterwards; never rewrite history to clean it up.

2. fbf126e — 2026-08-08. The protocol had a hole.

A session finished editing three files, ran git status --short (clean — nothing foreign staged),
presented its scope to the developer, and waited for approval. In that window a parallel session ran
git add and committed. All three files were swept into "docs(ADR-039): 3B-1c-iv SHIPPED". When
approval came back, git commit answered "nothing to commit, working tree clean."

What the protocol missed: steps 1–4 protect the staging window — they assume the danger is a
foreign file entering your commit. This is the mirror case: your own unstaged edits being taken by
someone else's git add.
A clean git status proves nothing about a file's fate, because the working
tree is shared and any session can stage anything in it at any moment.

The corollary we drew: the dangerous gap is edit → commit, not add → commit. Every minute a
finished edit sits uncommitted is a minute another session can adopt it — and a "present the diff, await
approval" ceremony deliberately widens exactly that window.

3. cca8700 — 2026-08-11. And this one had read the protocol.

A session adding a new section to master-work-items.md found a parallel session holding 42 lines of
uncommitted edits in that same file
. It did everything the protocol asks: ran git status --short,
read both columns, identified the foreign edits, and deliberately excluded the file from its own
commit
so as not to sweep the other session's work.

That restraint worked perfectly and was beside the point. The parallel session's next git add carried
the new section into "docs(PM-9): the 08-11 nightly confirms the flake classification…", pushed before
the first session's own commit landed.

The structural finding, which is the useful part of this report:

When a shared file already contains another session's uncommitted edits, there is no clean way to commit your own edit to that same file. Staging it sweeps theirs. Not staging it leaves yours adoptable. Both branches lose something, and "be careful" does not resolve it.

---

How often the exposure occurs — measured, because three anecdotes have no denominator

Added 2026-08-14. The three incidents above are what we noticed. The question that decides whether
this is worth tooling is how often the conditions for one arise, so we measured the 14 days before we
changed anything (2026-07-31 → 2026-08-13) against our highest-contention file,
master-work-items.md — a work-item tracker every project session updates at close-out.

| | 14 days before |
| --- | --- |
| Repo commits, all files | 286 |
| Commits touching master-work-items.md | 106 |
| …as a share of all commits | 37% |
| …that also carried other files | 58 (55%) |
| …that carried 4 or more files | 23 (22%) |
| Active days | 13 → ~8 commits/day against one file |

Be careful what that 55% does and does not say. Most of those bundles are legitimate: a session
updating its work-item row, its ADR and its handoff prompt in one coherent commit. We are not
claiming 58 incidents. We are claiming that in 58 commits this file's authorship was shared with other
work — and that is the population the three confirmed incidents were drawn from. A single file being
touched by ~8 commits a day, across six sessions that cannot see each other, is the exposure. The
incidents are just the times it landed badly enough to notice.

The other number worth stating: 37% of everything committed touched this one file. Whatever
coordination mechanism exists has to be right about that file specifically, far more than about the
repo in general.

---

Mitigation 1: a written protocol — insufficient, and we can say why

It is documented, sessions read it, and it prevented the first failure mode. It cannot prevent the
second or third, because both turn on a fact no amount of care can change: git status reports that a
file is dirty; it never reports who is editing it or why.
You cannot avoid a collision you cannot see.

Two of three incidents happened to sessions that had read this protocol. That is the measurement that
told us to stop writing rules and start building tools.

---

Mitigation 2: session-claims.js — working code, and its measured limit

An advisory claims registry. Before editing a shared file, a session claims it; at commit, it
releases.

node tools/session-claims.js claim   --session project-a-stories master-work-items.md
node tools/session-claims.js who     master-work-items.md
node tools/session-claims.js list          # everything claimed; flags CONTESTED
node tools/session-claims.js release --session project-a-stories
node tools/session-claims.js prune         # drop claims from sessions that died (default 12h)

The design decision worth copying: one file per session, at .session-claims/<label>.json. A
single shared registry file would race exactly like the file it protects — two sessions appending to one
index collide the same way two sessions editing one document do. Because no two sessions ever write the
same path, the registry can never itself become the contention point, and "who holds X" is a lock-free
directory scan. The claims are git-ignored: they are ephemeral facts about one machine's live sessions
and are useless to anyone else.

A CONFLICT result exits 3, so it can gate a script.

What it fixed: collisions became visible. Sessions can now see that a file is held, by whom, and
for how long, and can choose to work elsewhere.

What it did not fix, measured on 2026-08-13: it is advisory. On that day master-work-items.md went
CONTESTED — two sessions held it simultaneously — and nothing stopped either. A third session
(ours) needed to correct two stale rows in that file and simply could not, so the corrections are still
outstanding. Separately, one session's git push carried an unrelated session's commit with it.

Visibility is a real improvement over a rule. It is not a solution.

---

Mitigation 3: the inbox pattern — built 2026-08-13, and measured over 7 days

The insight: every approach above tries to make it safe for two sessions to edit one file. The
alternative is that they never do.

  • Each session writes only to a path unique to it — master-work-items.d/<session-label>.md

containing the rows or fragments it wants added. Zero contention, structurally, because no other
session will ever write that filename.

  • A short merge step folds in every pending fragment, commits, and deletes them.

Why this is stronger: it converts a long edit window — minutes or hours during which a session
holds a contended file, including the time it spends waiting for a human to approve a diff — into a
short append window measured in seconds. The failure mode requires two sessions to be in the same
file at the same moment; this makes that window almost nonexistent rather than merely visible.

We got our own specification wrong, and found out on day one

The paragraph this section replaces said the pattern suits "row-shaped, appendable content", and we
nearly built it append-only on that basis. That was wrong, and it would have failed the exact case
that commissioned the work.

Of the first four operations real sessions queued, three were corrections to existing rows, not
appends
— a story number replacing a needs a story placeholder, a status moving from planned to
shipped. Close-outs mostly revise something a session wrote earlier. And the two updates that were
blocked on 2026-08-13, the ones that motivated building this at all, were both stale flags needing
correction
. An append-only inbox could not have expressed any of them.

So the fragment format carries four directives:

| Directive | Does |
| --- | --- |
| @@ note: <line> | goes into the merge commit body |
| @@ section: <exact heading> | append at the end of that section |
| @@ row: <exact heading> | append rows to the last table in that section |
| @@ replace: <exact existing line> | replace that line (empty block deletes it) |

Everything anchors on a heading or an exact line, never a line number — and that mattered within
hours. One fragment sat queued while other sessions added content above its target; its anchor resolved
to line 2092, then 2149, then 2151, and landed correctly each time. A line-based format would have
written into the wrong section twice.

The tool refuses rather than guesses. A missing or ambiguous heading, a table row whose column count
differs from its header, a @@ replace: target that is not unique, or content with no anchor skips that
fragment whole and leaves it on disk. One session's typo cannot block another's queued work, and
nothing ever half-lands.

The refusal that matters most: if the shared file already carries uncommitted changes, the merge
refuses the entire run and exits non-zero. The fragment stays queued, costing nothing. That is the case
this report's structural finding said had no clean answer — and the answer turns out to be that *not
landing yet* is safe once your work is no longer sitting inside the contended file.

Measured: 2026-08-13 to 2026-08-20

| | 14 days before | 7 days after |
| --- | --- | --- |
| Commits touching master-work-items.md | 106 | 21 |
| …that carried other files | 58 (55%) | 3 (14%) |
| Merge commits through the inbox | — | 18 |
| Fragments folded | — | 23, by 20 distinct sessions |
| Merges folding more than one session | — | 4 |
| Merge commits that carried a foreign file | — | 0 of 18 |
| Fragments lost or abandoned | — | 0 |

Every one of the 18 merge commits contains exactly one file. The bundling that produced all three
incidents went to zero through the pattern — by construction, not by care. Nine of the 23 fragments
landed inside a shared merge commit, which under the old convention would have been nine sequential
edits to one contended file.

It is not airtight, and the way it leaks is the most useful thing we learned

Three sessions edited the file directly anyway — 08-15, 08-18 and 08-20 — despite the rule being in
the standards document every session reads at startup, and despite a session-start check. Roughly one
every two days. The 08-18 one is the worst shape: master-work-items.md bundled with an ADR, a prompt,
the standards document and two tool files in a single six-file commit. Exactly the signature the pattern
exists to eliminate.

Compliance is therefore ~88% (18 inbox merges against 3 direct edits), not 100%, and the residue is
not carelessness in any interesting sense. It is instruction decay: a rule that a session read hours
earlier competes with everything else in its context, and a long session drifts back to the obvious act
of editing the file in front of it. We can write the rule, check it at startup, and build a tool that
refuses bad input — and none of that reaches a session that simply never invokes the tool.

And it cannot cross a machine boundary at all. A second developer's clone has the tool via git
pull
but their sessions were never told the convention; their commits still edit the file directly, and
one of them was a git merge resolving conflicts inside it. Every mitigation in this report is
per-machine userland scaffolding, and the second developer is invisible to all three.

Honest limits

  • It is not a lock. Between the tool reading the file and writing it, another session could write it

too. That window is milliseconds instead of hours, which is the whole of the claim.

  • It does not generalise to prose. Deliberately scoped to the work-item tracker, with no --target

flag. A fragment saying "insert under §X" in a standards document is more fragile than a contested
manual edit. Prose stays on the claims registry — so this is a second mechanism, not a replacement.

  • A queued fragment is invisible until someone runs the merge. The inbox directory is git-ignored,

so a session that dies leaves rows git status will never show. We made the read-only dry run a
session-start check to cover it, but it is a real failure mode and it was ours to find.

  • The tool needed two safety fixes in its first week, both found by other sessions using it: it

committed the whole index rather than by pathspec (so it could sweep what it existed to prevent), and
it deleted fragments even when the commit failed. Neither lost data. Both are the kind of mistake a
userland reimplementation of a coordination primitive invites.

---

What we think belongs at the tool level

Offered as observations from the field, not as a specification. Items 6–8 were added 2026-08-21 after
seven days of running mitigation 3, and they are the ones we could not have written before building it.

  1. Concurrent sessions on one machine do not know about each other. Everything above is us building,

in userland, an answer to "who else is working in this directory right now?" — a question the harness
is much better placed to answer, since it already knows which sessions are open and which files each
has edited.

  1. The risky moment is git add / git commit, not the edit. A warning at commit time — *"session B

has uncommitted edits to 2 of the files in your index"* — would have prevented all three of our
incidents, and requires no locking or coordination protocol.

  1. git commit commits the index, not your paths. This surprises people, and it is the direct cause

of incident 1. A selective git add offers no protection against changes another actor pre-staged.
Our own merge tool shipped with this bug in its first week, in the tool written to prevent it.

  1. Attribution is the damage, and it is silent. In all three incidents the content survived and every

test passed. Nothing failed loudly. The cost lands later, in git blame and in code archaeology, and
by then two of ours were pushed and unrewritable.

  1. A "present the diff and await approval" interaction pattern widens the dangerous window. Anything

that encourages holding finished edits uncommitted while waiting on a human increases exposure. We
now prefer committing promptly and presenting the SHA.

  1. **A convention a session must remember is a convention it will eventually forget — this is the big

one.** Mitigation 3 removes the collision structurally, and still leaked at about one direct edit
every two days: 18 inbox merges against 3 sessions that simply edited the file. The rule was in the
standards document every session reads at startup, plus a session-start check, plus a tool that
refuses bad input. None of that reaches a session that never invokes the tool. A tool can only
refuse the calls it receives.
The harness sees the write itself, which is the only place a
convention about writes can actually be enforced — even a soft *"3 sessions have this file in an
inbox; are you sure you want to edit it directly?"* at write time would have caught all three.

  1. Userland coordination cannot cross a machine boundary, and ours silently didn't. A second

developer's clone received all three mitigations via git pull and adopted none of them, because the
conventions live in that developer's session context, not in the repo. Their commits kept editing the
contended file directly and one was a git merge resolving conflicts inside it. Any coordination
built per-machine has this ceiling; a repo-level or account-level signal does not.

  1. Sessions found real bugs in each other's tools by using them, which is worth designing for. Two

safety defects in our merge tool were caught within a week by other sessions that had only the tool
and its documentation — including the index-vs-pathspec bug in point 3, and consuming queued fragments
even when the commit failed. Concurrency made the tooling better as well as harder; the same sessions
that collide also cross-check.

---

What you can take

Two pieces of working code, both in daily use, both dependency-free, both about 200–600 lines:

  • tools/session-claims.js — the advisory claims registry. The one-file-per-session design has held

up completely; it is the visibility layer and still the right answer for prose documents.

  • tools/merge-work-items.js — the inbox merge tool. Anchors fragments by heading rather than line

number, refuses ambiguous anchors rather than guessing, and refuses the whole run when the target is
already dirty.

Take them, improve them, or treat them as a sketch of the requirement — our interest is in the problem
being solved, not in our solution surviving. If we had to keep one finding, it is point 6: we removed
the structural collision and the residue was purely that a long-running session forgets a rule. That part
is not solvable in userland.

The incident SHAs are ours and the repository is private, but the diffs and commit messages can be shared
on request if the detail is useful.

View original on GitHub ↗

4 Comments

kcarriedo · 8 days ago

This is a remarkably detailed field report -- the measured data (37% of all commits touching one file, 55% bundling unrelated work) makes it concrete in a way most reports like this don't.

Point 6 in your "what belongs at the tool level" section is the crux of it. You've essentially proven that any mitigation that relies on session memory -- a rule read at startup, a claims check, a tool call -- degrades under real conditions. You hit ~88% compliance over 7 days, and the leakage is structurally guaranteed to grow with session duration, not shrink. A long-running session that was informed at startup is a different session by hour 6.

The cross-machine finding (point 7) is just as sharp. Per-machine userland coordination is a ceiling, and your second developer sailed straight through it.

The ask you're describing in points 1-2 -- sessions knowing about each other at the harness level, a warning at commit time when another session has uncommitted edits to your index -- is the right level. The harness sees the write. Userland sees the result of the write.

One thing that might be worth documenting as a known mitigation until this lands natively: per-session git worktrees eliminate the shared-index problem structurally for new sessions (each session's git add operates on its own index), though I know you're constrained to one working directory. The worktree approach has its own lifecycle overhead, but it's the only userland path that removes the race at the index layer rather than trying to coordinate around it.

Thanks for sharing the working code. The inbox pattern (point about the @@ replace: directive handling line-drift correctly) is the kind of thing that only gets right by running it.

dtechApps-rjoy · 8 days ago

Update — the code, and a correction to my own numbers.

Working code, both dependency-free Node:
https://gist.github.com/dtechApps-rjoy/32626192ef63dccac6b6925966659e8e

  • session-claims.js (233 lines) — mitigation 2, the advisory claims registry
  • merge-work-items.js (641 lines) — mitigation 3, the inbox merge tool

The gist README also lists the six mistakes we made building them, which may be the more useful
part: we specified the inbox append-only and were wrong (most real operations turned out to be
corrections to existing rows, not appends); anchoring fragments by line number would have written
into the wrong section twice within hours; and the merge tool shipped committing the whole index
instead of by pathspec
— the exact bug it exists to prevent, inside the tool built to prevent it.

Correction to the report body. It says compliance was ~88% (18 inbox merges against 3 direct
edits). Re-measured today: 19 inbox merges, 24 fragments, and four direct edits — a fourth
occurred the day after I filed this. So ~83%, and the rate is flat at roughly one every two days
rather than tailing off.

That fourth one sharpens the ask rather than weakening it. I had assumed the residue was
long-running sessions whose context had drifted from a rule they read hours earlier. But the newest
violation came from a session working a freshly-started task — i.e. one that had loaded the rule
recently and still edited the file directly, bundling it with two other files. If re-reading the rule
does not prevent it, then re-stating the rule more often is not the fix, and the remaining lever really
is the write itself.

Concretely, the cheapest thing that would have caught all four: a soft check when a session is about
to write a file that another session has queued fragments for, or has claimed. Not a lock — just
"this file is coordinated; are you sure?". The information needed already exists on disk in both
tools' state directories; what's missing is anything at write time that consults it.

One further data point on scope. A second developer's clone received all three mitigations by
git pull and adopted none of them, because the conventions live in session context rather than in
the repo. Their commits kept editing the contended file directly, and one was a git merge resolving
conflicts inside it. Any coordination built per-machine and per-session has that ceiling — which is
also why the userland fix we're now considering is a pre-commit hook, i.e. us reimplementing an
enforcement point at the only boundary we can actually reach.

dtechApps-rjoy · 8 days ago

Update — we built and deployed the enforcement layer. Posting the design now, with no effectiveness data yet.

Following the comment above: the residue was four sessions editing the tracker directly despite the
rule, so we moved enforcement to the commit. Three pieces, all shipped today.

1. A commit-msg hook that refuses a commit touching the tracker.

commit-msg rather than pre-commit, for a reason worth naming: pre-commit cannot see the message,
so it cannot distinguish the merge tool's own legitimate commit from a hand edit.
It allows the merge
tool's commits, a real merge/cherry-pick/revert resolving the file, and an explicit
[inbox-bypass: <reason>] in the message.

The escape hatch is deliberately not --no-verify. That leaves no trace, and an untraceable
override of a convention is how the convention quietly dies; a token stays in git log forever. On
refusal the staged content is left alone, so nothing is lost and the edits can be moved into a
fragment. Verified over six cases in a sandbox clone plus a live refusal on the real repo.

2. A CI job as backstop, because the hook has the same adoption ceiling as every prior mitigation:
it only works where someone installed it. It examines only the commits in the push event, so it can
never fail on history predating the convention. It went green on the push that introduced it.

3. The install is a tool, not a README line — and this is the part I'd flag as evidence rather than
implementation detail. .git/hooks is not versioned, so a tracked hook does nothing until each clone
runs git config core.hooksPath. We wired that into the shared-tool check our sessions already run at
startup, so a machine picks it up without anyone remembering to. It refuses rather than clobbers: an
existing core.hooksPath, or a clone with real .git/hooks, is reported and left alone.

What this cost, which is the actual argument. To enforce one convention about one file we now
ship: a hook, a CI workflow, an installer tool, a session-start check that runs the installer, a
standards-document section explaining all of it, and an audit token convention for the override. Six
artifacts. Every one of them exists to reimplement an enforcement point at a boundary we can reach
(the commit, and the CI server) because we cannot reach the one that actually matters — the write.

A session edits a file and nothing in the world knows until a commit is attempted. Everything above is
a workaround for that single gap.

No effectiveness data yet — deployed today. I don't know whether it closes the leak, and I'd rather
say that than imply it. Two outcomes and both are informative: if the direct-edit rate goes to zero,
that quantifies exactly what a userland enforcement point costs to build and maintain. If it doesn't,
the case for harness-level support is stronger than anything I've written here. I'll report back either
way.

One thing already visible: the escape hatch will be the interesting variable. A refusal a session hits
often enough becomes a habit of typing the bypass token, at which point we're back to a convention
maintained by goodwill — just with better logging.

danbarua · 7 days ago

Have you tried using Subversion instead of Git?

It has native Lock-for-Editing semantics, which is what you're hand-rolling with scaffolding and ceremony.