<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: paultuddenham</title>
    <description>The latest articles on DEV Community by paultuddenham (@paultuddenham).</description>
    <link>https://dev.to/paultuddenham</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4231371%2Fade9d166-8a86-496f-805a-d5e49f154a6d.png</url>
      <title>DEV Community: paultuddenham</title>
      <link>https://dev.to/paultuddenham</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paultuddenham"/>
    <language>en</language>
    <item>
      <title>Stopping parallel Claude Code sessions from undoing each other's work</title>
      <dc:creator>paultuddenham</dc:creator>
      <pubDate>Sat, 07 Nov 2026 02:29:54 +0000</pubDate>
      <link>https://dev.to/paultuddenham/stopping-parallel-claude-code-sessions-from-undoing-each-others-work-40af</link>
      <guid>https://dev.to/paultuddenham/stopping-parallel-claude-code-sessions-from-undoing-each-others-work-40af</guid>
      <description>&lt;p&gt;I run several Claude Code sessions at once. One refactors, one writes tests, one chases a&lt;br&gt;
flaky service, and a couple run unattended. Each is good at its job. None of them knows the&lt;br&gt;
others exist, and that causes two kinds of damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent undo.&lt;/strong&gt; A session stops a background service on purpose, because it kept&lt;br&gt;
dropping my remote connection. Hours later another session is asked to check everything is&lt;br&gt;
healthy. It finds the service stopped, decides that is the bug, and starts it again. Nothing&lt;br&gt;
in the second session was careless. The reason for the stop lived only in the first&lt;br&gt;
session's context, and that context was gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hidden overlap.&lt;/strong&gt; Two sessions work on two different tasks that happen to touch the&lt;br&gt;
same config file. One finishes a careful change. The other saves its own copy over the top a&lt;br&gt;
few minutes later. The first change disappears and neither session notices. Tracking tasks&lt;br&gt;
cannot catch this, because the tasks really are different. What they share is a resource.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why a rule in CLAUDE.md does not fix it
&lt;/h3&gt;

&lt;p&gt;The obvious fix is one line: "check what other sessions are doing before touching shared&lt;br&gt;
things." It loads into every session. It still fails, for two reasons. Agents load&lt;br&gt;
instructions reliably and act on them unreliably: ten tool calls into a fix, nobody re-reads&lt;br&gt;
their standing orders before each write. And even an agent that does stop has nothing to&lt;br&gt;
check against. "What are the other sessions doing?" has no answer unless something records&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;Hooks fix both. They fire on every tool call whether the agent remembers anything or not.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer one: every write claims what it touches
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;PostToolUse&lt;/code&gt; hook records a claim for each write: the session id, the exact resource and&lt;br&gt;
a time. The resource is the file path for Edit and Write, the target for an MCP write, and&lt;br&gt;
for a shell command, the files the command would actually change. Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads claim nothing.&lt;/strong&gt; My first version claimed on reads too, and a &lt;code&gt;grep&lt;/code&gt; across a repo
ended up "holding" every file in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quoted text is ignored.&lt;/strong&gt; Otherwise a commit message that mentions a file name claims
that file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claims expire after 45 minutes unused, or when the session closes. They are not dropped at&lt;br&gt;
the end of a turn, so a session waiting for your answer still holds what it was changing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer two: a guard that blocks once
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;PreToolUse&lt;/code&gt; hook runs before every write and works out the same keys. If another live&lt;br&gt;
session holds one of them, it denies the call with a plain reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"hookSpecificOutput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecisionReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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."&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three choices made this work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deny, not ask.&lt;/strong&gt; Hooks can return &lt;code&gt;ask&lt;/code&gt;, which prompts the user. In permission modes that&lt;br&gt;
auto-approve, the ask can be resolved without the agent ever seeing the reason. My guard&lt;br&gt;
fired correctly, logged correctly, and the write landed anyway. &lt;code&gt;deny&lt;/code&gt; is the decision that&lt;br&gt;
always reaches the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once, not forever.&lt;/strong&gt; The retry goes through. A guard that can trap a session behind a&lt;br&gt;
crashed peer gets switched off within a day. Block-once means the warning is always seen and&lt;br&gt;
the agent is still free to act on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember per resource.&lt;/strong&gt; My first version remembered "I warned you about this set of&lt;br&gt;
conflicts". On a busy file the set changes every few seconds, so the same session was blocked&lt;br&gt;
again and again for one real overlap. Remembering each resource separately fixed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stale copy
&lt;/h3&gt;

&lt;p&gt;A live claims table cannot see this one: session A reads a file at 14:00, session B edits it&lt;br&gt;
at 14:14 and finishes, session A saves its old copy at 14:20. They never held the file at the&lt;br&gt;
same moment. So the guard also keeps a short record of who last wrote each file and when each&lt;br&gt;
session last read it. If someone else wrote it after your last read, you get told before you&lt;br&gt;
save. Reading the file again clears the warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fail open, and prove it still fires
&lt;/h3&gt;

&lt;p&gt;Every hook exits 0 with no output on any error. A guard that crashes must never stop work.&lt;br&gt;
The price is that a broken guard looks exactly like a quiet one, so I keep a one-line log of&lt;br&gt;
every decision and a self-test that seeds a collision in a throwaway folder and checks the&lt;br&gt;
guard denies it. Test both directions: that it blocks what it should, and lets through what&lt;br&gt;
it should.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;The claims hook and guard are free and MIT licensed, one Python file with no packages:&lt;br&gt;
&lt;a href="https://github.com/paultuddenham/claude-collision-guard" rel="noopener noreferrer"&gt;https://github.com/paultuddenham/claude-collision-guard&lt;/a&gt;. Two commands install it as a Claude Code plugin, and the README covers wiring&lt;br&gt;
the hooks by hand.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://tudlab.com/memory" rel="noopener noreferrer"&gt;https://tudlab.com/memory&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was written with the help of AI. I built the hooks with Claude Code; Claude&lt;br&gt;
wrote most of the code and helped draft this post. I designed the rules, tested it on Windows&lt;br&gt;
and Linux, and checked every claim here.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>python</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
