You split a large refactor across two parallel agents — one handles src/auth/, the other handles src/api/. Both decide src/utils/helpers.ts needs changes. Agent A writes a new formatToken() helper. Agent B, running concurrently, read the original file before Agent A wrote, adds a parseHeaders() function, and writes it back — overwriting Agent A’s formatToken() entirely. The final file has parseHeaders() but no formatToken(), and the auth module throws TypeError: formatToken is not a function at runtime. No conflict marker, no warning, just silent data loss.
Fastest fix (as of June 2026): give each agent its own filesystem so they physically cannot overwrite each other, then merge at the end. If you run Claude Code, this is now built in — start each session with claude --worktree <name> (native worktree support shipped in early 2026), or add isolation: worktree to a subagent’s frontmatter, and each agent edits a private checkout. For framework orchestration (LangGraph, CrewAI, AutoGen) the equivalent is: write through a single serialized merge step, never let two parallel nodes write the same file path. The rest of this page covers diagnosis, the built-in fix, and the manual fix for non-Claude setups.
Which bucket are you in?
| Symptom | Most likely cause | Jump to |
|---|---|---|
| Edits silently vanish, no conflict marker | Two agents share one working tree, last write wins | Step 1, Step 2 |
LangGraph throws INVALID_CONCURRENT_GRAPH_UPDATE | Parallel nodes write the same state key without a reducer | Step 5 |
git status shows real merge conflict markers | You already used worktrees/branches (this is the safe case) | Step 2, FAQ |
| Same file edited twice by two agents with identical task | Duplicate/retried task dispatched to two workers | Prevention |
CrewAI raises a LockException under multiprocess | Stale lock backend in an older CrewAI release | FAQ |
Common causes
1. Agents share a single working tree (last writer wins)
Running multiple Claude Code instances, or any agents, in the same repository directory means they all edit files in one working tree. Two agents read the same file at t=0, make independent edits, and both write back. The OS keeps only the second write; the first is gone with no conflict marker.
How to spot it: Check whether all agent processes share the same cwd. On macOS/Linux run lsof | grep helpers.ts (or fuser src/utils/helpers.ts) while a run is in progress to see multiple processes holding write handles to the same file. In Claude Code, run git worktree list — if there is only one worktree and you launched several sessions, they are colliding.
2. No file-level lock in the orchestration layer
Most framework orchestration — LangGraph, CrewAI, AutoGen — does not lock files by default. The agent’s write_file / Write tool overwrites the whole file. Even when two agents edit different functions, the second write uses the version it read (which lacks the first agent’s change) and clobbers it.
How to spot it: Grep your tool wrappers for any mutex, filelock, or file-claim mechanism before writes. If there is none, concurrent edits to a shared path are unprotected.
3. Shared utility files not excluded from agent scopes
When you partition work (“Agent A owns auth, Agent B owns api”), shared utilities (helpers.ts, constants.ts, types.ts, package.json, tsconfig.json) sit outside both domains but get edited by both. No one declared “only one agent may write shared utilities.”
How to spot it: List every file each agent touched and look for overlap. With git, git diff --name-only on each agent’s branch, then intersect the lists (see Step 1).
4. Agents read a stale file version before writing
In async pipelines whose steps run within milliseconds of each other, both agents finish a read_file call before either writes. Both generate edits from the same original content, so the second write discards the first.
How to spot it: In the run trace, compare timestamps of read_file and write_file tool calls. If two read_file calls for the same path both complete before any write_file, a lost-write race is guaranteed.
5. Fan-in step has no merge or conflict check
The pipeline fans out to parallel agents and fans back in, but the fan-in node just takes the last agent’s output (state.update(agent_output)). There is no merge or conflict resolution. Whoever finishes last wins, even if their output is incomplete.
How to spot it: Review the fan-in node in your workflow graph. In LangGraph specifically, if two parallel nodes write the same state key and that key has no reducer, the runtime raises INVALID_CONCURRENT_GRAPH_UPDATE instead of silently losing data — that error is actually the framework protecting you.
Shortest path to fix
Step 1: Detect which files were touched by more than one agent
# Each agent should commit to its own branch. Then intersect the file lists:
git diff --name-only main...agent-a-branch | sort > agent_a_files.txt
git diff --name-only main...agent-b-branch | sort > agent_b_files.txt
comm -12 agent_a_files.txt agent_b_files.txt # files both agents edited = conflict candidates
If your agents all share one tree (no branches), instrument the write tool to append agent_id, path, timestamp to a shared log, then look for the same path written by two agent IDs.
Step 2: Isolate each agent in its own worktree
A git worktree is a separate working directory with its own files and branch that shares one .git history. Two agents in two worktrees physically cannot overwrite each other’s files; real conflicts surface as ordinary git merge conflicts you can resolve, not silent loss.
Claude Code (built-in, as of June 2026): Native worktree support shipped in early 2026. Start each session in its own worktree:
# Creates .claude/worktrees/feature-auth/ on branch worktree-feature-auth
claude --worktree feature-auth
# In a second terminal:
claude --worktree feature-api
--worktree (short flag -w) branches from origin/HEAD by default; set worktree.baseRef to "head" in settings to branch from your current local HEAD instead. Add .claude/worktrees/ to .gitignore. Because a fresh checkout has no untracked files, copy local config (.env, etc.) by adding a .worktreeinclude file (uses .gitignore syntax). When the session exits clean, Claude removes the worktree and its branch automatically; if you made changes, it prompts to keep or discard. While an agent runs, Claude holds a git worktree lock so a cleanup sweep cannot delete it.
Manual (any tool):
git worktree add ../agent-a-work -b agent-a-branch
git worktree add ../agent-b-work -b agent-b-branch
cd ../agent-a-work && run_agent_a &
cd ../agent-b-work && run_agent_b &
wait
# Merge explicitly — conflicts become real, recoverable git conflicts
git checkout main
git merge agent-a-branch
git merge agent-b-branch
git worktree remove ../agent-a-work && git worktree remove ../agent-b-work
Step 3: For Claude Code subagents, set worktree isolation in frontmatter
If you delegate to subagents within one session, set isolation permanently so each subagent edits a private checkout. Each subagent gets a temporary worktree that is removed automatically when it finishes without changes:
---
name: refactor-helper
description: Refactors a module in isolation
isolation: worktree
---
You refactor the assigned module...
Claude Code Agent Teams build on the same mechanism: each teammate gets its own worktree, and a shared task list uses status flags to claim work so two teammates never pick up the same file. Even when a backend and a frontend agent both touch package.json, they edit private copies and the merge happens once at the end.
Step 4: If you cannot isolate, claim the file before any write
When agents must share one tree (no worktrees), serialize writes with a lock. Use in-process locks for same-machine agents; use a Redis-backed lock for agents in separate processes or machines. Always write atomically (temp file + rename) so a crash mid-write cannot corrupt the file:
import threading, os, tempfile
_file_locks: dict[str, threading.Lock] = {}
_registry_lock = threading.Lock()
def claim_file(path: str) -> threading.Lock:
abs_path = os.path.abspath(path)
with _registry_lock:
lock = _file_locks.setdefault(abs_path, threading.Lock())
if not lock.acquire(timeout=30):
raise RuntimeError(f"Could not claim file within 30s: {abs_path}")
return lock
def atomic_write(path: str, content: str) -> None:
d = os.path.dirname(os.path.abspath(path))
fd, tmp = tempfile.mkstemp(dir=d)
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
os.replace(tmp, path) # atomic on POSIX and Windows
# Usage: read the CURRENT file inside the lock, so you never edit a stale copy
lock = claim_file("src/utils/helpers.ts")
try:
content = read_file("src/utils/helpers.ts")
atomic_write("src/utils/helpers.ts", agent.edit(content))
finally:
lock.release()
The key detail is reading the file inside the lock — that closes the stale-read race from cause 4.
Step 5: In LangGraph, give parallel state keys a reducer
If parallel nodes write the same state key, do not rely on default assignment (last write wins). Annotate the key with a reducer so writes combine instead of overwrite. This is also the fix for the INVALID_CONCURRENT_GRAPH_UPDATE error LangGraph raises when an un-reduced key gets concurrent writes:
from typing import Annotated, TypedDict
import operator
class State(TypedDict):
# parallel branches append their file edits instead of overwriting
edited_files: Annotated[list[str], operator.add]
For actual file writes (outside graph state), keep the fan-out nodes pure: have each parallel node return a patch/diff object, and apply all patches sequentially in the fan-in node. Serial application removes the race entirely.
Step 6: Add a conflict check to your fan-in node
def fan_in(state: dict, agent_outputs: list[dict]) -> dict:
edited = [set(out["edited_files"]) for out in agent_outputs]
for i in range(len(edited)):
for j in range(i + 1, len(edited)):
overlap = edited[i] & edited[j]
if overlap:
raise ConflictError(f"Agents {i} and {j} both edited: {overlap}")
return merge_outputs(agent_outputs)
How to confirm it’s fixed
- Re-run the same parallel task. Afterward run the intersect from Step 1 —
comm -12should print nothing, meaning no file was written by two agents. - Confirm both agents’ contributions survived:
git log --onelineshould show both branches merged, andgrep formatToken src/utils/helpers.ts(and the other agent’s symbol) should both return a hit. - Run the build/tests. The original
TypeError: formatToken is not a functionshould be gone. - If you used worktrees,
git worktree listshows one per active agent during the run and prunes back to the main checkout afterward.
Prevention
- Use worktree isolation for any parallel workload that edits files. In Claude Code that means
--worktreeper session orisolation: worktreeper subagent; outside it,git worktree addper agent. Conflicts then surface as real merge conflicts, never silent overwrites. - Define explicit file-ownership scopes before launching agents; mark shared files (
tsconfig.json,package.json,types.ts) as read-only during the parallel phase and route their edits through a single sequential post-parallel step. - Read the current file inside the write lock, not before, so no agent edits a stale copy.
- Use atomic writes (temp file then
os.replace()) so a failed write or crash cannot corrupt a file even if a lock is dropped. - Give every parallel-written LangGraph state key a reducer (
Annotated[..., operator.add]); add an overlap check to the fan-in node before merging. - Make task IDs idempotent so a retried or duplicated task is not processed by two workers (a common source of double-writes).
- Run a dry pass first that lists which files each agent would edit, and review for overlap before the real run.
FAQ
Q: Does Claude Code prevent this automatically now?
A: Only if you turn on isolation. Native worktree support shipped in early 2026, but several sessions launched in the same directory still share one working tree and can overwrite each other. Use claude --worktree <name> per session, or set isolation: worktree on the subagent, and each agent edits a private checkout that merges at the end. As of mid-2026, teams report 4–8 concurrent worktrees per developer as the reliable range before review becomes the bottleneck.
Q: Does LangGraph prevent this automatically?
A: For graph state, yes-ish: if two parallel nodes write the same key without a reducer, LangGraph raises INVALID_CONCURRENT_GRAPH_UPDATE rather than losing data silently. Fix it by annotating the key, e.g. Annotated[list, operator.add]. But raw filesystem writes outside graph state get no protection — you still need worktrees or a write lock.
Q: Can I just use Git’s own merge to fix conflicts after the fact?
A: Yes, if each agent commits to its own branch (which worktrees give you for free). git merge surfaces overlapping changes as conflict markers you resolve by hand. Non-overlapping edits to the same file merge automatically. This is the most reliable approach for large parallel refactors and needs no custom locking code.
Q: CrewAI throws a LockException when I run tasks across processes.
A: That was a known issue with the lock backend in older CrewAI releases. A 2026 release fixed the lock exceptions under concurrent multi-process execution and made the locking backend overridable. Upgrade to the latest CrewAI, then confirm the installed version with pip show crewai.
Q: Do separate Docker containers per agent solve this? A: Containers give strong isolation of the file edits, but not merge semantics — you still need a step to combine each container’s output. That is the same worktree-plus-explicit-merge model, just with heavier isolation.
Q: Two agents genuinely need to edit the same file. What then? A: Serialize them. Agent A edits and commits; Agent B reads the post-A state before editing. If the edits are truly independent (different functions, non-overlapping lines), sequential execution adds little overhead and removes all conflict risk.