How to Prevent File Conflicts Between AI Coding Agents: Git Worktrees vs File Locking
How can multiple AI coding agents work on the same codebase without stepping on each other’s toes?
I ran into this problem last month. I had three Claude Code agents running in parallel - one refactoring the authentication module, one updating the database layer, and one working on API endpoints. All three tried to edit src/types/index.ts at the same time. The result? Two agents’ changes got overwritten, and I spent an hour untangling the mess.
After researching how other teams handle this, I found two main approaches: file locking and git worktrees. Each has its place, but they solve the problem differently.
The Problem: Parallel AI Agents, One Codebase
When you run multiple AI coding agents simultaneously, they all see the same file system. If Agent 1 and Agent 2 both try to edit auth.py, one of them loses.
Agent 1 reads auth.py (10:00:00)Agent 2 reads auth.py (10:00:01)Agent 1 writes auth.py with changes (10:00:02)Agent 2 writes auth.py with DIFFERENT changes (10:00:03)Agent 1's changes are GONEThis isn’t theoretical. It happened to me, and I’ve seen it discussed on Reddit where someone reverse-engineered Claude Code to build a better orchestrator. The coordination problem is real.
Approach A: File Locking
The simpler solution. Each agent claims exclusive access to files before editing.
How it works:
class FileLockManager: def __init__(self): self.locked_files = {}
def acquire(self, agent_id: str, file_path: str) -> bool: if file_path in self.locked_files: return False # Already locked by another agent self.locked_files[file_path] = agent_id return True
def release(self, agent_id: str, file_path: str): if self.locked_files.get(file_path) == agent_id: del self.locked_files[file_path]
def is_locked(self, file_path: str) -> bool: return file_path in self.locked_filesUsage in an agent:
async def safe_edit(agent_id: str, file_path: str, changes: dict): if not lock_manager.acquire(agent_id, file_path): raise FileLockError(f"{file_path} is locked by another agent")
try: current_content = read_file(file_path) updated_content = apply_changes(current_content, changes) write_file(file_path, updated_content) finally: lock_manager.release(agent_id, file_path)With retry logic for busy files:
async def edit_with_retry(agent_id: str, file_path: str, changes: dict, max_retries: int = 3): for attempt in range(max_retries): if lock_manager.acquire(agent_id, file_path): try: return await apply_file_changes(file_path, changes) finally: lock_manager.release(agent_id, file_path)
# Exponential backoff before retry await asyncio.sleep(2 ** attempt)
raise FileLockTimeoutError(f"Could not acquire lock on {file_path} after {max_retries} attempts")Pros:
- Simple to implement and understand
- No merge conflicts to resolve
- Works well for naturally file-disjoint tasks
Cons:
- Limits parallelism - if two agents need the same file, one waits
- Can create bottlenecks on shared files like
types/index.ts - Risk of deadlocks if Agent 1 holds file A and needs file B while Agent 2 holds file B and needs file A
Claude Code’s built-in approach uses a variation of this. The Reddit discussion mentioned: “tasks have to be file-disjoint. If two agents need to touch the same file, one has to wait.”
Approach B: Git Worktrees
This is what the custom agent system (CAS) described in the Reddit thread uses. Each agent gets its own isolated copy of the repository on its own branch.
How it works:
main-repo/ # Original repo, main branch├── src/└── ...
.worktrees/├── agent-1/ # Agent 1's isolated copy│ ├── src/ # Full copy, own branch│ └── ...├── agent-2/ # Agent 2's isolated copy│ ├── src/│ └── ...└── agent-3/ # Agent 3's isolated copy ├── src/ └── ...Setting up worktrees:
# Create worktree for Agent 1 on its own branchgit worktree add .worktrees/agent-1 -b agent-1-task
# Create worktree for Agent 2git worktree add .worktrees/agent-2 -b agent-2-task
# Create worktree for Agent 3git worktree add .worktrees/agent-3 -b agent-3-task
# List all worktreesgit worktree listEach agent works independently:
# Agent 1 works in its isolated copycd .worktrees/agent-1# ... agent makes changes to any files ...git add .git commit -m "Agent 1: Refactor authentication"
# Agent 2 works in its own copycd ../agent-2# ... agent makes changes to the SAME files ...git add .git commit -m "Agent 2: Update database layer"Supervisor merges all branches:
# Back to main repocd ../..
# Merge Agent 1's changesgit merge agent-1-task
# Merge Agent 2's changes (may have conflicts!)git merge agent-2-task# Resolve conflicts manually or with AI assistance
# Merge Agent 3's changesgit merge agent-3-task
# Clean up worktrees after mergegit worktree remove .worktrees/agent-1git worktree remove .worktrees/agent-2git worktree remove .worktrees/agent-3Pros:
- True parallelism - all agents can edit the same file simultaneously
- No waiting for locks
- Natural integration with git workflow
Cons:
- Merge conflicts must be resolved by a supervisor (human or AI)
- More disk space (multiple repo copies)
- More complex setup
The Reddit poster noted: “each worker gets its own git worktree - a full copy of the repo on its own branch. Three agents can edit the same file at the same time.”
Quick Comparison
| Feature | File Locking | Git Worktrees |
|---|---|---|
| Parallelism | Limited (file-dependent) | Full |
| Merge Conflicts | Never | Possible (need resolution) |
| Setup Complexity | Low | Medium |
| Disk Usage | Low | Higher (multiple copies) |
| Debugging | Easy (one source of truth) | Harder (multiple branches) |
| Best For | Small teams, disjoint tasks | Large parallel tasks, refactoring |
When to Use Each Approach
After experimenting with both, here’s my decision guide:
Use file locking when:
- Your agents work on naturally file-disjoint tasks
- You have a small number of agents (2-3)
- Simplicity matters more than speed
- You want to avoid merge conflicts entirely
Use git worktrees when:
- Multiple agents need to edit the same files
- You have a supervisor (human or AI) to handle merges
- Parallelism is critical for productivity
- You’re doing large-scale refactoring
My Recommendation
Start with file locking. It’s simple and works well for most use cases.
When you hit the ceiling - when agents are waiting too often, when shared files become bottlenecks - graduate to worktrees. But have a merge strategy ready. Either:
- Human reviewer: You review and merge each branch manually
- AI supervisor: A separate AI agent trained to resolve merge conflicts
- Sequential merge: Merge branches one at a time, fixing conflicts as they appear
What I’m Using Now
For my project, I use a hybrid approach:
class HybridCoordinator: def __init__(self): self.lock_manager = FileLockManager() self.worktree_dir = ".worktrees"
def assign_task(self, task: Task) -> WorkContext: # Check if task files overlap with running tasks if self._has_file_overlap(task.files): # Use worktree for overlapping files return self._create_worktree(task) else: # Use file locking for disjoint files return self._create_locked_context(task)
def _has_file_overlap(self, files: list[str]) -> bool: return any( self.lock_manager.is_locked(f) or f in self._active_worktree_files() for f in files )This gives me the simplicity of file locking for most tasks, with worktrees as an escape hatch when conflicts are unavoidable.
The key insight: coordination strategy should match your workload. Don’t over-engineer for file-disjoint tasks, but don’t fight merge conflicts manually if you can avoid them.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Reddit: I reverse-engineered Claude Code to build a better orchestrator
- 👨💻 Git Worktree Documentation
- 👨💻 Claude Code Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments