Two Parallel Agents Edit the Same File

Parallel agents overwrite each other's changes, causing lost edits and broken code. Here's how to detect conflicts and enforce file-level locking.

You split a large refactor across two Claude Code instances running in parallel — one handles src/auth/ and the other handles src/api/. Both decide src/utils/helpers.ts needs changes. Agent A writes a new formatToken() helper. Agent B, running concurrently, reads the original file, 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 now throws TypeError: formatToken is not a function at runtime. No conflict marker, no warning, just silent data loss.

Common causes

1. No file-level lock registry in the orchestration layer

Most agent orchestration frameworks — LangGraph, CrewAI, AutoGen — do not implement file locks by default. Two agents can read the same file at t=0, make independent edits, and both write back. The second write wins and the first is gone.

How to spot it: Check your orchestration code for any mutex, lock, or file-claim mechanism before file writes. If there is none, concurrent edits are possible whenever agents share a working directory.

2. Shared utility files not excluded from agent scope assignments

When you partition work (“Agent A owns auth, Agent B owns api”), utility files (helpers.ts, constants.ts, types.ts) sit outside both domains but are edited by both. No one explicitly said “only one agent may edit shared utilities at a time.”

How to spot it: List every file edited in the last run with git diff --name-only. Files that appear in both agents’ edit lists are conflict candidates.

3. Agents read a stale file version before writing

In async pipelines with steps that run within milliseconds of each other, both agents complete a read_file call before either writes. Both then generate an edit based on the same original content. The second write silently discards everything from the first.

How to spot it: Compare timestamps of read_file and write_file tool calls across agents in the trace. If two read_file calls for the same path complete before any write_file, a lost-write race is certain.

4. Git worktrees not used — agents share a single working tree

Running multiple Claude Code instances in the same repository directory means they all edit files in the same working tree. Any concurrent write to the same file is immediately visible to the OS and the last writer wins.

How to spot it: Check if all agent processes share the same cwd. Run lsof | grep helpers.ts to see multiple processes with write handles to the same file.

5. Merge step missing from the orchestration workflow

The pipeline fans out to parallel agents and fans back in, but the fan-in step just takes the last agent’s output. There is no explicit merge or conflict resolution step. Whoever finishes last “wins” even if their output is incomplete.

How to spot it: Review the fan-in node in your workflow graph. If it calls state.update(agent_output) without checking for conflicts, the last writer wins by default.

Shortest path to fix

Step 1: Detect which files were touched by multiple agents

# After a parallel run, check git for concurrent edits
git diff --name-only HEAD~1 HEAD > agent_a_files.txt
# (repeat for agent B's run on its branch)
comm -12 <(sort agent_a_files.txt) <(sort agent_b_files.txt)

Or instrument your agents to log every file write to a shared list and diff afterward.

Step 2: Add a file-claim registry before any write

import threading
import os

_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:
        if abs_path not in _file_locks:
            _file_locks[abs_path] = threading.Lock()
    lock = _file_locks[abs_path]
    acquired = lock.acquire(timeout=30)
    if not acquired:
        raise RuntimeError(f"Could not claim file within 30s: {abs_path}")
    return lock

def release_file(path: str):
    abs_path = os.path.abspath(path)
    _file_locks[abs_path].release()

# Usage:
lock = claim_file("src/utils/helpers.ts")
try:
    content = read_file("src/utils/helpers.ts")
    new_content = agent.edit(content)
    write_file("src/utils/helpers.ts", new_content)
finally:
    release_file("src/utils/helpers.ts")

For distributed agents (separate processes or machines), use Redis-backed locks instead of threading locks.

Step 3: Use Git worktrees to give each agent an isolated working tree

# Create isolated worktrees for each parallel agent
git worktree add ../agent-a-work main
git worktree add ../agent-b-work main

# Run agents in their respective directories
cd ../agent-a-work && run_agent_a &
cd ../agent-b-work && run_agent_b &

# After both complete, merge their branches
git checkout main
git merge agent-a-branch
git merge agent-b-branch  # resolve conflicts here, not silently

Worktrees surface file conflicts as proper Git merge conflicts, which are explicit and recoverable.

Step 4: Explicitly partition shared files in agent scope definitions

AGENT_A_SCOPE = {
    "owned": ["src/auth/**"],
    "read_only": ["src/utils/**"],  # can read but not write
    "forbidden": ["src/api/**"]
}

AGENT_B_SCOPE = {
    "owned": ["src/api/**"],
    "read_only": ["src/utils/**"],
    "forbidden": ["src/auth/**"]
}

Mark shared utilities as read_only for all parallel agents. Changes to shared files should go through a single “shared-file editor” agent that runs after the parallel phase.

Step 5: Add a conflict-detection step to your fan-in node

def fan_in(state: dict, agent_outputs: list[dict]) -> dict:
    all_edited_files = [set(out["edited_files"]) for out in agent_outputs]
    # Check for overlap
    for i, files_a in enumerate(all_edited_files):
        for j, files_b in enumerate(all_edited_files):
            if i >= j:
                continue
            overlap = files_a & files_b
            if overlap:
                raise ConflictError(
                    f"Agents {i} and {j} both edited: {overlap}"
                )
    # Only merge if no conflicts
    return merge_outputs(agent_outputs)

Prevention

  • Define explicit file ownership scopes for every parallel agent before launching them; mark shared files as read-only during the parallel phase.
  • Use Git worktrees for any parallel workload that involves file edits — conflicts surface as merge conflicts, not silent overwrites.
  • Implement a file-claim registry (in-process locks for same-machine agents, Redis locks for distributed) and wrap every write call with a claim/release.
  • Add a conflict-detection step to your fan-in node that compares edited-file sets across all parallel agents before merging.
  • Schedule shared-file edits in a sequential post-parallel phase, not during the parallel phase.
  • Log every file write with agent ID and timestamp; set up alerts if the same file is written twice within a time window.
  • For large refactors, run a dry-run pass first that lists which files each agent would edit, then review for overlap before the real run.
  • Use atomic file writes (write to a temp file, then os.rename()) to prevent partial writes from corrupting files even if a lock fails.

FAQ

Q: Does LangGraph prevent this automatically? A: No. LangGraph’s State merges happen through reducers you define. If two parallel nodes both update the same key in state with operator.add, edits are appended. If they use default dict assignment, the last writer wins. File system writes outside of LangGraph state get no protection at all.

Q: Can I use Git’s own merge to resolve conflicts after the fact? A: Yes, if you use worktrees and commit each agent’s changes to a separate branch. git merge or git rebase will surface conflicts with markers. This is the most reliable approach for large parallel refactors and does not require custom locking code.

Q: What if two agents need to edit the same file legitimately? A: Serialize their access. Agent A edits and commits the file; Agent B reads the post-A state before editing. If both edits are truly independent (different functions, non-overlapping lines), a sequential approach adds little overhead and avoids all conflict risk.

Q: Does using separate Docker containers per agent solve this? A: It prevents file conflicts within a shared directory, but you still need a merge step to combine their outputs. Containers give you strong isolation; they do not give you automatic merge semantics. You still need the worktree + explicit merge approach.

Tags: #AI coding #Agents #Troubleshooting