0

I replayed two weeks of Claude Code transcripts to see what my parallel sessions did to each other

For two weeks I ran Claude Code the way a lot of people now do: several sessions at once, usually one per git worktree, each starting its own subagents, dev servers and test runs. Every session behaves as if it owns the computer. None of them does. They share one process table, one set of ports, one pool of RAM, one disk, and sometimes one working tree.

I suspected this was costing me. Servers vanished in the middle of a test. Commits contained files I did not remember touching. The machine would stall and then come back. What I did not have was a count. So I took the transcripts Claude Code had already written to disk and replayed every shell command in them against a set of rules, each describing one way a command can hurt another session. This post covers what came out, how the rules work as hooks, and where the approach stops working.

The data

Claude Code stores a JSONL transcript for each session and each subagent under ~/.claude/projects. Two weeks of mine came to 1,536 transcript files and 7.0 GB, covering 204 sessions and 101,167 shell commands (Bash and PowerShell tool calls). Reading all of it took 39 seconds. The first number that surprised me was not about commands at all. At the peak, on November 1, 13 sessions were running at the same time. I would have guessed far fewer.

The second number: 507 of those commands match a rule and would have been blocked. As a share of the total that is small. But each one is a command that can take down work belonging to some other session, and with that many sessions open, the other session is usually me in another window.

The case I found most uncomfortable: 25 of the 507 were git add -A, run in a working tree where a different session was active within ten minutes of the command.

What the collisions look like

I am not going to walk through all 507. The rules are written around a handful of command shapes, and these are the ones that matter most when sessions share a machine.

Staging everything in a shared tree. Session A is halfway through a refactor in src/. Session B, in the same checkout, finishes a small fix and runs git add -A && git commit -m "fix typo". B's commit now holds A's unfinished refactor. Nothing fails. You find out later, in review or during a bisect. Staging by file name avoids the problem, so the rule only objects to the "take everything" forms (git add -A, git add ., git add -u, git commit -a) and lets named paths through.

Killing by program name. An agent wants to restart its dev server and reaches for pkill node, or taskkill /IM node.exe on Windows. That stops its own server, every other worktree's server, and any Node test runner. The agent only meant one PID.

Two servers, one port. Two worktrees of the same app both run npm run dev. The second one either fails on port 3000 or silently takes 3001, while its agent keeps opening localhost:3000 and testing the other branch's code.

Whole-disk scans. An agent asked what is using disk space runs du -sh ~, or starts ten du processes in parallel. Every other session's git status, build and test run slows down until it finishes.

Deleting a folder that is in use. A cleanup task runs git worktree remove or rm -rf on a worktree that another live session is still working in.

None of these is a model bug. Each command is reasonable if you are the only process on the machine. The problem is that the agent has no idea the other sessions exist.

Turning the rules into hooks

Claude Code has hooks: programs it runs at fixed points in a session. A PreToolUse hook registered for the Bash and PowerShell tools receives the pending tool call as JSON on stdin before the command executes. If the hook exits with code 2, Claude Code cancels the call and hands whatever the hook wrote to stderr back to the model as the reason.

That stderr message matters more than the block. A bare "denied" tends to make the agent try a small variation of the same command. A message that says what to do instead usually settles it in one turn. The kill rule answers with how to find and stop the agent's own PID. The port rule names a free port. The staging rule asks for file names. The checkout rule suggests git worktree add.

Each guard works in the same order. It first matches the command text cheaply, and since most commands do not match, the hook usually exits 0 at once. Only on a match does it look at the machine: the process table, listening ports, which Claude sessions are alive and where they work, free RAM and git status. If the command would hurt another live session or lose uncommitted work, it logs the case and exits 2 with a message.

Two design choices came from getting this wrong early. First, the guards fail open. If a guard throws, receives JSON it cannot parse, or cannot read the process table, it exits 0 and the command runs. A bug in a guard should cost you protection, not your work. Second, the command parser unwraps bash -c, powershell -Command, cmd /c, eval, sudo, xargs and similar wrappers a few levels deep, because agents wrap commands more often than people do. It also ignores text that only mentions a command, such as a commit message or a grep pattern, so writing about pkill in a changelog does not trigger anything.

Replaying instead of guessing

The scan runs the same guard code against history instead of a live call. For each shell command in a transcript, it rebuilds whatever context it can and asks every guard whether it would have blocked. The report is grouped by guard and rule: number of hits, number of sessions they came from, how many a hook or the user had already stopped at the time, and the newest examples with anything that looks like a token masked.

For the rules about shared trees, the question "was another session working here" is answered from the transcripts themselves, by checking which sessions were active in the same git tree within ten minutes of the command. That is how the 25 git add -A cases turned up.

Limits

The approach has clear edges, and I would rather list them than have someone find them the hard way.

It reads command strings, so it is not a sandbox. A kill inside a script file, an alias, or an encoded command the parser cannot open goes through. The aim is to catch the forms an agent reaches for when it is not thinking about other sessions, not to contain an agent that is actively working around the rules. Some checks cannot be replayed. Transcripts do not record which ports were listening or how much memory was free at a given moment, so the port rule and the low-memory check are left out of the scan. The 507 does not include them, which means the real number on my machine was higher by an amount I cannot measure from history. Matching a rule is also not the same as causing damage. Some of the 507 would have been harmless at that exact moment. The count says how often my sessions did something unsafe on a shared machine, not how many times something broke. Finally, the scan only reads. It reads ~/.claude/projects, prints to the terminal, writes nothing and sends nothing over the network. Secrets are masked in the examples it prints.

Run it on your own transcripts

The guards and the scan ship as a Claude Code plugin called claude-bulkhead (MIT, Node 18 or later, no dependencies). Install it from inside Claude Code:

/plugin marketplace add dooly6276/claude-bulkhead
/plugin install bulkhead@bulkhead

Then replay the last month:

/bulkhead:scan 30d

From a shell it is bulkhead scan --since 14d, with --project to narrow it to one repository and --json if you want to process the result yourself. Installing the plugin also turns the guards on for new sessions, and each guard can be switched off on its own if you only want the report.

If you run one session at a time, expect a short report, since most of these rules are about sessions getting in each other's way.

Repository: https://github.com/dooly6276/claude-bulkhead


Disclosure: I built claude-bulkhead. This article was written by an AI agent from my scan data and the project docs, at my request.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí