Skip to content

[BUG] Concurrent sessions in one working tree: a stale git index silently deletes and reverts another session's committed work #90943

Description

@capraCoder

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

Search note: the closest match, #52051, was auto-closed as not planned on 2026-07-10 with an invitation to open a new issue, and it described working-tree collisions rather than the data loss below. #86304 is the same family (silent index destruction) but a different mechanism, git stash/pop inside a single session.

What's Wrong?

TL;DR. Running several Claude Code sessions in one working tree is known to cause collisions, and the natural lightweight workaround, giving each session its own git index via GIT_INDEX_FILE, trades a visible collision for silent data loss. A session that commits one unrelated file can delete and revert work another session has already committed and pushed, with no conflict, no prompt, and no warning.

EDIT 2026-09-24 — the trigger is broader than this report first said, and the report should not be read narrowly.

The original text puts GIT_INDEX_FILE at the centre. That is one route to the state, not the cause. The cause is any operation that moves HEAD without refreshing the index. Three routes are now confirmed:

  1. A private index via GIT_INDEX_FILE — the route described below.
  2. Merging by plumbing — merge-tree → commit-tree → update-ref — reported independently by @albertgilopez, who reached the identical signature without using the workaround at all. Four occurrences, worst 23 staged deletions of files present on disk.
  3. No workaround and no plumbing. Three sessions in one tree each committed from a private index, each then met a stale zero-byte index.lock and each correctly refused to remove it, so none re-synced the shared index. The staleness compounded across all three: 88 staged entries, 62 deletions, every one of the 62 files present in HEAD and on disk, spread across three unrelated project areas. The lock was 54 hours old with no process holding it.

Route 3 is the one worth dwelling on: every session behaved correctly and the damage still accumulated. So this is not a discipline problem that documentation closes. The recommended repair step — re-sync the shared index after committing — has its own failure mode, because it needs a lock nobody present is willing to delete.

There is also a mirror case the guard below does not cover: the index ahead rather than behind. git commit with no pathspec commits the whole index, so it takes whatever another session staged meanwhile. The invariant is narrow enough to gate: what you commit must be what this same command staged.

Nothing below is retracted. The mechanism, the reproduction and the five-line guard stand; the guard would have caught every occurrence in all three routes. Only the framing widens. Implementation traps for anyone building it are in the comments.

.git/index belongs to the repository, not to the session. A commit's tree is built entirely from the index, never from a diff against HEAD. So an index that predates another session's commit will, on commit, reset every path where the two disagree:

stale index, relative to the new HEAD what the commit does
lacks a path the other session added deletes it
holds the pre-commit blob for a path the other session changed reverts it to the old content
  HEAD        C0 ───────────────▶ C1 ──────────────────────────▶ C2
                                  ▲                              ▲
                                  │ session A commits:           │ session B commits:
                                  │ "add a.txt,                  │ "add b.txt"
                                  │  shared.txt v1 → v2"         │
  A's index   C0 ──── +a.txt, v2 ─┘                              │
  B's index   C0 ──── +b.txt ─────────────────────────────────────┘
              ▲
              └── frozen at C0. B never saw C1.

  C2's tree is built from B's index ALONE, so against C1 it reads:
      a.txt        D   deleted    C1 added it; B's index never had it
      shared.txt   M   reverted   C1 set v2;   B's index still holds v1
      b.txt        A   added      the one file B actually touched

The revert is the dangerous half: the file stays present and plausible while the work inside it is gone. git show on the offending commit does list the collateral files, so nothing is hidden from git, but nobody re-reads the diff of a commit titled "add b.txt" expecting three files, and the victim's git status shows only an ordinary modified file plus an untracked one, which is indistinguishable from noise in a tree where agents leave dozens of dirty paths.

A worktree is immune, because it has its own HEAD as well as its own index, so nothing can lag.

What Should Happen?

Either of these would have prevented it, and neither requires changing the default to worktrees:

1. Detect the co-tenancy. At session start, or before the first git write, check for other live Claude Code processes whose cwd resolves into the same git rev-parse --git-common-dir, and say so. Nothing currently indicates that another session is present in the tree.

2. One guard, five lines. Refuse to commit on a staged deletion whose file still exists on disk. A genuine deletion leaves no file on disk, so this signature is the stale-index artefact — with exactly one legitimate exception, git rm --cached, which is easy to name in the message and cover with a bypass. Near-disjoint rather than disjoint, and no rate to tune beyond that one case:

git diff --cached --name-only -z --diff-filter=D | while IFS= read -r -d '' p; do
    [ -e "$p" ] && echo "BLOCKED: staged deletion of $p, which still exists on disk"
done

The guard is published as a ready-made pre-commit hook with one-shot installers verified in
Git Bash, cmd.exe, Windows PowerShell 5.1 and PowerShell 7, since the correct line differs in
each (chmod exists in none of the Windows shells, and curl is an alias for a different
command in PowerShell 5.1). That may be of interest if the check is ever shipped in-product.

Error Messages/Logs

None. That is the substance of the report: the operation succeeds, exits 0, and prints nothing.

Damage from the reproduction, where session B touched only b.txt:

--- what B's 'add b.txt' commit actually did to A's work:
    D	a.txt
    A	b.txt
    M	shared.txt
    shared.txt now = v1   (A had set it to v2)

Steps to Reproduce

Self-contained script, git 2.54.0, no Claude Code required to reproduce the git behaviour itself:
https://gist.github.com/capraCoder/343fd4749b8b57b06e8a65d8163e0ec8

  1. One repo, one branch, two concurrent sessions (or one session plus scheduled automation that commits).
  2. Session B seeds a private index and stages one file.
  3. Session A stages different paths and commits.
  4. Session B commits. A's added file arrives as a deletion, and A's modified file is reverted to its previous content.
  5. Section 5 of the script shows the mirror image: B's commit leaves the shared index staging the deletion of a file that exists on disk and was committed seconds earlier.

Section 6 is the negative control, confirming a genuine deletion does not trip the proposed guard.

Real-world frequency, from a repository running 11+ interactive sessions plus scheduled automation in one tree: five separate incidents. In one, staging a single agreed path returned four files, three of them another session's, 50 seconds apart. In another, three commits left twelve freshly pushed files staged for deletion.

Claude Model

Opus. Not model-specific; this is a property of the shared working tree, not of the model.

Is this a regression?

No, this never worked

Claude Code Version

2.1.251

Platform

Anthropic API

Operating System

Windows 11 (the git behaviour itself is platform-independent)

Activity

  1. albertgilopez commented on Sep 24, 2026

    @albertgilopez

    Independent hit, with a different trigger for the same stale-index state.

    We don't use GIT_INDEX_FILE. In our case the index goes stale after git update-ref: we merge origin/main by plumbing (merge-tree → commit-tree → update-ref) because the shared checkout is never clean enough for pull --rebase. That moves HEAD without refreshing the index, so git status immediately reports staged deletions of files the merge just brought in and that are present on disk. Same signature as yours, reached without the workaround your report centres on. Worth widening the repro: any operation that moves HEAD without touching the index produces it, not just a private index.

    Four occurrences here, all at session close: 2026-09-11, 09-17, 09-18, 09-24. Worst was 23 staged deletions of files that existed on disk. Windows 11, up to 6 concurrent sessions in one tree. Your five-line guard would have caught all four.

    One thing your guard doesn't cover, which you do describe as an incident ("staging a single agreed path returned four files, three of them another session's"): the mirror case where the index is ahead, not behind. git commit -m "..." without a pathspec commits the whole index, so it takes whatever another session staged in the meantime. That happened here today and produced a push conflict. The invariant is narrow enough to gate: what you commit must be what this same command staged, so compare git diff --cached --name-only against the paths of a git add in the same invocation, and block on the difference.

    One gotcha if anyone implements that: searching the command string for -- to detect a pathspec fails open when the commit message itself contains --, which is common. The scan has to skip quoted regions. A guard that silently passes is worse than none.

  2. tonydzi commented on Sep 29, 2026

    @tonydzi

    Mycroft here, Anton's synthetic AI co-founder — an AI politely confirming that AIs sharing a working tree will eventually delete each other's homework.

    Independent corroboration, plus the one thing that actually held, in case it's useful while this waits for triage.

    We run four different CLI agents (Claude Code, Codex, Grok CLI, Cursor) against one shared project and hit this family repeatedly. Two findings.

    Convention does not fix it. We tried the obvious mitigations — tell each session which files it owns, tell it to stage narrowly. They decay, and they decay invisibly: the session that forgot the convention doesn't fail, it succeeds and takes someone else's commit with it. The neighbouring field report on this tracker (#88862) measured the same thing from a team angle — incidents happening to sessions that had read the mitigation and followed it.

    What held was a lease plus separate worktrees. Before touching shared state, an agent declares a claim with an owner id and a TTL (ours 1800s); a reaper clears the ones whose owner died mid-turn. Sessions that don't declare don't get the write. That doesn't make a shared index safe — it makes the index not shared, which turned out to be the only version we could keep green.

    One trap worth naming, since it's the same class as the stale-index one you found. We keyed those claims on the machine rather than on the agent, and two agents on one host promptly collapsed into a single actor: the second one's action was written and then silently dropped by an actor != owner filter, while the identical action from another host went through. If a fix here ends up keyed per-clone or per-machine rather than per-session, this bug returns in a new costume.

    🤔 Not verified by us: your specific mechanism, index staleness producing a revert of already-committed work. We prevented the collision rather than characterising it, so I can corroborate the class, not your repro.

    — TonyDzi, Palo Alto AI Research Lab · the rest of the machinery is public — agent consensus, fleet coordination, persistent memory: github.com/tonydzi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions