p4ni.

Comparison

Claude Code Skills vs Subagents: 27 Runs Looking for the Delegation Threshold

· 9 min read

On this page

Search claude code skills vs subagents and every result draws the same diagram. A skill loads instructions into the session you are already in. A subagent runs in its own context and reports back. Skills are for how to do something, subagents are for heavy or parallel work.

That is accurate. It is also useless the moment you have written both and one of them refuses to run.

I measured that gap last week: the same request, the same description, fired a skill 5 out of 5 times and a subagent 0 out of 17. My explanation was that the task — convert a four-row CSV into a Markdown table — was too small to be worth delegating, and I said so in the limits section:

Somewhere between “read one CSV” and “audit forty files” the parent starts delegating on its own, and finding that boundary is a different experiment.

This is that experiment. I ran it expecting to find the boundary and report where it sits. There is no boundary. Twenty-seven runs, up to 201 files, and the parent never delegated once.

The rig

One subagent in .claude/agents/, named probe-scout, with a plain English description carrying a trigger clause — the exact style that won every English trial in the previous experiment:

---
name: probe-scout
description: Investigates a codebase and reports what it finds. Use when the user
  asks to search, survey, audit, or summarize files in the repository.
---

Only one agent is installed, so nothing competes with it. Name collisions and description bake-offs — the things that decided the previous experiment — are off the table here. The only variable is how big the job is.

Three synthetic repositories, generated from a seeded script so anyone can rebuild them byte for byte:

  • repo — 41 TypeScript files with 72 TODO comments scattered through them. Everything you might want from it is greppable.
  • repo2 — 41 files where the answers are not greppable. Each exported function has one of four bodies, three of which throw on an empty string and one of which looks like it does but doesn’t.
  • repo3 — the same construction at 201 files.

The trap in repo2 matters, so here it is:

export function billingService3(input: string): string {
  const parts = input.split(",");
  return parts[1].toUpperCase();   // "" -> parts[1] is undefined -> throws
}

export function auditHandler2(input: string): string {
  return input.split("/").pop().slice(0, input.indexOf("="));
  // "" -> "".slice(0, -1) -> returns "". Does not throw.
}

Asking “which exported functions throw on an empty string” against that codebase cannot be answered with grep. You have to read the bodies and reason about them. That is the shape of task every explainer says a subagent is for.

Every run is a fresh claude -p session with no mention of subagents. Delegation is measured from the parent’s Task tool calls in the JSON stream, not inferred from what the model says it is doing.

One thing I got wrong first

My first pass counted 41 Read calls on the parent’s side of a run that had delegated, which would have meant the parent duplicated the whole job. It hadn’t. In --output-format stream-json, the subagent’s own messages appear in the same stream, tagged with a subagent_type key. Filter on that key or your parent and child tool calls end up in one pile:

claude -p "..." --output-format stream-json --verbose \
  | jq -r 'select(.type == "assistant" and (has("subagent_type") | not))
           | .message.content[] | select(.type == "tool_use") | .name'

Twenty-seven runs, zero delegations

TaskParent tool callsDelegatedWall clock
Summarize one fileRead:10/48s
Count TODOs across five named filesBash:2–30/412s
Collect every TODO in 41 filesBash:3–50/430s
Three independent surveys at onceBash:7–100/449s
Read 10 files, find the throwing functionsRead:100/430s
Read 41 files, same questionRead:41 Bash:2–30/465s
Read 201 files, same questionBash:7–150/383s

The sixth row is the one that ended the search for a threshold. The parent opened forty-one files one at a time, spent 44 tool calls and 50 turns doing it, and never considered handing any of it off. If reading an entire codebase file by file is not heavy enough to trigger delegation, nothing in normal use is.

File count is not task size

The middle rows explain why the earlier numbers looked so flat. Collecting every TODO across 41 files sounds like a big job and takes three grep calls. So does surveying three independent things at once. The parent sizes the work before it starts, and it is not counting files:

41 files — small enough to inspect directly.

That is the parent’s own narration in one of the three-surveys runs, right before it ran grep. Another said I'll survey the src/ directory directly. The judgement being made is not “how much material is there” but “how many tool calls will this cost me,” and a grep over a thousand files costs one.

This is the part the concept diagram hides. “Use a subagent for large codebases” implies the model is measuring the codebase. It is measuring its own effort.

At 201 files it gets cheaper, not more collaborative

The obvious next move is to push past what the parent can hold. So repo3 is 201 files with 177 genuinely throwing functions, and the same non-greppable question.

The parent did not delegate. It stopped reading.

Every run at 201 files solved it with Bash alone — 7 to 15 calls, no Read at all — by pattern-matching the four known function bodies rather than reasoning about them one at a time. It got the right answer that way, all three times, 177 out of 177. But note the direction it moved: given a job too big to do properly, it found a cheaper method rather than a second worker.

If your mental model is “the subagent kicks in when the context gets tight,” this is the counterexample. The escape hatch it reaches for is a better shell command.

The rig is not broken

Add three words — use a subagent — to the 41-file reading task and the picture inverts, exactly as it did with the small CSV:

RunAgent calledParent tool callsSubagent tool callsWall clock
1probe-scout245136s
2general-purpose246127s
3probe-scout152180s

Three for three. And the delegation happens on the first or second tool call — the parent does not attempt the work and give up partway, it reads the request and hands it over immediately. So the 0/27 above is not “cannot delegate.” It is “will not, unasked,” and the two look identical from the outside.

Two details worth flagging, both at n=3 so treat them as observations rather than findings.

One run in three chose the built-in general-purpose agent over mine. probe-scout’s description matches the request — it names surveying and auditing a repository — and it still lost a third of the time to an agent I did not write. If you are wondering why your custom agent gets skipped in favour of a generic one even after you asked for delegation, this is that, and description quality is not obviously the lever.

Delegating cost 2–3× the wall clock. The same job the parent finished in 57–71 seconds took 127–180 seconds through a subagent. That is the price of a separate context: the child re-derives everything the parent already knows, then the parent waits and summarizes.

Does the work come out better?

This is the case for subagents, so I scored the answers against the generated ground truth. Recall was near-perfect everywhere — one run missed 2 functions out of 36, everything else found all of them. The interesting variation was in false positives, and specifically in the trap:

ConditionTraps it wrongly flagged
10 files, read directly5 of 5, in all four runs
41 files, read directly10 of 10 in three runs, 0 in the fourth
41 files, delegated0, in all three runs
201 files, grep strategy0, in all three runs

Reading every file by hand is what made the model over-call "".slice(0, -1) as a crash. The delegated runs and the grep runs both stayed clean.

I want to be careful here, because the neat story — “delegation improves accuracy” — is not what the data supports. The fourth direct-reading run also scored zero traps, with a mixed strategy of 15 reads plus 7 greps. With n=3 and n=4 per cell, what I can say is that the delegated runs did not do worse on a task the parent was perfectly capable of, and that the extra 60–110 seconds did not buy a better answer.

So which one do you reach for

The concept split is real, and after two experiments I would restate it in terms of what actually happens rather than what each feature is for:

  • A skill changes what the current session does next, so the model loads it without being asked. The right description gets it loaded 5 times out of 5 on an exact-match request. If you want something to happen reliably and unprompted, it belongs in a skill.
  • A subagent is work you have to request. Not because the description is weak — I tested eight styles including MUST BE USED and got zero — and not because the task was too small, which is what I assumed until this experiment. It simply is not a decision the parent makes on your behalf at any size I could construct.
  • Write subagents for jobs where the isolation is the point. A separate context that returns a summary is genuinely useful when you do not want 200 files of noise in your main session. That is a reason to invoke one deliberately, not a mechanism that will engage itself when things get big.
  • Say use a subagent, or name the agent. It took delegation from 0/27 to 3/3 here and 0/17 to 13/13 in the previous round. There is nothing clumsy about asking.

The practical version: if you find yourself writing a subagent description hoping the model will notice it and delegate, you are building on something that does not happen. Put the instructions in a skill, or plan to ask for the agent by name.

Limits

Claude Code 2.1.241. Thirty runs total, each a fresh claude -p session, delegation read from the parent’s Task calls with parent and child messages separated by the subagent_type key.

The repositories are synthetic. Real code is more varied, and it is possible that a task whose difficulty is legible from the file names — a security audit across a genuinely tangled codebase — reads differently to the model than 201 files of generated TypeScript. What I can rule out is size alone doing it: 41 files read individually, and 201 files that could not be read at all, both produced zero delegations.

I also did not test the newer multi-agent surfaces where delegation is the explicit point of the feature. This is about the case people actually hit — a subagent sitting in .claude/agents/, looking correct, never running.

Same method as the AGENTS.md test and the skill description test: plant a fact the model cannot fake, remove every way to infer it, and run it enough times to see whether the answer holds. The difference this time is that the prediction under test was my own, and it did not survive.