Series · Claude Agent SDK in Production · Part 11 of 14

· 35 min read

Claude Agent SDK in Production, Part 11: Subagents and Skills: The Analyst Grows a Team

A reviewer subagent re-derives the numbers in a fresh context and catches what the analyst missed, nested badges show the colleague working, and a SKILL.md hands out the house report format on demand instead of on every prompt.

claude-agent-sdk · subagents · skills · tutorial

While recording Part 7's demo video, I threw away a take because the analyst volunteered, on camera, that the top two stores brought in "63% of total company revenue". The tables around that sentence were correct to the penny. The 63% was invented; the true share was 57.8%. Part 6 had already caught this species twice: perfect SQL-backed tables, decorated with hand-summed totals that were wrong. And this morning, while building this very part, it happened again, twice in two runs: "Portland stores account for 63% of total company revenue" (true value: 59.0%), and "38% above the chain average" (true value: 48.9%). The grounding rule helps. The eval suite in Part 13 will measure it. But you already know how real teams solve this, because no bank ships a report the analyst graded themselves: someone else checks the numbers. Today the analyst gets that someone: a reviewer subagent with fresh eyes and a strictly read-only toolbox, plus a skill that carries the house report format. And a certain duplicated March row, planted in the sample data since Part 1, finally meets its match.

The end of this part, from a real run: an independent set of eyes re-derives the report's every number, catches the wrong 63% AND a duplicated database row, and the analyst fixes its own report. $0.0630 for the whole review. Every figure verified against SQL.

That screenshot is one message: "Have the reviewer double-check that report before I send it out." Everything indented under the delegation badge is a second agent working in a context the first one never touched. This part builds that colleague, wires its activity into the transcript as nested badges, and then hands the analyst the other half of teamwork: the employee handbook, as a skill it reads when the job calls for it.

Why "double-check your work" doesn't work

The cheap fix looks obvious: add "always double-check your numbers" to the system prompt. Here's why that mostly buys you a more confident wrong answer. When the analyst re-checks its own work, the check runs inside the same context as the mistake: the conversation history, the reasoning that produced the number, the number itself sitting right there looking plausible. The model re-reads its own working and agrees with it. Ask a person to re-grade their own exam and you get the same effect; the literature calls it anchoring, and you don't fix it with a sterner prompt.

A subagent fixes it structurally. The SDK can spawn a second agent, in a fresh context, that receives exactly one thing: the brief inside the delegation call. Not the conversation. Not the analyst's reasoning. Not the system prompt the analyst runs on. If the analyst's number is wrong, the reviewer cannot inherit the path that led there, because it never saw it. Fresh context is not a limitation of subagents; it is the entire point. Link for the concept: subagents.

The roster: one AgentDefinition

A subagent is defined, not summoned: you describe the colleague on ClaudeAgentOptions and the main agent decides when to call them. Two pieces make delegation exist at all. First, the roster:

backend/app/runner.py
REVIEWER = AgentDefinition(
description=(
"A meticulous numbers reviewer. Delegate to it to independently "
"re-derive key figures and check data quality before numbers go "
"to the user."
),
tools=[
"Read", "Glob", "Grep",
"mcp__beanline__query_database", "mcp__beanline__get_schema",
],
model="haiku",
)

Read the tools= list slowly, because it's a security decision disguised as a config line. The reviewer gets the desk's search tools and the two read-only database tools from Part 6, and that's all. No Bash, no Write. It cannot change a byte on disk, which means nothing it does can ever need a Part 7 approval card, and there's nothing to approve because there's nothing it could break. (Yes, an AgentDefinition can carry MCP tool names, verified live; and yes, I tried a Bash-armed reviewer first: its shell commands routed into the same can_use_tool gate as anyone's, seventeen approval cards in one review. Each colleague gets the toolbox their job needs, and a checker's job needs read-only.)

The description is doing quiet work too. It's Part 6's lesson again, one level up: tool descriptions are prompts, and a subagent's description is what the main agent reads when deciding whether to delegate. Ours says what the reviewer is for and when to use it. One house rule in ANALYST_PROMPT completes the wiring, the same when-not-how split as every tool we've shipped:

backend/app/runner.py
- When the user asks you to double-check, verify, or review numbers, delegate
to the reviewer subagent instead of re-checking your own work, then report
what it found, discrepancies and data-quality flags included."""

Second piece: the delegation tool itself. Subagents ride a built-in tool named Task, and like everything since Part 1, it only exists if tools= says so:

backend/app/runner.py
tools = ["Read", "Glob", "Grep", "Bash", "Write", "AskUserQuestion", "Task", "Skill"]
if mode == "plan":
tools.append("ExitPlanMode")
return ClaudeAgentOptions(
cwd=str(workspace),
tools=tools,
# The team roster: every key here becomes a subagent_type the main
# agent can delegate to. The CLI launches subagents in the
# background by default on current versions; the env line restores
# synchronous delegation, which is what a chat turn wants.
agents={"reviewer": REVIEWER},
env={"CLAUDE_CODE_DISABLE_BACKGROUND_TASKS": "1"},
# "project" scopes discovery to the workspace, and the server is
# the only writer of workspaces: the skill on the desk is the one
# we installed there. This stays OFF until a part needs it; magic
# you didn't opt into is how reader machines and yours diverge.
setting_sources=["project"],
mcp_servers={"beanline": beanline_server},
allowed_tools=[
"Read", "Glob", "Grep", "Skill",
"mcp__beanline__query_database", "mcp__beanline__get_schema",
],

agents={"reviewer": REVIEWER} makes every dict key a subagent_type the model can name. Skill and setting_sources are the second half of this part; ignore them for a few minutes. The line that needs its own section is the env= one, because without it, this part doesn't work the way you'd expect.

The trap: subagents run in the background by default

On current CLI versions, delegation is asynchronous out of the box. The main agent calls the Task tool, and instead of waiting, it gets a result back immediately: "Async agent launched successfully." The subagent keeps working in the background; the main turn wraps up and ends. I captured it live, without the env line:

The same delegation, twice, real runs. Async by default: the 'result' is a launch confirmation, and the main turn ends at 8.4s while the reviewer is mid-audit. With the env line: the Agent call blocks until the reviewer reports back.

Background agents are a real production pattern: Claude Code's own UI launches subagents this way, tracks them with task_started and task_progress system messages, and lets you check in on them later (that agentId in the result is the handle). For a long-running research assistant, that's exactly what you want. For a chat product where this turn's answer depends on the review, it's a trap: the analyst would tell you "I've delegated the task to the reviewer" and end the turn, receipt and all, with the report arriving into a void. One environment variable restores synchronous delegation, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1, passed through the options' env= so it applies to the SDK's subprocess and nobody's shell profile. That's the honest state of this surface in 0.2.110: the default serves the CLI's interactive UI, the env line serves an embedded product, and you should know both exist.

Watching the colleague work

Delegation now works; next, make it visible, because a badge-less sixty-second silence while a subagent audits your numbers reads as a hang. The SDK already tells us everything: while a subagent works, every one of its messages carries parent_tool_use_id, the id of the delegation call that spawned it. The main agent's messages carry None there. One measured surprise made the frontend's life dramatically calmer: on this CLI, subagent output produces no StreamEvent deltas at all. Its prose never streams into the main reply; its activity arrives only as complete messages, parent id attached. So the translator forwards exactly one new fact:

backend/app/events.py
elif isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, ToolUseBlock):
if block.name in CARD_BACKED_TOOLS:
hidden.add(block.id) # the gate already emitted the card
continue
event = {
"type": "tool_use_start",
"tool_id": block.id,
"tool_name": block.name,
"tool_input": block.input,
}
# Part 11: a subagent's messages carry the id of the
# delegation call that spawned them. Forward it and
# the UI can nest the colleague's work under it.
if message.parent_tool_use_id:
event["parent_tool_id"] = message.parent_tool_use_id
yield event

Count the vocabulary changes: zero new event types. tool_use_start gains one optional field, parent_tool_id, and a Part 3 client that has never heard of it keeps working untouched. Nine parts of only-ever-extending, still holding. On the client, the reducer looks up the parent to learn the colleague's name (the delegation call's own input carries subagent_type, so no lookup table anywhere):

frontend/app/page.tsx
if (event.type === "tool_use_start") {
// Part 11: a parented call belongs to a subagent. Find the delegation
// call it hangs off and read the roster name from its input, so the
// nested badge can be labeled without any new lookup table.
const parent = event.parent_tool_id
? blocks.find((b) => b.type === "tool_use" && b.id === event.parent_tool_id)
: undefined;
return [
...blocks,
{
type: "tool_use",
id: event.tool_id,
name: event.tool_name,
input: event.tool_input,
done: false,
parentId: event.parent_tool_id,
agentName:
parent?.type === "tool_use"
? String(parent.input.subagent_type ?? "subagent")
: undefined,
},
];
}

and the badge itself learns an indent, a hairline, and a name tag. Not a redesign; the same ToolBadge, shifted right:

frontend/components/ToolBadge.tsx
export function ToolBadge({ block }: { block: ToolBlock }) {
const [open, setOpen] = useState(false);
return (
// Part 11: a subagent's call is the same badge, indented under the
// delegation call with a hairline and a name tag. Nesting is one
// level deep by design; the roster has no sub-subagents.
<div
className={`my-1.5 max-w-xl overflow-hidden rounded-lg border border-stone-200 bg-white dark:border-stone-800 dark:bg-stone-900 ${
block.parentId ? "ml-7 border-l-2 border-l-stone-300 dark:border-l-stone-600" : ""
}`}
>
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center gap-2.5 px-3 py-2 text-left hover:bg-stone-50 dark:hover:bg-stone-800/60"
>
<StatusIcon block={block} />
{block.agentName && (
<span className="shrink-0 rounded bg-stone-100 px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider text-stone-500 dark:bg-stone-800 dark:text-stone-400">
{block.agentName}
</span>
)}

Run it and the transcript does something it has never done in ten parts: it shows two agents at once.

Mid-review, mirrored from the recorded demo run: the delegation badge spins for the whole review while the reviewer's own calls land nested under it, name-tagged. The spinner on the parent is honest: the analyst really is waiting for the report.

Three notes for the record, all verified. The delegation call arrives in the stream as a ToolUseBlock named Agent (the toolbox lists Task; the block says Agent; our toolLabel handles both). Part 8's hooks see every one of the reviewer's calls, so your audit log covers new hires automatically, no registration changes. And because these are ordinary tool_use_start events riding Part 9's log, nested badges are born durable: refresh mid-review and the replay rebuilds the whole hierarchy, parent lookup and all. Measured in this part's e2e: 57 milliseconds from reload to nested badges back on screen. One honest limit: the Part 5 history endpoint replays old conversations from the main session's diary, which stores the delegation call and its final report but not the nested calls; those live in the subagent's own transcript (the SDK ships get_subagent_messages if you ever want the full archaeology).

The review that pays for the part

Now the run this series has been setting up since Part 1. The Beanline sample data has always contained one planted flaw: the row 2026-03-01,S02,P10 appears twice, identical to the byte, $87.50 each time. Ten parts of analyses have sailed past it, because nobody asked "is this data clean?" and the analyst doesn't volunteer suspicion. Watch what an independent checker does with the March total:

  1. The analyst queries the database: $206,815.80. Correct, for the data as it stands.
  2. It delegates: Agent {subagent_type: "reviewer", description: "Verify March 2026 total revenue", prompt: "The analyst reports $206,815.80 for March 2026. Re-derive it…"}. The brief is all the reviewer gets.
  3. The reviewer re-derives the total with its own SQL, then runs the checks nobody asked for: row counts, date coverage, negative values, and an exact-duplicate query (GROUP BY every column, HAVING COUNT(*) > 1). One row comes back.
  4. Its report lands as the delegation call's tool result: total confirmed, duplicate found, with the corrected figure: $206,728.30 if the double-counted row is removed.
  5. The analyst relays both, and flags that someone should decide whether that's a data-entry error or a real repeated sale. Receipt: $0.0365, 31 seconds, 8 reviewer tool calls.

That last step matters: the reviewer doesn't fix anything, because it can't, by toolbox. It reports; the analyst (or you) decides. And the hero screenshot at the top of this page is the same machinery pointed at a whole report: the reviewer read report.md off the desk, re-derived all six store rows exactly, caught the invented 63% (its report, verbatim: "Portland market concentration claim: INCORRECT... $122,027.60 out of $206,815.80 total = 59.0%, not 63%"), caught the duplicate and its per-store impact (Airport drops to 10,511 units and $43,338.40 without it), and the analyst rewrote its own report with a card-approved Write. The cold open's failure class, caught by structure instead of vigilance.

The tool result carries one more gift. The CLI appends a receipt trailer to every delegation result, straight from the real run: <usage>subagent_tokens: 5558 tool_uses: 8 duration_ms: 23635</usage>. Your per-review accounting, free.

Ten parts of analyses walked past that receipt. The colleague who never saw the working found it in eight tool calls.

Fresh context cuts both ways

Here's the wrinkle most subagent tutorials skip, and it cost me a real run to learn. My first working reviewer found the duplicate and helpfully offered the corrected total, $206,728.30. Right number. Then I checked how it got there: no query computed it. The reviewer had subtracted $87.50 in its head, in prose, the exact method our analyst has been banned from using since Part 6. Of course it did. The grounding rule lives in ANALYST_PROMPT, and the reviewer has never read ANALYST_PROMPT. Fresh context means fresh ignorance of your house rules. The isolation that prevents anchoring also strips every lesson the main prompt has accumulated; whatever still matters, you restate in the AgentDefinition's own prompt. Ours now carries the full checklist, its own grounding rule included:

backend/app/runner.py
prompt="""You are the Beanline reviewer. You will be given numbers and the
question they answer. Every review has two mandatory parts:
1. Numbers: re-derive each figure with your own database queries. Do not
assume the analyst's approach was right; write your own SQL.
2. Data quality: run the checks nobody asked for, on the relevant range.
Always include an exact-duplicate check (GROUP BY every column, HAVING
COUNT(*) > 1). Look for gaps and suspicious values too, and cross-check
the desk's CSV files with the search tools where they cover the same
ground.
Report back briefly, in two sections (Numbers, Data quality): each figure
confirmed or corrected, each flag with the query or file evidence for it.
A review that only re-runs the analyst's query is not a review. Every
number in your report must come from a query result, corrected figures
included: never do arithmetic in your head. You share that rule with the
analyst you are checking.""",

That prompt is also three measured drafts, same texture as Part 10's house rule. Draft one ("re-derive the numbers, report discrepancies") produced a review that ran one confirming query and declared victory: $0.0511 spent re-deriving the analyst's own arithmetic, no duplicate check, no catch. The mandatory two-section structure with a named duplicate query is what made the catch reliable, and the closing sentence is what made the reviewer's own numbers query-backed; in the very next run it computed a per-store sum with a SELECT 51319.6 + 43425.9 + … rather than in prose, which is both slightly ridiculous and exactly right. A reviewer is an agent too. It needs the same engineering as the agent it checks.

Skills: the handbook nobody recites

The reviewer fixes who checks. The second half of this part fixes how work gets formatted, and it starts from a smell you've watched grow for seven parts: ANALYST_PROMPT. Chart rules, report rules, grounding rules, asking rules, reviewing rules. Every rule rides on every request, billed every turn, whether the turn writes a report or answers "which store sold the most lattes". Push that trend three more parts and the system prompt becomes an employee who recites the entire handbook every morning.

The SDK's answer is a skill: a markdown playbook that lives on disk, announces itself with one description line, and gets read on demand, only when the model decides the job calls for it. Here is the whole house report format, one file:

backend/skills/beanline-report/SKILL.md
---
name: beanline-report
description: The Beanline house format for written reports. Load this before writing report.md or any findings document for the user.
---
# The Beanline house report format
Every report.md follows this structure, in this order:
1. Title line: `# Beanline analysis: <topic in a few words>`
2. `**TL;DR:**` one bold sentence carrying the single most important number.
3. `## Key numbers`: a markdown table of metric and value. Format currency
as $1,234.56: dollar sign, thousands separators, two decimals.
4. `## How this was computed`: one bullet per data source, naming the tool
(database query or file) and the operation, so a colleague could redo it.
5. `## Caveats`: always present. List the data-quality issues you noticed
(duplicate rows, gaps, outliers). Write "None found" only if you
actually checked for them.
6. Last line, exactly: `Prepared by the Beanline analyst.`
Chart conventions: PNG at dpi=150, headline-style titles, labeled axes
with units, no gridlines, one chart per file.

The SDK discovers project skills at <cwd>/.claude/skills/<name>/SKILL.md, and our cwd is the workspace, so the server installs the handbook on every new desk:

backend/app/workspaces.py
# Part 11: the skills every desk starts with. The folder ships with the
# backend; create_workspace() copies it to where the SDK looks for
# project skills: <cwd>/.claude/skills/<name>/SKILL.md.
SKILLS_SOURCE = Path("skills")
def create_workspace() -> str:
"""Mint a new desk. The id is the server's choice, never the client's.
Part 11: every new desk gets the house skills installed under
.claude/skills/. The workspace is the agent's project directory, so
project-scoped discovery (setting_sources=["project"]) finds them.
"""
workspace_id = uuid.uuid4().hex
workspace = WORKSPACES_ROOT / workspace_id
workspace.mkdir(parents=True)
if SKILLS_SOURCE.is_dir():
shutil.copytree(SKILLS_SOURCE, workspace / ".claude" / "skills")
return workspace_id

Enabling discovery is the two options you already saw in build_options: setting_sources=["project"] (the switch this series has deliberately kept off since Part 1, now scoped to a folder only the server writes) and Skill in tools= plus allowed_tools (reading instructions is not a mutation; no card). One small housekeeping note rides along: the artifact watcher's snapshot now skips dotted directories, or the installed SKILL.md would appear in the panel as something the agent produced.

One more measured honesty before the payoff. With the skill installed and discoverable, my first report request produced... no skill load at all. The model went straight to Write; the handbook sat on the shelf. Init data showed beanline-report right there in the discovered skills. Discovery is not motivation: a busy model treats a one-line skill description like you treat documentation, which is to say, optimistically. The fix is the same when-vs-how split as the database tool and the reviewer: one line in ANALYST_PROMPT ("Before writing report.md, load the beanline-report skill with the Skill tool and follow the house format it describes, exactly"), and from then on every report run opens with a Skill badge before the Write. The prompt says when. The skill says how, in as much detail as the job needs, billed only when a report is actually being written.

The same question, twice, report.md verbatim from both runs. The skill bought structure: TL;DR, methodology, mandatory caveats, sign-off. It did not buy arithmetic: the right sheet's caveat still volunteers a wrong 63%. Structure is the skill's job; numbers are the reviewer's.

Look at the right sheet's caveat one more time, because the two halves of this part just shook hands. The skill made the analyst volunteer a market-concentration caveat, in perfect house format, with a hand-summed percentage that's wrong by four points. Format discipline and numerical discipline are different failure modes, and the hero screenshot at the top of this page is the reviewer catching this exact sheet's error. A skill without a reviewer writes beautiful wrong reports; a reviewer without a skill corrects scruffy ones. Hire both.

The system prompt is what the analyst recites every morning. A skill is the page it looks up when the job actually calls for it.

The delegation, drawn once

Everything in this part, on one sheet, with the real run's numbers:

The whole mechanism: a brief goes in, parented messages ride the stream, a report and a receipt come back. The premium on the same question, measured: $0.0095 alone, $0.0365 reviewed. What the review bought: a duplicated row and a wrong percentage, caught.

The graduation table, complete

Part 8 opened this table with five mechanisms and a promise that Part 11 would finish it. Here it is, complete. Seven mechanisms now shape what the analyst does, and choosing the wrong one is still the most common failure in real agent codebases:

MechanismThe question it answersEnforced byHow it fails
System promptWhat should it do?The model's goodwillPersuasion, not enforcement: a determined input talks it out
tools=What exists for it?The SDK, absolutelyCan't be talked around, but it's all-or-nothing per tool
allowed_toolsWhat runs without asking?Name match, before your codeNames, not arguments: Glob waved through means every glob
can_use_toolWhat does a human say, now?Your callback + a FutureHumans sleep, get tired, click Approve on the tenth card
HooksWhat is always true, and who saw it?Your code, first, every callOnly as sharp as your patterns
SubagentWho does this job, with whose context?A fresh context + its own toolboxCosts multiply; and a colleague needs its own rules, or it improvises
SkillWhat knowledge arrives, and when?On-demand loading, model-initiatedA handbook nobody opens: discovery is not motivation; the prompt still says when

The new rows answer questions none of the first five could touch. Governance says what the agent may do; a subagent changes who is doing it and what they know; a skill changes what expertise is in the room for this specific job. Read the table bottom to top when quality disappoints, the same way you read it when something breaks: was the knowledge even loaded (skill)? Did the right specialist do it (subagent)? Was it on the record (hooks)? Was a human asked (callback)? Was it pre-approved (allow list)? Could it exist (tools)? Did we merely hope (prompt)? This table is the series' synthesis, and it's yours now.

The cost ritual

All real runs from building this part:

RunResultCost
March total, no reviewone query, correct answer$0.0095 · 4s
March total + reviewer double-checkduplicate row found, corrected total offered$0.0365 · 31s
First reviewer prompt draftone confirming query, no duplicate check: not a review$0.0511 · 11s
The review that first caught the duplicate9 read-only queries, dup + corrected total$0.0347 · 26s
Report with the skill on the deskfull house format, table exact, caveat's 63% wrong$0.0277 · 13s
Same report question, Part 10 appplain format, "38% above average" wrong (true 48.9%)$0.0358 · 20s
"Double-check that report" (the hero)caught the 63% AND the duplicate, rewrote report.md$0.0630 · 62s
Async-default spike (main turn only)"launched successfully", report into the void$0.0299 · 8s
Fresh-clone e2e, both turnsskill report + review, nested badges back 66ms after reload$0.0295 + $0.0666
The recorded demoduplicate catch on camera, below$0.0492 · 32s

Read rows one and two as the price of trust: a nickel for an answer, four nickels for an answer someone independent has re-derived, dissected for duplicates, and signed off on. Row seven is the full workflow at report scale. Whether that premium is worth it depends entirely on what the number is about to be used for, which is why it's a phrase you say ("double-check this") and not a mode you set.

A real run, recorded: "What was total revenue for March 2026 across all stores? Have the reviewer double-check the number before you answer." The delegation badge opens, the reviewer's queries nest under it with name tags, and the reply reports the total, the duplicated row (2026-03-01, S02, P10, $87.50), and the corrected figure, receipt on camera: $0.0492, 32 seconds.

What you built

Part 11
  • A reviewer subagent via agents={'reviewer': AgentDefinition(...)} plus Task in tools=: the main agent delegates a brief, a fresh context re-derives the numbers, and the report comes back as the delegation call's tool result (with a usage trailer as the receipt).
  • Fresh context is the feature and the cost: no anchoring on the analyst's working, and no inheritance of its house rules either; whatever still matters gets restated in the AgentDefinition prompt, grounding rule included.
  • The reviewer's toolbox is a security decision: Read/Glob/Grep plus the read-only database tools, no Bash, no Write, so a review can never mutate the desk and never raises a card. Subagent calls still pass hooks and the gate like anyone's.
  • Nested badges from one optional field: subagent messages carry parent_tool_use_id, the translator forwards it as parent_tool_id on tool_use_start, and the UI indents and name-tags: zero new event types, durable through Part 9's log (nested badges rebuilt 57ms after a mid-review refresh).
  • Skills are on-demand playbooks: SKILL.md installed on every desk, discovered via setting_sources=['project'] plus the Skill tool, nudged by one prompt line saying when; the before/after runs show house structure appearing while the arithmetic mistakes stay for the reviewer. Sync delegation needs env CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 on current CLIs.

Test yourself

Score ··
01

What makes delegation exist in this app, and what arrives in the stream when it happens?

02

Why can the reviewer catch a number the analyst got wrong, when the analyst re-checking itself usually can't?

03

How do the reviewer's tool calls become nested badges, and what changed in the wire vocabulary?

04

You delegate on a current CLI without CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1. What happens?

05

The skill file is discovered (init lists beanline-report) but reports come out unformatted. What's the most likely cause, per this part's measured runs?

Commit it, from the project root:

BASH
git add backend frontend
git commit -m "part 11: subagents and skills - a reviewer with fresh eyes and the house playbook on demand"

The analyst has a team now: a colleague who re-derives its numbers without inheriting its assumptions, and a handbook it reads at exactly the right moment. But every tool on every belt so far was built in this house, by us, in this very series. Real products plug in tools other people built, and the MCP ecosystem has thousands of them, one config block away. Which raises the question Part 8's tripwire callout left hanging: a tripwire is not a sandbox, and code you didn't write deserves walls you didn't have to remember to build. Next: the analyst reaches the open internet through an external MCP server, and we put the whole thing in a box the operating system enforces.

The complete, tested code for this part lives in part-11-subagents-skills in the companion repo. Code blocks with a GitHub icon link straight to the exact file; "View full file" shows the whole file in place with this section's changes highlighted.