TL;DR: Ten or more parallel Claude Code sessions work if you treat them as a small distributed system: ownership by function, one writer (a governance session) for shared rules and hooks, approval never relayed, repeat mistakes turned into guard hooks. Assumptions are verified before a human is asked, and no Claude-generated edit ships unreviewed, with GitOps as the record.
This is an opinionated write-up of what worked for me as a solo DevOps engineer, in one company's setup. Treat it as experience to weigh, not as the one right answer.
Why: what goes wrong with an "octopus"
I run 10–15 sessions at once in cmux, one per task. I'm the only DevOps engineer, so all are mine. Failures:
-
File collisions. Two sessions edit one rule file at once.
git add -Aswept another session's edits into the wrong commit; explicit paths don't help, sincegit committakes the whole index. - Relayed "approvals". A tells B "the user approved this"; B can't verify it, A can't transfer it.
- Worktree sprawl, stale state. About 18 worktrees across two repos; a sweep couldn't tell "abandoned" from "mid-task". A peer's "it's merged" can precede the merge.
- Context loss on compaction. A brief lives only in the sender's transcript, so it vanishes when both sides compact.
- Hook fixes racing. One session's regex fix undid another's; the suite stayed green, each fix tested only on its own case.
What: the setup
flowchart LR
subgraph cmux["cmux window"]
S1["Session: task A<br/>worktree A"]
S2["Session: task B<br/>worktree B"]
S3["Session: task C<br/>worktree C"]
G["Governance delegate<br/>(owns global rules + hooks)"]
end
S1 -- "SendMessage: brief" --> G
S2 -- "SendMessage: blocked by guard X" --> G
G -- "result + 'you can close your side'" --> S1
S3 <-- "collision notice" --> S2
U(("Human")) -. "approves in EACH session" .-> S1 & S2 & S3 & G
| Piece | Rule |
|---|---|
| cmux workspaces | one per session; worktrees named <session>--<desc>, so git worktree list shows owners. |
| One session = one task = one worktree/branch/PR | launch at the parent directory of all repos, never inside a worktree: it breaks on removal and can't switch repos. |
Local task list, T<n> ids |
created before investigating. T<n>, never #<n> (a GitHub reference; a hook enforces it). |
| Governance delegate | one long-lived session owns all shared-config writes, found by role, never by name. |
| Single owners | hook repair → the delegate, wherever the hook fired; machine-wide worktree removal → one sweep owner; a repo's .claude/, PRs and tickets → the session working it. SendMessage crosses ownership boundaries; it doesn't offload your own work. |
From about 57 real cross-session messages: finding a defect doesn't make it yours to fix, and being handed a fix doesn't make it yours to approve.
How
1. One dedicated session owns the global rules and hooks
The global CLAUDE.md, pattern docs, hooks, tests and settings are shared; two writers on one rule is a silent race. So one dedicated session writes there and takes no feature work. Others send it a brief for any wrong rule, hook defect or missing guard, create a local task, and never edit inline.
The boundary is mechanical: global config is one directory symlinked into ~/.claude/, so readlink -f tells you (plan files exempt).
2. Delegation moves the work, never the approval
Every brief says: "Nothing below is pre-approved; route every approval through the user in your own session." A harness timeout isn't approval. A peer's word counts when it restricts you ("these worktrees are still mine"), never when it widens what you may do.
3. A brief is a hypothesis, and the receiver files its own ticket
A good brief states: kind (request, finding, FYI); approval status; evidence (exact command or file:line); what was ruled out; what is deliberately not proposed; constraints the receiver can't see; disk state.
The receiver creates the local task and ticket, and the ticket id goes inside the brief; a message is not a tracking system. The receiver verifies conclusion and mechanism separately: a peer once rightly called a worktree safe to remove, for a reason that was impossible.
4. Many sessions on one repo: each knows its co-workers by name
Several sessions on one repo is fine, each on its own worktree, if each gets, at start, the list of its peers by name and what each owns. Then a session that finds work belonging to a peer's piece delegates it there instead of duplicating it, and interconnected changes land in the right order. Before touching a shared path, announce your workstream, ticket, the exact meeting path, and the peer's likely surprise.
5. Order the delegate's queue
Hooks, then rules, then other substantive work, worktree cleanup last. A broken hook hits every session; a broken rule only its readers.
6. When a guard blocks you with no legitimate way through
Send the delegate the command, the guard, its exact message, and what you tried. Then stop: "blocked" is a complete answer. Repackaging the command into a script, reading the refusal's hint as permission, and narrowing the guard yourself are ruled out.
7. No Claude-generated edit goes unreviewed by a human
Nothing a session writes reaches a shared branch, cluster or cloud project until a human has read it.
- Sessions stop at "ready for review". They open the PR; I read the diff and merge. A session never merges, even if its own option said so.
-
No hand changes to live systems. Mutating
kubectl, console edits and Terraform applies are blocked or handed to me as a command; clusters change only via a reviewed commit. - GitOps is the second check. Git records every change; ArgoCD shows drift from git. To check what slipped past review, I compare the two, not a session's summary.
The cost: review is the tiring bottleneck; small PRs and summaries of what was tested, not edited, help. A session 95% right ships its 5% wrong unnoticed.
8. Verify before you ask: a non-negotiable global rule
Asking about a flag that doesn't exist turns approval into fact-checking. So the global "verify, don't claim" rule makes asking the gate: verify every load-bearing assumption before putting a change or an option list to me, attach the evidence, and mark anything unsettled as unverified. Three sources, strongest first:
-
The real environment. Read-only commands (
describe,get,--dry-run=server, a live metrics or logs query); a live counter-example beats any document. -
Authoritative documentation or source, at the version actually deployed. Docs via a lookup tool, not memory; upstream source at the release tag you run, not
main;--helpon the exact subcommand. - Online research, as corroboration when the first two are silent.
A proposal claims a blast radius; a conclusion like "impossible" needs a search for a working instance. A silent authoritative source is my decision, never a gap to guess.
A hook enforces the query part. The version clause came from fact-checking this series: a checker proposed a migration flag that upstream main has but the target release lacks, and that isn't equivalent to the documented one. Only per-tag source showed that.
The payoff: fewer, evidence-backed questions and fewer false results in PRs and docs.
9. Sessions have priorities: visit the urgent ones more often
An incident outranks a doc tidy-up. A waiting session is stalled, so a rarely visited one runs at my response rate. I visit urgent sessions far more often and answer questions as they arrive.
Having ADHD really helps here: jumping between many short threads is how my attention works anyway. Every session also writes for it: action first, open questions restated in full, one next step.
Rule layering: where knowledge goes
| Layer | Holds | Test |
|---|---|---|
Global CLAUDE.md |
Short non-negotiables pointing to detail docs | Loaded every session; characters cost tokens |
| Global pattern/workflow/troubleshooting docs | Examples, command lists | Default home for anything over one line |
In-repo .claude/ + CLAUDE.md |
Team-visible conventions | Would a teammate's agent act wrongly without it? |
| Per-user memory | Facts about this human | Agent-behaviour lessons are not personal |
- Copy, never point. A team repo never references a personal KB file teammates lack; copy the content (a hook rejects personal-KB paths).
- If you broke an existing rule, extend its doc, never memory: a silent rule lets the next session repeat the breach.
Guard hooks as enforcement
About 49 hook files, 55 test files:
| Class | Example |
|---|---|
Block mutating kubectl |
reads, diff, --dry-run=server, ArgoCD sync patches allowed. |
Block commits/edits on main |
team repos need a worktree; personal exempt. |
| Require full PR URLs | a bare #123 can't be opened from a terminal. |
Prevent scratch files in /tmp |
it vanishes mid-session. |
| Require query verification | denies PromQL/LogQL/SQL in a doc unless that exact query ran live this session: a documented join had never matched a row. |
| Stop-time push check | blocks ending a turn with unpushed commits or a PR-less branch, in every repo (checking only the current repo once missed one). |
The enforcement ladder, weakest first: prose in a doc → a hook that blocks the call → removing the option entirely. A bare-name permissions.deny entry removes a whole tool from Claude's context; a scoped rule like Bash(rm *) only blocks matching calls.
Use PreToolUse (exit 2 blocks; PostToolUse cannot): a post-edit CLAUDE.md size warning only watched it grow; denying the projected size in PreToolUse fixed it.
Hooks are code: test-first, every time
A broken hook blocks work everywhere or silently stops guarding, so hooks get strict TDD:
-
Write the failing test first. Every hook has a suite in
hooktests/; each new case (an input to deny, a near-miss to allow) goes in first. - Prove it goes RED. Run the new cases against a copy of the hook from before the change; a test passing against the old hook may not exercise the branch.
- Make it GREEN, then run the whole suite. One runner executes every suite, a syntax sweep, and a check that every registered hook path resolves (a parsing hook can be dead if unregistered); per-fix spot checks caused the racing failure.
- A hook change without a test change is denied by a commit guard, so time pressure can't skip it.
-
Fail open on unparseable input or a missing
jq; an over-broad Bash guard also removes file search.
Team rules (in-repo)
- Branches
<ticket>/<type>/<desc>, never commit to main, Conventional Commits without scopes (on squash-merge repos the PR title's type is the release bump). - Each repo has a KB index (
.claude/README.md) and its own hooks in.claude/settings.json, so the team runs them too. QuoteSKILL.mddescriptions: unquoted ones hid 9 of 14 skills from a stricter-YAML agent.
Cost: model routing
-
Opus primary plans, synthesises and reviews; cheaper
workersubagents execute in parallel. Routine work: Sonnet primary, with an Opusoraclefor hard decisions. Never downgrade reasoning on risky or irreversible decisions. - Context isolation saves the most: one misconfigured autocompact threshold (a 1M-context session compacting only at about 800k tokens) outweighed everything else.
Pros / cons
| Pros | Cons |
|---|---|
| Real parallelism, full context each | Coordination overhead: briefs, notices, a queue |
| One writer removes write races | Delegate is a bottleneck; you still approve everything |
| Hooks catch repeat mistakes everywhere | Hooks need tests; a bad hook blocks every session |
| Review plus GitOps catches what guards miss | Reviewing every PR is the real, tiring bottleneck |
When not to do this: one or two sessions, or no shared global config.
Gotchas
- After removing a worktree, re-list and tell the owning session, or its next command fails.
- A session's readable name needs a cmux call; the environment holds only UUIDs.
- cmux's default next/previous-workspace shortcut crosses groups; since 0.64.23, bind Next/Previous Workspace in Group.
Takeaways
- One dedicated session writes global rules and hooks; approval is never relayed; the receiver files the ticket.
- Copy team knowledge into the repo; memory holds only facts about the human.
- Climb the ladder: doc,
PreToolUsedeny, remove the option; hooks are test-first with a proven RED. - A human reviews every change; git plus ArgoCD's diff shows nothing slipped past.
- Verify before asking: live environment, docs or source, then the web.
- Name each session's co-workers; visit urgent sessions often.
References
- Claude Code hooks reference (exit codes): https://code.claude.com/docs/en/hooks
- Claude Code permissions (
permissions.deny): https://code.claude.com/docs/en/permissions - cmux changelog (0.64.23): https://github.com/manaflow-ai/cmux/blob/main/CHANGELOG.md
I wrote this article with the help of Claude, an AI assistant. I reviewed and edited it by hand, and it is based on my own hands-on experience.
Top comments (0)