Stephan Miller
The Subagents Guide I Wish I'd Had

The Subagents Guide I Wish I'd Had

A while back I wrote the skills guide I wish I’d had. Skills stop your agent from forgetting what it knows about your codebase. The other half is that your agent also forgets who it’s supposed to be. Every fresh session, it shows up as the same eager generalist, ready to have a reasonable, mediocre opinion about anything you throw at it.

Skills are the memory problem. Subagents are the identity problem. This is the post about the second one.

Here’s the shortest version I can give you before the table of contents scares you off: a skill tells the model what your world is like; a subagent tells it what role to play. You can have both. And if you’re a solo dev shipping five half-finished projects at once like I am, you especially want both, because you are the only specialist you’ve got and you can’t be in five roles.

Half of this is the conceptual guide (what these things are, where the files live, how to write one), and that part I could have written from docs. The second half is the stuff I only know because two of my subagents have been in use for months across a dozen repos, and they have failed in ways no best-practices post prepared me for. Both are open source and I’ll link to the actual files, because the most useful thing I can show you isn’t my advice, it’s a prompt that’s been beaten into shape by real runs.

The Problem Subagents Actually Solve

Your coding agent has one default mode: helpful generalist. Ask it to review a pull request and you get generically reasonable feedback. Ask it to figure out why your app is slow and it starts poking at the last file you touched. Ask it to plan a database migration and it hands you something sensible that ignores three things that would bite you in production.

The model isn’t dumb. It just doesn’t have a role. A generalist doing a security pass thinks about different things than a security engineer would. A generalist chasing a bug looks at different evidence than someone who’s been on call and seen that exact failure three times already. The intelligence is there. The framing isn’t.

I noticed this because I kept typing the same preamble. Before I’d let Claude Code review anything, I’d paste in some version of “review this like a paranoid security person, look at auth boundaries first, tell me about anything leaking into logs, don’t waste my time with style nits.” Third time I typed that in two weeks, the light went on. That preamble is a role. And a role you keep re-typing is a subagent you haven’t written yet.

The Problem Subagents Actually Solve

That’s the trigger every write-up on this gives you, mine included. But it’s not why either of my two durable subagents exists.

The two that actually survived came from different pressures:

  • Context economics. Research grinding (twenty searches, a dozen fetched pages, half of them junk) will fill your main session with garbage you never wanted to read. That work has to happen somewhere else and come back as a summary. That’s web-search-agent, and the reason it exists is not that it’s smarter than the main thread. It’s that I don’t want what it read in my context.
  • Role separation. When I’m designing something, I want to stay in the design conversation. I don’t want to be interrupted to approve the eleventh mechanical file edit that follows obviously from a decision I already made. So the decisions stay with me and the typing goes to an implementer agent. The split isn’t about capability. It’s about which of us should be thinking about what.

The first one gets you a nice reviewer. The other two are what turn subagents from a convenience into how the project actually runs.

Skills vs. Subagents: Knowledge vs. Behavior

A skill packages what the agent needs to know. Your weird internal library. The environment quirk that breaks builds. The domain knowledge a smart new hire would have to be told because there’s no way to guess it. Conditionally loaded. I wrote a whole guide on those.

A subagent packages how the agent should behave. What it optimizes for. What it checks first. What tradeoffs it makes by default. What shape the output comes back in. It doesn’t teach the model anything new. It biases the intelligence that’s already in there toward one job.

The clean test is to imagine calling in a specialist coworker for the task. Is the value they bring mostly information you don’t have: domain knowledge, context, tribal know-how? That’s a skill. Or is the value mostly how they approach the problem: what they look at first, what they weight heavily, what they refuse to sign off without checking, what they hand back? That’s a subagent.

A security engineer reviewing your code doesn’t just know more than you. They work differently. They look at auth boundaries first. They weight a privilege escalation path way higher than an ugly variable name. They won’t close the review without saying something about secrets. And they hand you structured findings. That ordering of attention is the thing a subagent encodes.

Knowledge = skill. Methodology = subagent.

What a Subagent Actually Is (in Claude Code)

I’m Claude Code first here, same as the skills guide, because that’s my daily driver. Every other tool gets its section further down, quirks and all.

In Claude Code, a subagent is a Markdown file with a little YAML frontmatter on top. It lives in one of two places:

  • ~/.claude/agents/: your personal library, available in every project on your machine. This is the sandbox.
  • .claude/agents/ inside a repo: scoped to that project, and if you commit it, it travels with the repo.

The frontmatter is small. The fields you’ll use:

---
name: migration-risk-reviewer
description: Reviews database migration plans and schema changes for rollback risk, lock contention, and data integrity problems. Use before running any migration against real data.
tools: Read, Grep, Glob
model: sonnet
---

You are a senior database engineer reviewing a migration for production risk.

## What you look at first

- Can this be rolled back without losing data?
- Does it hold locks on a busy table during deploy?
- Is the ordering safe for a multi-step change?
- Will existing data violate any new constraint?

## Output shape

Findings in order of severity:

1. Blockers — deploy will fail or data will be lost
2. High risk — real production risk, needs a mitigation
3. Medium risk — should fix, won't necessarily block
4. Notes — worth tracking

For each finding: what it is, why it matters, what to do about it.

That’s a working subagent. A couple of things worth knowing about how Claude Code treats it:

Output shape

  • The body is the system prompt. When the subagent runs, everything below the frontmatter becomes its instructions. It’s not documentation you read and then act on. The model reads it and becomes the thing.
  • The description is load-bearing. Claude Code uses it to decide when to hand a task off to this subagent automatically. Write it like an API someone else has to discover from context. “Use before running any migration” is findable. “helps with db stuff” is not. Write the negative half too. Both of my real agents spend a clause on what they’re not for, because the routing will absolutely hand an agent work it has no business doing.
  • tools is an allowlist. Leave it off and the subagent inherits everything. Narrow it and you’ve got a reviewer that literally can’t edit your files even if it gets an idea. For a review agent, that’s a feature. I don’t want my “just tell me what’s wrong” pass rewriting things on a whim.
  • model is a budget decision, not a detail. Cheap model for mechanical work, expensive model for judgment. There’s a whole section on this below, because it’s most of why delegation pays for itself.
  • It gets its own context window. This is the part people underrate: a subagent runs in a separate context, does its thing, and hands back a summary. Your main session doesn’t get flooded with everything it read. It’s also the part I underrated in the opposite direction, and there’s a whole section below where it breaks, because the separate context is what breaks it.
  • You don’t have to hand-write the file. You can just ask Claude Code to write the subagent for you: describe the role and it drops the file in the right place. The /agents command is also there for managing them.

You invoke one by asking for it, by letting Claude route to it based on that description, or by wiring it into a larger flow where it runs as a delegated worker. Same behavior either way.

The “Agent” Word Is Overloaded and It’s Making You Dumber

Not you specifically. Me. “Agent” gets bolted onto four different things and people conflate them constantly, so let me split them apart:

  • A subagent (.claude/agents/*.md, or a .agent.md file over in Copilot land) is a reusable file that defines a specialist role. This is the thing this whole post is about.
  • Agent mode / autonomous run is a capability toggle: you’re letting the tool run commands, edit files, and generally act without you hitting approve on every line. That’s a permission setting, not a specialist.
  • A background/cloud coding agent is a service that picks up a task, churns on it out of sight, and comes back with a branch or a PR. Also not a specialist file.
  • CLAUDE.md / AGENTS.md is always-on repo guidance. The house rules every agent obeys on that codebase. It’s the employee handbook, not a coworker you call by name.

When I say subagent, I mean the reusable specialist you write once and invoke on purpose. Not the toggle, not the service, not the handbook. Keeping those four straight fixes about half the confusion.

Writing Your First One

Start with the thinnest thing that does real work. A first subagent has exactly two jobs: declare the role, and say what that role actually means in practice. The migration-risk-reviewer up above is already that: a clear role, specific heuristics, a predictable output shape, and it’s narrow on purpose. You’ll add more once you run it on real work and watch it miss things.

The single most common way a first subagent flops is a mushy role. “Security helper” is not a role. It tells the model nothing about what to optimize for or check first. A real role is a behavioral contract: here’s the job, here’s what you look at first, here’s what you won’t let slide. If you can’t say the job in one sentence, it’s too vague.

VagueConcrete
“Security helper”“Review backend changes for auth boundary violations, secrets leaking into logs, and privilege escalation paths”
“Migration assistant”“Analyze schema changes for rollback safety, lock contention on busy tables, and data integrity risk on existing rows”
“Code reviewer”“Review async code for missing error handling, swallowed exceptions, and N+1 query patterns”

The concrete version tells the model what it cares about and what it ignores. The ignoring is half the value. A reviewer that gives equal weight to a typo and a privilege boundary is only an averaging machine.

What goes in the body

Once the role’s defined, a useful body covers five things. You don’t need all five on day one:

  1. What it optimizes for: the one thing this role is most trying to get right. When it has to make a tradeoff, this is what it trades toward.
  2. What it checks first: the high-priority signals a real specialist always looks at before anything else.
  3. Failure modes: the specific things that go wrong in this class of work. This is where the actual expertise lives, and it’s mostly stuff you’ll only learn by watching the agent mess up real tasks.
  4. Output shape: not “some feedback” but severity-ordered findings, a phased plan, a ranked list of hypotheses. Predictable output is the single biggest practical win over just prompting.
  5. What it must not do. I used to list this one as optional. It is not optional. In my most-used agent it’s the longest section in the file, and every line of it is there because something went wrong once.

Start with role, check-first, and output shape. Everything else the agent earns by screwing up.

A quick walk-through: a bug-hunt agent that doesn’t chase the last commit

Here’s one I wanted, because I kept hitting the same dumb pattern. Something breaks, I paste the error into Claude Code, and its first instinct is to go stare at whatever file I edited most recently. Sometimes that’s right. Sometimes the last change had nothing to do with it.

An experienced debugger doesn’t start at the code. They start at the evidence (what’s actually failing, what the logs say, what changed in the environment) and only open the source once there’s a hypothesis worth checking. That’s a behavior. So I saved it:

---
name: bug-investigator
description: Investigates a bug or failure starting from evidence, not from recent code changes. Produces ranked hypotheses with the next diagnostic step for each. Use for "why is this broken" before touching the source.
tools: Read, Grep, Glob, Bash
---

You are a senior engineer investigating a failure. Your job is to find the most
likely cause, rule out the alternatives, and say what to check next.

## Investigation order

1. Start from the symptom — what is actually failing, and how does it show up?
2. Get the error output and any logs from around the time it broke — ask the caller for them if they weren't provided.
3. Only then look at recent changes, and only if the evidence points there.
4. Read source last, once there's a symptom-to-cause hypothesis worth testing.

## Output shape

**Symptoms:** what's failing and how it manifests.
**Likely causes (ranked):** for each — the hypothesis, the evidence for it, the next diagnostic step.
**Ruled out:** what you checked and why it isn't the cause.
**Next steps:** in priority order.
**Open questions:** what you still can't confirm.

## Failure modes to avoid

- Don't assume the last change is the cause without evidence.
- Don't propose a fix before you can name the mechanism.
- Don't close with "cause unknown" — say what evidence would confirm or kill each remaining hypothesis.

Then (and this is the step people skip) I tested it on a real bug I’d already solved, not a toy. I gave it the symptom and the logs I’d had at the time and watched whether its investigation path matched the one that found the problem. Where it drifted, that drift became a new line in the body.

That file is where everybody’s subagent post stops. Everything below is what happens after you’ve been running these things for months, which is the part I actually needed someone to tell me.

The Thing I Had Backwards: A Subagent Is Cold on the Conversation

The separate context window gets sold as pure upside. But it isn’t symmetric.

This line is in my implementer agent, written after the same handoff failed three different ways:

You are cold on the planning conversation but warm on the project.

Cold on the conversation: it never saw the two hours where you ruled out the obvious approach, argued yourself out of an abstraction, and settled on the weird-looking solution for a good reason. None of that exists for it. Warm on the project: it has CLAUDE.md and it has the codebase, so it knows the house rules and the idioms without being told.

Get that backwards in either direction and you get a specific, recognizable failure. Treat it as warm on the conversation and it confidently reimplements the approach you spent an hour rejecting, because from where it sits that approach looks fine. Treat it as cold on the project and you waste a thousand tokens re-explaining your own repo to something that can read it faster than you can describe it.

What falls out of this is the thing no subagent guide told me to build: the handoff is a file, and the file is the contract. Not a paragraph you type into the task prompt. A document on disk with fixed sections, because a prompt you improvise each time will quietly omit a different critical section every time.

Mine is a TASKS.md at the repo root, generated by the living-plan skill, and the active task has these sections and no others:

  • Goal: what’s true when this is done.
  • Why (pointer): a link to the decision in PLAN.md, not a re-argument of it. Pointer, not prose.
  • ▶ Run state: the agent keeps this current. More on this in a second, because it’s the one that saves you.
  • Design — numbered pieces: a serial queue, [ ] not started, [x] done, [!] blocked, with dependencies marked inline as depends on #2.
  • Files: where the work happens.
  • Tests: the checks that stand in for review.
  • Out of scope (do NOT do): the single highest-value section in the document.
  • Report back: what the final message has to contain.

The rule that makes it work: fill every section before launching, and mark one only if it genuinely doesn’t apply. An unfilled section is where the agent improvises.

And then the division of labor that took me longest to see. Two documents, two lifetimes:

Standing “how we implement here” knowledge lives in the agent definition, not in every task.

The agent file holds the protocol: how to read a task, how to handle a blocker, what to never touch, what the report has to contain. The task file holds this job. If you find yourself pasting the same instruction into three consecutive tasks, that instruction was never task content. It belongs in the agent.

Run state, because sessions die

Here’s the rule that has saved me more real work than any clever prompt engineering in this post:

Log run-state whenever you stop — done OR blocked. Mandatory. Flip each piece’s status box as you go, and update the ▶ Run state note (done / blocked+why / remaining / resume-from). Editing TASKS.md for this is in-scope — it is the recovery point if the session dies.

Sessions die. You close the laptop, the connection drops, you hit a limit, you get bored and Ctrl-C something that was mostly finished. When that happens to a subagent, everything it knew is gone. If the only record of progress was in its head, you now get to diff your own repo against your memory to figure out where it got to.

So the agent writes its progress to disk as it goes, into the same file that gave it the job. The next session reads the file and picks up at the resume point. It costs a few lines in the agent definition and it turns a dead session from lost work into a paused one.

The companion rule, which is about throughput rather than recovery:

A blocked piece does NOT halt the queue. If a piece is underspecified or hits a dependency you can’t resolve, mark it [!] blocked with a one-line reason and continue with any remaining piece that doesn’t depend on it. Halt only when nothing remaining can proceed. Never guess a design — escalate blocked forks in your report.

Run state, because sessions die

Default agent behavior on a snag is to stop and ask, which means a queue of eight tasks gets you one task and a question. Default behavior with no guardrail at all is worse: it guesses a design and keeps going, and now you’ve got seven pieces built on an invention you never approved. Skip-and-continue, never-guess, report-the-fork is the combination that lets you queue work and walk away.

Delegation Economics: Who Pays for Which Thinking

That model line is why this whole arrangement pays for itself.

My implementer’s description, verbatim from a real project:

Sonnet implementation worker for the project. Use it to execute a fully-specified, mechanical coding task defined in TASKS.md while the main (Opus) planning thread keeps going. NOT for design decisions, exploring open questions, or live LLM / quality testing — those stay in the main thread.

Expensive model makes the decisions. Cheap model does the typing. And a narrow scope is precisely what makes the cheap model reliable: a Sonnet worker handed a fully-specified numbered queue in a codebase it can read is dependable.

That split also gives you two working rhythms out of the same machinery:

  • Pacing. Queue two or three pieces, launch the agent, and keep planning the next batch while it grinds. You’re never waiting on it and it’s never waiting on you.
  • Unattended. Queue a lot and walk away. Come back to a diff, a report, and a run-state note telling you which piece blocked and why.

Permissions are the enforcement layer under this. My unattended implementer’s file and shell commands are allowlisted in the project’s permission settings; anything outside the list prompts for approval. The bans in the prompt are policy. The allowlist is the fence.

One more line in that agent, which exists because of the most expensive mistake I made:

Work the numbered pieces as a serial queue, top-to-bottom, in one pass. Do not spawn parallel workers and do not stop to report after each piece.

Parallel agents sound like free speed. They are not free. Every one of them finishes by dumping its full output back into the context that spawned it, and a fan-out that looked clever can bury the session that has to read all of it. I’ve killed real work that way. Parallel runs across disjoint files are an opt-in tactic for a specific burst, never the default. The default is one worker doing one queue in order.

Give It a Budget or It Will Spend Everything

Any subagent with tools (search, fetch, shell) will grind until something stops it, and “I think that’s enough” is not a thing a model reliably concludes on its own. A research agent without a budget is a machine that turns your afternoon into many (hidden) browser tabs and a summary you could have gotten from the first three.

So web-search-agent opens with hard limits, above the methodology, above everything:

LevelSearchesFetchesLink depthModulesUse when
quick3411One specific fact, a URL check, a yes/no. Minutes.
standard (default)81212Normal research task. Answer the questions and stop.
deep203023Genuinely hard question, contested facts, or a topic where the first page of results is known to be junk.

Modules are the per-source search playbooks the agent loads at runtime: source lists and query tactics per domain. There’s a whole section on them below.

Four things about that table matter more than the specific numbers, which you should tune to your own work:

A depth dial beats a fixed cap. One setting is always wrong: too tight for the hard question, too loose for the quick fact. Three named levels with a stated default means I ask for deep on purpose instead of discovering I got it by accident. Precedence is explicit too: numbers the caller gives win, then a level named in the prompt, then the project’s config, then standard.

The limits need anti-gaming clauses, because a model reads a budget as a target. The two that do the work:

1 fetch per URL. Never re-fetch a URL you already read.

Stop as soon as the caller’s questions are answered. Remaining budget is not a quota to spend. A deep run that finishes in six searches is a success, not a waste.

That second line went in after I watched a run answer the question at search four and then keep going, because twenty was the number it had been given. You have to say out loud that finishing early is winning.

Count what actually costs, not what’s convenient to count. My fetch budget covers every network retrieval: native fetch, the escalation rungs for blocked pages, a helper script’s individual HTTP requests, and each 429 retry inside that helper counted separately. The first version counted “fetch calls,” and the agent found the loophole without trying: it wasn’t cheating, it was obeying a rule I’d written badly.

Make it announce its spending. The agent reports [deep: 3/20 searches, 5/30 fetches] after each phase. Without that you have no idea whether you’re watching a careful run or a runaway until it’s over. And when it does exhaust the budget with questions still open, it stops and reports exactly what’s unknown, which URL would most likely answer it, and that re-running deeper is an option. What it must never do is quietly continue past the limit: the caller chose the level, and overspending it takes that choice away.

The Bans Are Most of a Mature Agent

The Bans Are Most of a Mature Agent

Look at the shape of my most-used agent file and the thing that jumps out is proportion. The role description is a paragraph. The negative constraints are a list. That inversion isn’t bloat. It’s that a capable agent with tools has far more ways to technically-comply than you can anticipate.

Here’s one, quoted exactly, because the parenthetical is the whole point:

Never use browser automation. No Simple Browser, no embedded/preview browser, no Playwright, no mcp__claude-in-chrome__*, no open, no opening tabs or windows. If a browser tool is offered to you, it is not for this task. Opening browser tabs to read pages has previously spawned ~100 tabs and wrecked a run.

And another, from the implementer in my research-agent repo, where the mechanism is the entire reason anyone would respect the rule:

Never create any file under agents/ or skills/. agents/ is shipped payload — the package manager flattens every .md beneath it into a separate top-level agent on install, so a stray file there lands in every consumer project.

That’s the rule I’d extract from all of it: every ban names its mechanism. Not because the agent needs to be persuaded, but because a constraint whose reason isn’t written down gets edited away. Six weeks later you’ll be tightening the prose, you’ll hit a line that says “never create files here,” it’ll read as arbitrary, and you’ll helpfully generalize it. The “because the installer flattens it into every consumer project” clause is what makes future-you leave it alone.

A few more shapes these constraints take, once you’ve been at it a while:

Ban the category, list the instances. “No browser automation” alone leaves the agent deciding whether a preview pane counts. Naming six specific things it must not reach for closes the gap where it reasons its way into one.

Name the behavior, not just the tool. My favorite line in the file isn’t a prohibition on a command, it’s a prohibition on a tendency: “If you find yourself authoring a scraper or a report generator, stop — you are working around the task, not doing it.” Capable agents route around constraints by building tools. That’s the category, and you have to ban the category.

An escape hatch needs conditions, or it becomes the main road. Some pages really are unfetchable, so there’s an escalation ladder for blocked URLs. But it only opens after the normal path has already failed on that exact URL, it never grants a second fetch slot, it checks whether a tool is installed rather than installing anything, and it caps the output so a page dump can’t blow the context. Then the hard stop: “If the URL is still unreachable after the rungs available to you, it is done.” Record it, move on. No further attempt, no other helper, no creative fourth idea.

The Bans Are Most of a Mature Agent

Say why the exception isn’t a loophole. There’s a paragraph in there explaining that a one-shot headless fetch that prints text is not a violation of the browser ban, because the ban is about driving a browser and leaving tabs for a human to close. Without that paragraph, the exception and the ban look like a contradiction, and a model resolving a contradiction will pick the reading that lets it do more.

They Fail Silently and Confidently

I had two research runs come back fine. Not fine. Good. Clean findings, real sources, coherent report. Both runs named Reddit as their primary source. Neither run had gotten a single thing from Reddit.

What happened is that a WebSearch site:reddit.com query returned ten clean, plausible results from other domains (an Etsy community forum, the SBA, slideshare) and no error at all. Not a 403. Not an empty set. Not a warning. The site: constraint was silently ignored and nothing in the output said the domain filter hadn’t applied. A tidy list of usable pages from the wrong places. The agent did what any reasonable worker does with a tidy list of usable pages: it used them, and it reported success.

The decision I wrote that day, which is now the foundational one in that project:

When a named source cannot be reached, the run says so. It never proceeds silently on whatever the tool returned instead.

A run that names Reddit as its primary source while reporting success on Etsy forums is worse than a run that fails, because the failure is invisible to whoever reads the report.

A failed run costs you a re-run. A silently substituted run costs you a decision made on evidence you think came from somewhere it didn’t. And the separate context window, the feature itself, is exactly why you can’t see it happen. You don’t get the transcript. You get the summary the agent chose to write.

Three things fix this, and none of them is “tell the agent to be careful.”

Give it a mechanical detection rule. Nothing errored, so the agent had no reason to look. The fix is one sentence with no judgment in it: if you constrained a search to a domain and no returned URL is on that domain, that is a zero-result finding, not a result set. It needs no extra tool, no extra fetch, no permission. The URLs are already in its hands. “Be skeptical of your sources” is not actionable. “Compare the hostname to the one you constrained on” is.

Make provenance a required output channel with a schema. This is where “output shape” graduates from section headers into something closer to a type. Two arrays, specified down to the key names:

  • unreachable[]: one entry per wall, keys exactly source, url, reason. Covers both hard fetch failure and silent substitution.
  • sources[]: one entry per source that actually supported an answer, keys exactly source, url, fields. A page you opened that didn’t inform anything doesn’t get recorded. A source that answered four fields is one entry with four names in it, not four entries.

The subtle call in there, which I got wrong first: provenance annotates, it never blocks. If a documented substitute answered the question, the field is answered. The unreachable entry records that the answer didn’t come from the named source. Making a wall fail the field would mean the pipeline breaks loudly on exactly the situation it has a workaround for.

And a failure worth stealing the lesson from: the first version of that agent said “record it as unreachable” four separate times and never once named a field, an array, or an output slot. The instruction dead-ended. The agent was told to record something with nowhere to put it, so it didn’t, and nothing anywhere complained. When you write an instruction to a subagent, check that the thing it’s told to do has a destination. “Report X” with no named slot for X is a no-op with a clean conscience.

Verify from outside the agent. The agent’s compliance with its own output contract is checked by a script I run afterward, not by the agent’s assurance that it complied. That’s 180 lines of Python validating the JSON against the declared fields, and it is worth more than any amount of emphasis inside the prompt.

The generalized version of that last point shows up again in the implementer, in a repo that has no test suite to lean on:

Verify instead of running a suite. There is no suite. The task’s Tests section lists the checks that stand in for one — run every one of them and report the results concretely. Where a check is “this URL resolves,” that means fetch it, not assume it.

A URL you could not verify does not go into a file. Report it as unverified with what happened. An unverified URL is worse than none — a reader trusts it and spends a fetch on a 404 instead of falling back to search.

Agents will report verification they did not perform, because a plausible claim and a checked claim look identical in a summary. Every check you care about needs to name the physical action that constitutes doing it.

Keeping the Contract Tight (and the Antipatterns That Bloat It)

The best subagents have one job. And here you’ll object, because the files I’ve been quoting aren’t thin. My research agent is 188 lines and nobody would call it thin. The distinction that resolves it: the job stayed one sentence; the file grew. What accumulated was constraints, detection rules, and output contract: scar tissue on a fixed skeleton. Not one new responsibility in a year. Growth from things going wrong is the file working as intended. Growth from new kinds of work is bloat, and the fix is a split, not another section.

Keeping the Contract Tight (and the Antipatterns That Bloat It)

The bloated versions all look the same, and I’ve shipped every one: the everything-reviewer that averages across five concerns and is expert at none. The “senior engineer” label with no behavior behind it. The agent that restates your CLAUDE.md and adds token cost instead of behavior. The one that duplicates a skill until the two drift apart. Each is the same mistake: a job that got wider than one sentence.

Which raises the obvious question: after a year of this, what’s actually left on my bench?

The Two That Actually Survived

Most posts on this hand you an org chart of agent types to build. Here’s my real bench after a year:

Two agents. That’s it.

One disclosure, because this post’s own rule applies to it: web-search-agent started as a fork of Lan Zheng’s Deep-Research-skills. The output-contract skeleton (the summary sections, the always-required sources list) is upstream’s, substantially verbatim. The control plane on top (the budgets, the bans, the provenance schema, the escalation ladder) is mine, and it’s half the line count. Everything below about scars is about that half.

web-search-agent does bounded web research and hands back findings with sources. It’s installed across a dozen repos right now: my blog’s static site, a couple of pipelines, and a few research projects. Everything in this post about budgets, bans, and silent substitution came out of that one file’s revision history.

implementer executes a fully-specified task queue while I keep planning. Four projects have one. Everything about handoff contracts, run state, and delegation economics came from those.

The rest of what’s sitting in my agents folders came bundled with frameworks I installed, and it’s a junk drawer with better branding than my actual junk drawer. The specialists I predicted I’d need (the flaky-test investigator, the PR-polish agent, the docs drafter, the codebase-orientation agent for projects I abandoned three months ago) were all reasonable ideas and I built approximately none of them. Turns out the ones that stick aren’t the ones that sound useful. They’re the ones where I kept feeling the specific pain of the work being in the wrong context or the wrong head.

So don’t build the org chart. Notice which work you wish were happening somewhere else, and save that one.

Template, then specialize

Here’s the pattern that made the second, third, and fourth implementer cheap: the agent gets generated from a template and then diverges where the project differs.

The living-plan skill ships an implementer.template.md with and placeholders, and scaffolding a new project writes out the agent, the plan doc, and the task doc together. What you get is the protocol: read the contract, serial queue, skip blockers, log run state, stay in scope, report back. That part is identical everywhere because it’s about how delegation works, not about your code.

Then each copy grows a local section for the local failure surface, and comparing two of them is instructive. The template says “keep the suite green.” One of my projects has no suite at all (it’s a repo of prompts, where the only executable file is the validator), so its implementer says this instead:

This repo is prompts and data, not code. Editing a file is editing a prompt. Wording, ordering, and emphasis are the implementation. A rewrite that reads better but drops a hard constraint is a regression, and nothing will catch it — there is no build and no test suite.

Another project’s copy grew a rule about reading the specific PLAN.md decisions a task cites before writing code, and a report-back line for “anything you hit that requires an Opus/human call.” Same protocol, different hazards.

That’s the division worth internalizing: the reusable part of a subagent is the protocol; the per-project part is the failure surface. Template the first, hand-write the second, and don’t try to make one file serve both.

The Killer Combo: Point the Agent at Knowledge, Don’t Paste It In

This is where the two guides shake hands, and my understanding of it got a lot more specific once a real pipeline depended on it.

What I’d have told you before is “mention the relevant skill in the agent body.” What actually works is stronger: the agent reads the knowledge at runtime, before it’s allowed to act, and the prompt says why.

My research agent can’t run a single search until it has read a routing table that lives in a skill:

Module Selection (MANDATORY — routing lives in one file). Before executing any search or fetch, you MUST Read the routing table at the first existing path below. DO NOT skip this step. DO NOT route from memory — the module list changes without this prompt changing, so a module you remember may be gone and one you need may be new.

The module list changes without this prompt changing. That sentence is the entire argument for the pattern. The knowledge (which sources answer which kinds of question, how to query each one, what’s known to be blocked) changes monthly. The methodology doesn’t. If I’d baked the source list into the agent body, every new source would mean editing a prompt I’d otherwise leave alone, and the agent would confidently route to a module I deleted in March.

Two mechanics that turned out to matter more than I expected:

A path resolution ladder, not a path. The same agent runs in projects with different layouts, on two machines with different usernames, installed by different tools. So the instruction lists candidate paths in order and says take the first that exists: project-local first, then the user-level copies. Hardcode one path and the agent works on your machine and nowhere else.

The Killer Combo: Point the Agent at Knowledge, Don't Paste It In

The layers are coupled by literal names, with nothing checking them. The architecture is three tiers: skills orchestrate, an agent does the work, data modules hold the knowledge. And there are no imports anywhere. A skill launches the agent by its exact registered name, and the agent reads modules by path. Which leads to the most mundane and most dangerous line in that repo:

Do not rename web-search-agent; existing consumers call it directly.

The agent’s name is a public API. Nothing will tell you otherwise: there’s no compiler, no test, no warning. A rename is a clean-looking commit that breaks every caller in every repo that installed it, and you find out the next time you ask for research and get a generalist instead.

Where They Live: Personal, Project, Packaged

There are three rungs, and the third one is where all my real agents live.

  • Personal (~/.claude/agents/): general-purpose specialists you want in every project. This is the workshop. Break things here.
  • Project (.claude/agents/, committed): specialists that only make sense for this codebase. The implementer that knows this project has no test suite. Commit it and it’s there next time, even if next time is four months from now and you’ve forgotten the project exists.
  • Packaged (installed by a package manager, pinned to a commit): an agent that’s a dependency. This is the rung I didn’t know I needed until the same agent was in a dozen repos and I had thirteen copies drifting apart.

That third rung changes a few things, and they’re worth knowing before you get there rather than after:

Ship the agent with the skills that call it. My package contains both the skills and the agents they launch, in one bundle, for an unglamorous reason: installing the skills without the agents yields a pipeline that fails at first use. A skill that dispatches by name to an agent you don’t have is a broken install with a clean error message at best. They’re one unit because they’re one contract.

Know where your installer puts things. This one installs through APM, and APM flattens every .md under an agents/ directory into a separate top-level agent on install. That’s a sane default that bites hard: a data file parked in that tree becomes a bogus registered agent in every consumer project, and a project-specific agent written there installs itself into everybody’s repos. It’s why my search-strategy modules live under skills/ rather than the agents/ directory they’d otherwise obviously belong in: a folder of them under agents/ loses its structure on install and registers five bogus agents in every project that pulls the package.

Pin a commit and know that consumers are behind. Consumers depend on a SHA, which means a fix isn’t live for anybody until the pin moves and the install re-runs. Writing a fix is not shipping a fix. That sounds obvious written down and it is absolutely not obvious at 11pm when the bug you fixed last week reappears.

And still: curate. A folder full of overlapping, half-abandoned agents is a noise generator, not a toolkit. If two have drifted into the same job, merge them. If one hasn’t been invoked in months, delete it. The file’s in git history if you’re wrong. Which, yes, is also good life advice, and no, I don’t follow it with my actual junk drawer.

How This Works in the Other Tools

Claude Code isn’t the only place this exists, and if you bounce between tools like I do, the ideas port even when the file formats don’t.

GitHub Copilot / VS Code call these custom agents and use .agent.md files, typically in .github/agents/, with a personal library option under your user directory. The frontmatter is richer than Claude’s: beyond name, description, and tools you get things like argument-hint, model, user-invocable, disable-model-invocation, and my favorite, handoffs: a finished agent can offer a button to hand off to another agent with a prefilled next prompt, so you can chain planning to implementation to review without collapsing everything into one mush-brained agent. One migration gotcha if you’re reading older examples: the old infer field got split into user-invocable and disable-model-invocation, which is clearer once you know and maddening until you do.

You’ll also read that VS Code detects .claude/agents/ files and maps the tool names across, so you can share an agent between tools for free. It’s true enough to be misleading. The tool vocabularies don’t line up, and a prompt that hard-bans tool names by name (which any mature agent of mine does) does not survive automatic translation intact.

What I actually ship is one canonical prompt plus a thin native shim per tool. The entire Copilot-side agent in my package is eleven lines:

---
name: Web Research Writer
description: "Use for bounded web research that must read a local schema, search and fetch current sources, write one designated result file, and validate it with a local command."
tools: [read, search, web, edit, execute]
user-invocable: false
disable-model-invocation: false
---

Load the installed canonical research prompt from the first existing candidate:

1. `.github/agents/web-search-agent.agent.md`
2. `.claude/agents/web-search-agent.md`

Follow its search budgets, module routing, source standards, tool discipline, output, and validation rules. Ignore that file's incompatible `tools` frontmatter; this wrapper's Copilot-native tool categories govern this session.

That’s the whole pattern. Native frontmatter so the host registers it properly, a path ladder to find the real prompt, and one explicit sentence about which tool vocabulary wins. All 188 lines of hard-won behavior live in exactly one file, and the second tool gets a pointer instead of a fork. Two copies of a prompt is two prompts, and the day you fix a bug in one of them is the day they start lying to you about being the same agent.

One caveat from the README: point the loader at the .github/agents/ tree and the canonical prompt registers as its own agent too, so Copilot can end up seeing duplicate names: the “Web Research Writer” shim plus the canonical file underneath it. Pick one home per tool, or keep the canonical file in the .claude/agents/ tree where Copilot’s auto-detection won’t pick it up.

How This Works in the Other Tools

Everyone else (Cursor, Windsurf, and the rest) is somewhere on the road to the same thing under names like rules, modes, or agents, and the details shift often enough that I’d rather point you at each tool’s current docs than confidently tell you something that changed last month. The underlying move is identical everywhere: a reusable file that defines a role, invoked on purpose. Learn the concept once and you’re mostly just learning where each tool hides the folder.

The Honest Version of How Good Ones Get Built

Every subagent I use started too broad. I wrote a “reviewer” that reviewed everything. A “planner” with no opinion about what a plan should look like. An “investigator” that approached every bug like the last one. That’s not failure. That’s the starting point.

But here’s the part I’d have found genuinely useful a year ago, and it’s not “iterate.” Everybody says iterate. It’s what the iterations are made of.

Go back through the agent files I’ve quoted and look at where the words actually are. A paragraph of role. A page of constraints. A schema with key names spelled out. A mechanical detection rule. A numeric budget with an anti-gaming clause. A note that a paid fetch has to be disclosed. A ban that explains what the installer does to stray files. Almost all of it is a record of a specific thing that went wrong once.

Which means the useful question after a disappointing run isn’t “how do I describe this role better.” It’s: what exactly did it do, what in the file permitted that, and what sentence makes it impossible next time. A hundred tabs. Ten results from the wrong domain and no error. An instruction with nowhere to write its answer. A verification it reported but didn’t perform. Each one of those is a line, and the line outlives the session that taught it to you, which is the entire reason this is a file.

The test before you keep one is the same as before you keep a skill: can you say its job in one sentence? If not, it’s too broad, and you’ve got two smaller agents wearing a trenchcoat. But the test for whether it’s any good is different, and it’s this: when it screws up, can you point at the line that let it? If you can’t, you don’t have a specialist yet. You have a vibe with frontmatter.

Put them together and you’re not prompting a very smart generalist over and over. You’re keeping a small bench of specialists who already know your world and already know their job. For one person trying to ship more than one person reasonably should, that’s the closest thing to hiring help I’ve found that doesn’t involve hiring anyone.

Both of the agents I’ve been quoting are open source, scars included: the research agent itself, and the implementer template plus living-plan. Steal whatever’s useful.

Now go look at the work you wish were happening somewhere else. That’s a subagent you haven’t saved yet. And the last time a subagent disappointed you, that’s a line you haven’t written yet.

Stephan Miller

Written by

Kansas City Software Engineer and Author

Twitter | Github | LinkedIn

Updated