Last updated on

When Two AI Agents Edit the Same File: A git stash Gotcha


I've started running several Claude Code sessions against the same repository at once — one per issue, each chugging away in parallel. It's a genuinely nice way to work: while one agent documents a migration path, another cleans up a folder structure, and a third refactors a component. But "several editors sharing one working tree" is an old distributed-systems problem wearing a new hat, and last week it bit me in a way that's worth writing down.

The short version: I let an agent edit files directly on main while other sessions were also touching that checkout, then tried to migrate the work into an isolated git worktree with git stash. Because git stash push -- <file> operates at whole-file granularity, it swept up another session's edits to a file we'd both touched. That briefly yanked their work out of main and dragged it onto my branch. Untangling it was avoidable. Here's the anatomy.

The setup: a worktree per session

The pattern I use is one git worktree per concurrent task:

.claude/worktrees/issue-20-backups-folder   → branch for issue #20
.claude/worktrees/issue-18-css-modules       → branch for issue #18
.claude/worktrees/issue-39-taxonomies        → branch for issue #39

Each worktree is a separate working directory sharing one .git. A session that stays inside its own worktree is fully insulated from the others: separate files on disk, separate index, its own branch. Conflicts, if any, surface at merge time in the PR — which is exactly where you want them, reviewable and explicit. (Reviewable isn't the same as visible, though. Two branches can merge without a single conflict marker and still be wrong together — when two migrations collide is that failure mode.)

The whole scheme depends on one discipline: enter the worktree before you start editing. I didn't. I started making changes straight in the shared main checkout, figuring I'd tidy up later. That's the original sin; everything downstream flowed from it.

The incident

Partway through, I noticed the shared checkout had changes I didn't recognize — a component and a stylesheet that another session was clearly in the middle of editing. Good instinct to isolate now. So I did the natural thing: stash my files, create the worktree, pop them there.

$ git stash push -m "issue-20 work" -- .gitignore README.md scripts/backup-prod-db.sh

Note the pathspec — I was being careful, only naming my three files so I wouldn't disturb the other session's component work. And that part worked. But here's the trap: README.md was a file both sessions had modified. My change touched the "backups" section; theirs touched a "migrations" section further down. Two disjoint edits to the same file.

git stash push -- <pathspec> doesn't stash hunks. It stashes whole files. So "stash my README changes" actually meant "stash all uncommitted changes to README.md, regardless of who made them." The other session's migration edits went into my stash, and main's README reverted to HEAD — their work vanished from the shared checkout. When I popped the stash inside the new worktree, their edits landed on my branch, interleaved with mine.

Why this is easy to miss

The mental model most of us carry is that a pathspec narrows the operation. git add -- file, git checkout -- file, git diff -- file — all scoped to that file. So git stash push -- file feels surgical. And it is — at the granularity of files, not changes. If a file has changes from two sources, the pathspec can't tell them apart, because from git's perspective there's only one working-tree version of that file. There's no "my hunks" and "their hunks"; there's just the file, dirty.

With a single human at the keyboard this never matters — all the dirt in a file is yours. The failure mode is specifically multiple concurrent editors sharing one working tree, which is precisely the world parallel AI agents create.

The recovery, and the one thing that saved me

Because the two edit sets were now tangled together in one working tree, I had to pull them apart by hand: my backups edits back onto the worktree branch, their migration edits back onto main.

The thing that made this clean rather than a guessing game: my edits were exact string replacements. Each one had a precise "before" and "after." So I could reverse them deterministically — replace each "after" with its "before" — to strip my changes out of the combined file and leave exactly the other session's work behind. Then re-apply my changes fresh to a clean copy for the worktree. It was a mechanical, verifiable undo: no fuzzy three-way merge, no "did I get all of it?"

That's a small argument for a habit that pays off in lots of places: make edits that are precise and self-contained. Surgical string replacements are reversible. That's what turns a scary recovery into arithmetic.

The right way to migrate dirty work into a worktree

If you find yourself where I was — real changes already sitting in a shared checkout that other people (or agents) are also editing — don't move them with **git stash push -- <file>** on any file others have touched. Instead:

  1. Create the worktree from a clean origin/main base.
  2. Re-apply only your own hunks there, fresh.

Yes, it means re-typing your changes into the isolated branch. That's cheaper than surgically de-interleaving two sessions' work out of one file after the fact — and it never disturbs the shared checkout at all.

Better still, of course: don't dirty main in the first place. Enter the worktree before the first edit and none of this can happen.

A bonus gotcha: squash merges and "unmerged" warnings

One more thing surfaced at cleanup time. After the PR merged, I went to remove the worktree, and the tooling refused: "1 commit will be discarded." But the PR was merged — I'd just watched it happen.

The culprit is squash merges. A squash merge takes your branch's commits, flattens them into a single new commit on main, and lands that. Your original commit's SHA is nowhere in main's history. So any check that asks "is this commit reachable from main?" (like git branch --merged) answers "no," even though the content is fully merged.

The fix is to verify against the source of truth for merge state, which is the PR, not the local commit graph:

$ gh pr view --json state,mergedAt

Confirm there, then delete the branch without worrying about the graph-reachability warning.

Takeaways

  • Isolate before you edit. With parallel agents on one repo, a worktree per task is the unit of isolation. The discipline is starting in it, not retrofitting into it.
  • git stash push -- <file> is whole-file, not per-hunk. On a file multiple sessions have edited, it captures everyone's changes to that file. Migrate your work by re-applying your hunks to a clean branch instead.
  • Precise, reversible edits are a recovery tool. Exact string replacements can be mechanically undone. That property is worth more than it looks the day something goes sideways.
  • Trust the PR for merge state, not the local graph. Squash merges make merged commits look unmerged. Check gh pr view, not git branch --merged, before you discard anything.

None of this is exotic. It's the same coordination problem shared mutable state always poses, with agents rather than people as the concurrent writers. The tools already have the right answer (worktrees); the discipline is using them from the first keystroke. And the working tree isn't the only shared state that needs isolating: this site's preview deploys were quietly sharing production's database until the preview database that was still production.

Comments

No comments yet. Be the first to comment.

Leave a comment