Stopping parallel Claude Code sessions from undoing each other's work
I run several Claude Code sessions at once. One refactors, one writes tests, one chases a flaky service, and a couple run unattended. Each is good at its job. None of them knows the others exist, and that causes two kinds of damage. The silent undo. A session stops a background service on purpose, because it kept dropping my remote connection. Hours later another session is asked to check everything is healthy. It finds the service stopped, decides that is the bug, and starts it again. Nothing in the second session was careless. The reason for the stop lived only in the first session's context, and that context was gone. The hidden overlap. Two sessions work on two different tasks that happen to touch the same config file. One finishes a careful change. The other saves its own copy over the top a few minutes later. The first change disappears and neither session notices. Tracking tasks cannot catch this, because the tasks really are different. What they share is a resource. Why a rule in CLAUDE.md does not fix it The obvious fix is one line: "check what other sessions are doing before touching shared things." It loads into every session. It still fails, for two reasons. Agents load instructions reliably and act on them unreliably: ten tool calls into a fix, nobody re-reads their standing orders before each write. And even an agent that does stop has nothing to check against. "What are the other sessions doing?" has no answer unless something records it. Hooks fix both. They fire on every tool call whether the agent remembers anything or not. Layer one: every write claims what it touches A PostToolUse hook records a claim for each write: the session id, the exact resource and a time. The resource is the file path for Edit and Write, the target for an MCP write, and for a shell command, the files the command would actually change. Two details matter: Reads claim nothing. My first version claimed on reads too, and a grep across a repo ended up "holding" every file in it. Quoted text is ignored. Otherwise a commit message that mentions a file name claims that file. Claims expire after 45 minutes unused, or when the session closes. They are not dropped at the end of a turn, so a session waiting for your answer still holds what it was changing. Layer two: a guard that blocks once A PreToolUse hook runs before every write and works out the same keys. If another live session holds one of them, it denies the call with a plain reason: {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Another live session is changing src/pay.py (active 2 min ago). Ask it whether it is mid-change or done. If it is done, say so and run the call again."}} Three choices made this work. Deny, not ask. Hooks can return ask, which prompts the user. In permission modes that auto-approve, the ask can be resolved without the agent ever seeing the reason. My guard fired correctly, logged correctly, and the write landed anyway. deny is the decision that always reaches the agent. Once, not forever. The retry goes through. A guard that can trap a session behind a crashed peer gets switched off within a day. Block-once means the warning is always seen and the agent is still free to act on it. Remember per resource. My first version remembered "I warned you about this set of conflicts". On a busy file the set changes every few seconds, so the same session was blocked again and again for one real overlap. Remembering each resource separately fixed it. The stale copy A live claims table cannot see this one: session A reads a file at 14:00, session B edits it at 14:14 and finishes, session A saves its old copy at 14:20. They never held the file at the same moment. So the guard also keeps a short record of who last wrote each file and when each session last read it. If someone else wrote it after your last read, you get told before you save. Reading the file again clears the warning. Fail open, and prove it still fires Every hook exits 0 with no output on any error. A guard that crashes must never stop work. The price is that a broken guard looks exactly like a quiet one, so I keep a one-line log of every decision and a self-test that seeds a collision in a throwaway folder and checks the guard denies it. Test both directions: that it blocks what it should, and lets through what it should. Try it The claims hook and guard are free and MIT licensed, one Python file with no packages: https://github.com/paultuddenham/claude-collision-guard. Two commands install it as a Claude Code plugin, and the README covers wiring the hooks by hand. The longer write-up of the full memory setup these hooks sit inside (what each session is given at start, and how three layers of shared memory fit together) is at https://tudlab.com/memory. This article was written with the help of AI. I built the hooks with Claude Code; Claude wrote most of the code and helped draft this post. I designed the rules, tested it on Windows and Linux, and checked every claim here.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to