What I've learned running three or four Claude Code sessions at once while building Read Master (opens in new tab) — an AI-powered reading comprehension and retention app — on a six-year-old Intel Mac.
How this was written: Claude drafted this article (in one of the parallel sessions it describes), and I reviewed, edited, and fact-checked it. The incidents and numbers are real and mine.
One morning in August, my Mac's load average hit 214.
Load average is the number your operating system keeps of how much work is waiting on the CPU. My machine has 16 logical cores, so a load of 16 means every core is busy. A load of 214 means there were roughly thirteen computers' worth of work queued on one computer. The cursor stuttered. Tests timed out. A git push died halfway through its hooks.
The cause wasn't a bug. Three Claude Code sessions had each decided, independently and at the same moment, that it was time to run the full test suite. Each one was doing exactly what I'd asked.
That's the core problem with running AI coding agents in parallel: they can't see each other. Your machine becomes a shared resource nobody is managing, your git history becomes the place their work collides, and you become the scheduler whether you signed up for that or not.
I'm a solo developer, and parallel Claude Code sessions are how I get the output of a small team. The worktree trick that makes this possible is well covered — Anthropic's engineering guide (opens in new tab) recommends it, and there are solid setup walkthroughs around. What's written about much less is everything that happens after you have four sessions running: the melted machine, the duplicated work, the merge conflicts, the attention problem. After months of running three or four sessions a day, I've collected some scars there and, more usefully, some fixes. One theme runs through all of them, so I'll say it up front: written rules don't scale to agents. Enforcement does. Anything you have to remember to do, an agent will eventually forget for you. So every lesson below ends with two parts — the manual version, and how to make it happen automatically.
My setup, so you can translate
- Machine: a 2019 Intel Mac — 8 cores / 16 threads, 64 GB RAM. Not Apple Silicon. If these numbers hold on a six-year-old Intel box, your M-series laptop has headroom to spare.
- Stack: a TypeScript monorepo on Bun, Turborepo for task running, TanStack Start for the app, Vitest and Playwright for tests, Prisma + Postgres underneath.
- OS: macOS. On Linux, swap
sysctl -n vm.loadavgforcat /proc/loadavg, andtaskpolicy -c utilityfornice.
The env var names below (VITEST_MAX_THREADS, PW_WORKERS, etc.) are my stack's knobs. Every test runner has an equivalent; translate to yours.
Lesson 1: Budget heavy phases, not sessions
The first question everyone asks is "how many sessions can I run?" It's the wrong unit.
A session that's thinking, editing files, or waiting on CI costs almost nothing locally. A session running your test suite costs several CPU workers, a dev server, and a headless browser or two. So don't budget sessions. Budget concurrent heavy phases: test runs, lints, builds, end-to-end suites.
My numbers, on 16 threads:
- 3–4 active sessions is the sweet spot. Five or more only works if most of them are parked.
- At most 2 sessions in a heavy phase at any moment.
- Repo-wide sweeps (full lint passes, audits, mass codemods): one at a time, machine-wide.
The arithmetic: two capped test runs is about 8 busy workers, plus dev servers and a browser. That fits in 16 threads. The third suite is what put my machine at load 214.
The trick that makes the budget hold is phase-shifting: keep your sessions at different stages of their lifecycle. One is planning (free), one is implementing (light), one is verifying (heavy), one is watching CI (free). Same number of sessions, and the collisions mostly stop happening on their own.
Try it: next time you're running several sessions, notice how many are in a heavy phase. If the answer is ever "all of them," you've found your problem.
Make it automatic: the entry-level tool is your CLAUDE.md file — standing instructions every session reads on startup. Mine has a short section the agents actually follow:
## Parallel-session limits (all sessions)
- Before any suite-wide test/lint/build: check `sysctl -n vm.loadavg`.
If the 1-min load is above ~16, wait 2-3 minutes and re-check, or run
something smaller (one test file, one package).
- At most two suite runs on this machine at once.
- Repo-wide sweeps: one session at a time.
Instructions alone won't survive contact with reality (that's Lesson 3), but they're the right first layer, and they cost five minutes.
Lesson 2: Read the load average like a pre-flight check
You can read the load any time:
# macOS: 1-min, 5-min, 15-min averages
sysctl -n vm.loadavg
# Linux
cat /proc/loadavg
My thresholds on 16 threads: below ~16, go ahead. Between 16 and 20, wait a couple of minutes or scope down. Above 20, nothing heavy starts, no exceptions.
The less obvious half of this lesson: under heavy load, failures lie. When the machine is saturated I've watched subprocess tests "fail" with exitCode: null and empty output, a singleton race take out 100+ unrelated tests, and pushes time out mid-hook. None of those were real bugs. They were starvation wearing a bug costume.
So the debugging rule is: when you see a suspiciously large number of unrelated failures, run uptime before you believe any of them. If load is high, wait until the machine is quiet, then re-run the one failing thing by itself. Don't retry in a loop — retries add load — and don't skip your git hooks to squeeze past.
One more thing while I'm here: don't benchmark on a machine running parallel sessions. My test suite has a ~40-second noise floor from thermal throttling alone. Anything under a 10% difference is weather, not signal.
Try it: alias load to the command above. Check it before anything heavy.
Make it automatic: put the load in your status line, so the pre-flight check becomes ambient instead of a ritual. Claude Code lets you set a custom status line in ~/.claude/settings.json:
"statusLine": { "type": "command", "command": "bash ~/.claude/statusline.sh" }
#!/bin/bash
# statusline.sh — whatever you already display, plus the 1-min load
load=$(sysctl -n vm.loadavg | awk '{print $2}')
echo "$(git branch --show-current 2>/dev/null) | load $load"
Mine color-codes it: green below 16, yellow to 20, red above. Every session now shows me the machine's temperature at a glance, and the agents see it too.
Lesson 3: Make the machine enforce the rules
I wrote "don't start a third suite" into my instructions. It mostly worked. "Mostly" is doing real work in that sentence — an agent deep in a debugging spiral will run the suite because running the suite is what you do when you're debugging.
What actually fixed it was making the rule physically enforceable. Two mechanisms:
Cap your test runners' workers globally. Claude Code injects env vars from ~/.claude/settings.json into every command it runs:
{
"env": {
"VITEST_MAX_THREADS": "4",
"TURBO_CONCURRENCY": "4",
"PW_WORKERS": "4"
}
}
Now even a session that ignores every instruction can only grab 4 workers. One warning from experience: verify the cap actually reaches the process. Mine were silently swallowed by Turborepo's strict env mode for weeks until I allow-listed them with passThroughEnv. A cap you haven't watched work is a cap you don't have.
Add a gate hook. Claude Code hooks are small scripts that run around tool calls. A PreToolUse hook on Bash can check the machine before any heavy command runs, and refuse it with an explanation:
#!/usr/bin/env python3
# ~/.claude/hooks/gate-heavy-commands.py
import json, os, re, subprocess, sys
MAX_LOAD = 20.0 # ~1.25x my 16 logical cores. Tune to your machine.
MAX_SUITES = 2 # two suites are fine; the third gets denied
HEAVY = re.compile(r"vitest|playwright test|jest|turbo run (test|lint|typecheck)")
cmd = json.load(sys.stdin).get("tool_input", {}).get("command", "")
if not HEAVY.search(cmd) or "HEAVY_OK=1" in cmd:
sys.exit(0) # not heavy, or explicitly overridden: allow
load1 = os.getloadavg()[0]
procs = subprocess.run(
["pgrep", "-fl", "vitest|playwright test|jest|turbo run"],
capture_output=True, text=True,
).stdout.splitlines()
if load1 > MAX_LOAD or len(procs) >= MAX_SUITES:
print(
f"Machine busy (load {load1:.0f}, {len(procs)} suites running). "
"Wait ~3 minutes and retry, or run a single test file instead. "
"Prefix with HEAVY_OK=1 only if the human explicitly asked.",
file=sys.stderr,
)
sys.exit(2) # exit code 2 blocks the tool call; stderr goes to the agent
sys.exit(0)
Wire it up in the same settings file:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/gate-heavy-commands.py",
"timeout": 10
}
]
}
]
}
}
Notice the denial message teaches the agent what to do instead: wait, or run something smaller. In practice my sessions negotiate around each other without knowing the others exist. Hooks also apply to subagents, so a session that fans out helpers can't multiply its way past the limit.
While writing this article I got to watch the gate earn its keep: it denied a test run because a sibling session had the machine at load 183. The denied session waited, retried when things went quiet, and carried on. No melted machine, no intervention from me.
(Also, macOS users: prefix long unattended jobs with taskpolicy -c utility and they'll politely yield to whatever you're actively doing. Linux: nice.)
Lesson 4: One git worktree per session
If two sessions share one checkout, they'll edit the same files, race each other's builds, and occasionally commit each other's half-finished work. Git worktrees (opens in new tab) solve this: multiple working directories backed by one repository, each on its own branch.
git worktree add ../myapp-feature-x -b feature-x origin/main
cd ../myapp-feature-x
npm install # do not skip this, see below
claude
One session per worktree, always. The official best-practices docs (opens in new tab) recommend the same pattern, and if you want a deeper walkthrough of the setup itself, Code With Seb's production setup guide (opens in new tab) is a good one — his rule of scoping worktrees by module rather than by task rhymes with the partitioning advice in Lesson 6.
Two traps I fell into so you don't have to:
The silent-hooks trap. Tools like husky install your git hooks during npm install. A fresh worktree without an install has no hooks, and git skips missing hooks silently, exiting 0. My receipt: in a hookless worktree, git push took 3.0 seconds. With hooks, 55.9 seconds. That fast push ran zero checks and looked completely green.
The stale-codegen trap. Generated files — ORM clients, route trees — don't exist in a fresh worktree until you generate them. I once skipped that step and spent a while staring at 369 phantom type errors in code I hadn't touched.
And if your sessions spawn subagents: tell them, in the prompt, to never run git checkout, git reset, or git stash. A subagent of mine once checked out an old commit "to inspect it," and the next agent committed onto the wrong branch. Subagents read history with git show and git diff. Only the main session moves HEAD.
Try it: the four commands above, next time you'd otherwise open a second session in the same directory.
Make it automatic: wrap the ritual in a script so nobody (human or agent) does it from memory:
#!/bin/bash
# wt.sh — new worktree with everything a session needs
set -e
git worktree add "../myapp-$1" -b "$1" origin/main
cd "../myapp-$1"
cp ../myapp/.env.local . 2>/dev/null || true
npm install
npm run db:generate 2>/dev/null || true
echo "ready: $(pwd)"
Then put the rule in CLAUDE.md ("every session works in its own worktree; create one with ./wt.sh <branch>") so the agents run it themselves. The Claude Code desktop app can also create a worktree per session automatically, which is what I use day to day — with a small SessionStart hook that copies .env.local and re-runs codegen if it's missing.
Lesson 5: Make shared files impossible to conflict
With four branches merging every day, my conflicts didn't come from feature code. They came from the files every branch touches: the changelog, the progress log, the shared context doc my agents read.
The fix isn't better merging. It's removing the shared file entirely.
For any append-only shared file, use a fragment directory. Each branch writes its own small file instead of appending to the shared one:
docs/changelog.d/
2026-08-19-fix-reader-timeout.md
2026-08-21-add-arabic-toc.md
A build script concatenates fragments into the real changelog at release time. Every fragment has exactly one owner, so there is nothing for two branches to fight over. Conflicts don't get resolved; they stop being possible. If you've used changesets (opens in new tab) or Python's towncrier (opens in new tab), this is the same idea generalized to any shared document.
For files that genuinely must be edited by everyone, like a conventions doc:
- Keep lists one entry per line and sorted, so parallel additions land in different lines and merge cleanly.
- Consider
merge=unionin.gitattributes, which keeps both sides of a conflict instead of stopping — but know its trap: during a rebase it can silently keep duplicate copies of the same lines. One of my context files quietly grew from 223 to 275 lines of doubled bullets before I noticed. Union-merge only line-oriented prose, never JSON or source code, and check for duplicates in CI.
And the cheapest conflict reducer of all: merge fast. Small PRs that land the same day shrink every other session's window to collide with you.
Try it: pick your most conflict-prone shared file and split it into a fragment directory this week. It's an hour of work.
Make it automatic: three pieces, all set-and-forget. The .gitattributes line ships with the repo, so every clone behaves the same. The fragment rule goes in CLAUDE.md ("never edit CHANGELOG.md directly; add a file under changelog.d/") — agents follow this one reliably because it's a simple prohibition. And a small CI check greps union-merged files for duplicated lines, which turns the rebase trap from a silent corruption into a red build.
Lesson 6: Claim work before you start it
My most expensive failure wasn't a crash. Two sessions, working through the same list of code-review findings, built the same fix twice. Full test-driven implementations, both of them, hours each. The first one merged. The second discovered the collision only when its pull request hit a conflict, and all of that work went in the bin.
Agents can't see each other's intentions, so intentions have to live somewhere public. The most natural place is the one every session already checks: your PRs.
The protocol:
- Opening a draft PR is calling dibs. A work item belongs to whoever has a PR mentioning it — even an empty draft. (If you know the term mutex from concurrent programming, that's exactly what this is: a lock that stops two workers grabbing the same job. If you don't: it's dibs.)
- Before starting an item, look for someone else's dibs:
gh pr list --state all --search "FINDING-12"
git fetch && git log --oneline HEAD..origin/main
- Check again right before opening your own PR. A rival fix can land while you work. One landed an hour into mine.
- If you lose the race, don't fight the merged fix. Diff your branch against it and ship only what's genuinely missing, as a small follow-up.
Better than any protocol, though: partition. Give each session its own territory — one owns the reader, one owns billing — instead of letting two sessions pull from one to-do list.
Try it: make "open a draft PR within the first few minutes" the habit for every session.
Make it automatic: teach your PR-opening script to check for rivals itself: scan the branch's commits and title for work-item IDs, search existing PRs for each one, and refuse to open if another PR already claims it (with an override flag for deliberate follow-ups). As I write this, one of my Claude sessions is wiring exactly that check into my own PR script — which feels appropriately circular.
Lesson 7: Write decisions down the moment you make them
A failure mode nobody warned me about: session A decides "we debounce saves by 2 seconds, here's why," and an hour later session B — which can't see session A's conversation — helpfully "fixes" the debounce away. Both did reasonable work. They just never met.
Parallel sessions can only honor decisions they can read. So decisions live in the repo:
- A
docs/CONTEXT.mdfile records current decisions, the why behind non-obvious choices, and one line per in-flight work stream. Every session reads it; every session can check what its siblings are up to. - Updates land in the same commit as the code. A decision recorded "later" doesn't exist for the session that branches off in between.
This sounds like bureaucracy until you watch two AI sessions politely undo each other's architecture all afternoon.
Try it: create docs/CONTEXT.md with two sections — "decisions" and "in flight." Tell your sessions (via CLAUDE.md) to read it at start and update it when they change something.
Make it automatic: enforce the same-commit rule with a pre-commit hook that fails any commit touching source files without touching docs. Mine literally blocks "code-only" commits unless a doc, context file, or changelog fragment changed too (with an escape hatch for trivial fixes). Crude, but it converts "please remember to document" into "the commit won't land until you do" — and agents respond much better to the second kind of rule.
Lesson 8: You are the scheduler, so equip yourself like one
Everything above assumes a human notices things. Some five-minute setup makes that actually true:
- Turn on notifications. A session blocked on a permission prompt wastes its slot for exactly as long as you don't see it — and no load gauge will ever show you that idle time. Desktop app: allow Claude in System Settings → Notifications. Terminal: run
/configand enable notifications, or wire theNotificationhook to a system banner:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs input\" with title \"Claude Code\"'"
}
]
}
]
}
}
- Split view, not stacked tabs. Sessions side by side (desktop app, or one per tmux/iTerm pane) turn "who's blocked, who's running, who's done" into a glance. A permission prompt hidden behind a tab is the most common way a slot sits idle.
- Name sessions after their deliverable so the session list reads like a work board.
- Color-code your terminals per worktree. The classic parallel fat-finger is launching the suite in the session that already has one running.
- Set risk posture per session. The low-risk docs-and-tests session gets auto-accepted edits so it never blocks on you. The risky migration session stays on manual approval, in the pane you actually watch.
Make it automatic: the notification hook above is the automation — it converts "I should check on my sessions" into "my sessions tell me when they need me." That inversion is the whole point. The rest is one-time setup.
Lesson 9: The rate limit is a second machine, and you're saturating that one too
Your CPU is one shared resource. Your Claude subscription is the other. Every session, subagent, and background task draws from the same account-wide pool.
I learned this by launching ~18 heavyweight subagents at once for a code audit. Eleven failed with rate-limit errors. The same job, batched in groups of three, finished clean — and a different fan-out of 23 lighter-model agents ran wide with zero failures. So: batch your heavy-model fan-outs (about 3 at a time works for me), let cheap-model agents run wide, and never launch big fan-outs from two sessions at once. (The subagents docs (opens in new tab) cover the parallelism model itself; for the quota side, this dev.to piece on model selection under rate limits (opens in new tab) pairs well with the batching rule.)
This also puts cloud offloading in its right place. Running work in the cloud — Claude Code on the web, scheduled background tasks, CI-based review — takes load off your machine, and I use it exactly that way: weekly audits, docs-drift checks, and maintenance chores run as scheduled cloud tasks so my laptop never pays for them. But cloud work draws the same account quota as local work. Cloud relieves CPU, not rate limits. When the machine is saturated, sending the next job to the cloud beats opening a fifth local session; when the quota is saturated, nothing helps but patience and smaller batches.
Try it: next time you fan out subagents, count how many heavyweight ones run concurrently. If it's more than a handful, batch them.
Make it automatic: two standing rules in CLAUDE.md ("batch heavy-model agent fan-outs in groups of ~3"; "when load is high, prefer cloud sessions over new local ones"), plus moving every recurring maintenance job — audits, dependency checks, doc-drift scans — to scheduled cloud tasks so they never compete for your machine at all.
The cheat sheet
- 3–4 active sessions. At most 2 doing anything heavy. Sweeps and audits run alone.
- Check the load average before heavy runs; wait above ~1× your core count.
- Mass weird failures?
uptimefirst. High load means the failures are lying to you. - Enforce with env caps + a gate hook. Agents can't see each other; the machine can.
- One worktree per session. Never skip the install — that's what gives you git hooks.
- Shared append-only files become per-branch fragments. Union-merge needs a duplicate check.
- A draft PR is dibs. Check for rivals before starting and before opening yours.
- Decisions live in the repo, same commit as the code. Sessions only honor what they can read.
- Notifications on, split view, named sessions, color-coded worktrees.
- Batch heavy agent fan-outs. Cloud saves your CPU, not your quota.
Why I care about this
I'm building Read Master (opens in new tab) alone. It's an AI-powered reading comprehension app — pre-reading guides that prime you before a book, explanations on demand while you read, all wrapped around an accessibility-first reader. A one-person product only ships if that one person finds leverage somewhere, and parallel Claude Code sessions are the biggest lever I've found. Every lesson here was paid for with a real incident on a real codebase.
Read Master launches publicly in September. If you're curious what all this parallelism has been building, the waitlist is open (opens in new tab) — and if you're running parallel agent sessions yourself, I'd genuinely like to compare notes. What's your setup? What melted first?
Claude drafted this article in one of the parallel sessions it describes; I reviewed, edited, and fact-checked it. If you try the gate hook or the fragment pattern, tell me how it goes.