●NEST — Subagents can now spawn nested subagents up to depth 3 by default, up from 1, making research/build/verify pipelines practical without extra setup●APISKILL — The bundled claude-api skill now defaults to Claude Opus 5, with a documented migration path from Opus 4.8●ENVVAR — ${VAR} entries in managed MCP allowlists and denylists now resolve from the startup environment and managed-settings env, not the settings-file env●A11Y — A screen reader mode lets you follow a session with assistive tech, including announcements of deleted text●VOICE — Voice mode now runs on Opus, Sonnet, and Haiku alike, reaches connected tools like Gmail and Slack, and supports many more languages●TEACH — Claude for Teachers launched on July 14, alongside a $10M commitment to Canadian AI research●NEST — Subagents can now spawn nested subagents up to depth 3 by default, up from 1, making research/build/verify pipelines practical without extra setup●APISKILL — The bundled claude-api skill now defaults to Claude Opus 5, with a documented migration path from Opus 4.8●ENVVAR — ${VAR} entries in managed MCP allowlists and denylists now resolve from the startup environment and managed-settings env, not the settings-file env●A11Y — A screen reader mode lets you follow a session with assistive tech, including announcements of deleted text●VOICE — Voice mode now runs on Opus, Sonnet, and Haiku alike, reaches connected tools like Gmail and Slack, and supports many more languages●TEACH — Claude for Teachers launched on July 14, alongside a $10M commitment to Canadian AI research
Claude Code Multi-Agent Parallel Execution — Task Tool and SubAgent Patterns That Hold Up in Practice
Measured results from parallelizing Claude Code with the Task tool and SubAgents. Covers what to split and what never to split, a validating aggregation layer in TypeScript, and the disk contention and stale-input traps I hit along the way.
Back when I was running an article pipeline across four sites as an indie developer, watching a type check finish felt like a waste of a life. Tests could run. Linting could run. But only one at a time.
I parallelized the whole thing with the Task tool and SubAgents. The CI-equivalent stage went from 31 minutes to 9. That first run felt genuinely good.
And then it stopped improving. No matter how much I raised the degree of parallelism, nothing moved. Parallelism only touches the part of the work that can run independently. The rest is serial by nature, from the very beginning. This piece is mostly about where that line sits and how to find it.
What the Task Tool Actually Does
The Task tool is Claude Code's built-in mechanism for spawning new agents — SubAgents. Three behaviors matter.
Contexts are fully isolated. A SubAgent does not inherit the parent's conversation history. That is a design advantage, not a limitation. Each agent reasons in its own context, so running them together never contaminates one another's thinking.
Independent calls can be issued in a single message. Batch Task calls that have no dependency on each other into one message and their execution overlaps. If a call needs the previous result to compute its arguments, you have to wait. Whether something can be parallelized comes down to one question: are the arguments already known?
Results come back as strings. If you want structured data, instruct the SubAgent to return JSON — and validate it in the parent. Skip that validation and things break quietly, as I'll show.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A decision table for what to parallelize and what to keep serial — plus why the wrong call produces no errors and a broken artifact
✦A TypeScript aggregation layer that validates SubAgent JSON: absorbs preamble text, missing fields, and partial failures
✦Measured breakdown of a 21-minute pipeline dropping to 12 — and the serial section that refused to shrink
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The first decision in multi-agent design isn't how to word the prompts. It's what to split. Here is the table I work from.
Nature of the work
Parallelize?
Why
Read-only analysis (type check, lint, tests)
Yes
No side effects, order is meaningless
Work on disjoint file sets (per-module review)
Yes
Write targets never overlap
Queries against different resources (DB, API, disk)
Yes
Latency is dominated by I/O waits
Writes to the same file
No
Last writer wins, one edit vanishes
Work depending on an earlier result (build then test)
No
Arguments aren't known until runtime
Mutating shared state (env vars, global config)
No
Readers observe an indeterminate state
Getting this wrong is nasty, because nothing fails. Every agent reports success. The aggregate looks clean. Only the artifact is broken.
I once handed two SubAgents an append operation on the same config file. Both replied that they had appended. Exactly one edit survived. I didn't notice until I walked back through the logs.
When writes are involved and you're unsure, fall back to serial. Protect correctness before speed.
A Three-Agent Parallel Setup
This is the shape I actually run in my indie developer repository after a large change. Three SubAgents, each given independent read-only work.
Agent 1: type check and lint
## SubAgent: TypeScript CheckerYou are a code quality checker. Run the following and return JSON only.No preamble, no explanation.1. Run `npx tsc --noEmit 2>&1` and collect errors2. Run `npx eslint src/ --format json 2>&1`3. Summarize counts and severityOutput:{"tsc_errors": 0, "eslint_errors": 0, "eslint_warnings": 0, "details": ""}If a command cannot run, return {"error": "reason it could not run"}.
Agent 2: tests and coverage
## SubAgent: Test Runner1. Run `npx jest --coverage --json 2>&1 | tail -1`2. Extract the coverage summary3. Collect failing test names as an arrayOutput:{"total_tests": 0, "passed": 0, "failed": 0, "coverage_pct": 0.0, "failed_tests": []}On timeout, return {"error": "timeout"}.
Agent 3: dependency audit
## SubAgent: Security Auditor1. Run `npm audit --json 2>&1`2. Extract high and moderate vulnerabilities3. Assemble the fix commandOutput:{"high_risk": 0, "moderate_risk": 0, "affected_packages": [], "fix_command": ""}
All three are read-only and their write targets never overlap. That is exactly why they can be launched together in one message.
Write the Aggregation Layer With Validation
This is where the implementation lives or dies. SubAgents do not always return the format you asked for. They add a sentence of preamble. They drop a field. They time out and return nothing at all.
The parent absorbs all of it.
// aggregate.ts — validate SubAgent output while aggregating ittype Ok<T> = { ok: true; name: string; data: T };type Fail = { ok: false; name: string; reason: string };type AgentResult<T> = Ok<T> | Fail;export function parseAgentOutput<T extends object>( name: string, raw: string, required: (keyof T)[],): AgentResult<T> { // SubAgents often prepend a sentence, so extract the first JSON block only const block = raw.match(/\{[\s\S]*\}/); if (!block) return { ok: false, name, reason: "no JSON block found" }; let data: T; try { data = JSON.parse(block[0]) as T; } catch (e) { return { ok: false, name, reason: `parse failed: ${(e as Error).message}` }; } if ("error" in data) { return { ok: false, name, reason: String((data as Record<string, unknown>).error) }; } const missing = required.filter((k) => data[k] === undefined); if (missing.length > 0) { return { ok: false, name, reason: `missing fields: ${missing.join(", ")}` }; } return { ok: true, name, data };}export function aggregate(results: AgentResult<object>[]) { const succeeded = results.filter((r): r is Ok<object> => r.ok); const degraded = results.filter((r): r is Fail => !r.ok); return { // Abort only on total failure. Partial failure degrades and continues. verdict: degraded.length === results.length ? "aborted" : degraded.length > 0 ? "partial" : "complete", succeeded: succeeded.map((r) => r.name), degraded: degraded.map((r) => ({ name: r.name, reason: r.reason })), };}
Why pull the first JSON block with a regex? Because "return JSON only" still yields Understood. Here are the results: in front of the payload roughly one time in ten, in my experience. A strict JSON.parse(raw) dies right there.
Why make verdict three-valued? Because killing the whole run when one SubAgent times out throws away two perfectly good results. Keeping a partial state lets a human say: type check and tests passed, re-run the audit only.
Measured: What Shrank and What Didn't
Before and after, in seconds, median of three runs.
Stage
Serial
3-way parallel
Notes
Dependency install
212
212
Shared prelude, cannot shrink
Type check + lint
388
497
Bounded by the slowest branch (tests)
Tests + coverage
497
Dependency audit
171
Aggregate and write results
19
34
Cost of adding validation
Total
1,287 (~21 min)
743 (~12 min)
As measured on this repo
The 31 minutes I opened with was a cold-cache first run. Measured properly it's 21 minutes down to 12 — still a 42% cut.
The part worth staring at: the parallel section is bounded by its slowest single branch. While tests take 497 seconds, nothing else you speed up matters. Raising parallelism to four or five did nothing. What helps is shortening the longest branch. That realization is what finally pushed me to split integration tests into a separate job.
And the dependency install sits there at 212 seconds — 28% of the total, stubbornly serial. Until that shrinks, the ceiling on parallelism is already visible. Parallelizing a pipeline is, among other things, an exercise in making its serial sections impossible to ignore.
Three Traps I Fell Into
Three agents fighting over one disk.
When the execution environment was low on space, SubAgents unpacking caches at the same time failed partway through writes. Worse, the failure mode was polite: the command exited zero and returned truncated content. Before parallelizing, check free space and write permissions on the temp directory. It's an unglamorous step, and skipping it cost me half a day of hunting.
An agent reading a stale clone.
I reused a shallow clone as a scratch workspace, and one agent read a days-old snapshot and correctly reported "this feature isn't implemented." The agent wasn't lying. The input I gave it was old. Any SubAgent that makes a judgment call needs freshly refreshed input. Input freshness moves outcomes far more than prompt craft does.
Batching a check and the action that depends on it.
Put "run the gate, and if it passes, push" in one message and the push call can be issued before the gate's result is ever read. I shipped an artifact that hadn't passed, exactly this way. A verification step and the action that depends on it belong in separate turns. The same principle again: only calls whose arguments are already known may run together.
Common Anti-Patterns
Trying to hand context to a SubAgent.
# ❌ Based on our conversation so far, understand the project and refactor it# ✅ Refactor getUserById in src/api/users.ts:# - change the parameter to an object { id: string }# - throw NotFoundError instead of returning null when absent# - keep tests/api/users.test.ts passing
"Our conversation so far" does not exist for a SubAgent. Everything it needs goes in the task instruction.
Passing output downstream unvalidated.
Inserting parseAgentOutput alone eliminates most of the mysterious aggregation bugs.
Leaning on retries.
"If it fails, try again" is worse than it sounds, because failures stop being observable. Have the agent return {"error": "reason"} and let the parent decide. A structured failure is far easier to work with than a quiet success.
In Production: Review Assistance
The place I feel the benefit most is reviewing a change set.
change set finalized
│
├─ collect the list of changed files (serial — it defines the next arguments)
│
├─ [parallel] ──────────────────────
│ ├─ Agent B: review the logic changes
│ ├─ Agent C: check tests cover the change
│ └─ Agent D: check docs stay consistent
│
└─ parent: validate all three, aggregate by priority
Only the first step is serial, because that's where the file list handed to B, C, and D becomes known. I spent a while trying to parallelize it anyway. If the arguments aren't determined, it cannot run in parallel. A simple rule, but drawing it makes the hesitation disappear.
How to Start Small
You don't need a five-way fan-out on day one. The order I recommend:
Write out every step currently running serially and mark each with two flags: does it write anything, and does it consume an earlier result? Only the steps with no on both flags are candidates.
Then pick exactly two of them and run them together. Type check plus lint is usually the safest pair.
Finally, add validation equivalent to parseAgentOutput before you add a third. Defer validation and the mysterious inconsistencies begin the moment parallelism increases.
Follow that order and parallelism stops being frightening. Even running just tests and linting together changes the texture of the waiting.
I'm still in the middle of this myself — the test split is only half finished. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.