Which skill. Two skills run the same agent directories. This one runs them through the Claude Agent SDK, which starts the Claude Code engine as a subprocess: built-in file and shell tools, Claude Code's own system prompt, its skill and MCP loading, and a long-lived Node host to run on. The embedded-agents-messages-api skill runs them on the Claude API with the Anthropic SDK's tool runner, in the app's own process: only the tools the wrapper gives the agent, no subprocess, any Node host. It is the default for a new product. Use this one when the agent needs what only the engine has, or when the product already runs it.
Read the product's own agent instructions first (its root AGENTS.md or CLAUDE.md), and tell any subagent you spawn for this work to read them too: their rules on secrets, environment variables, validation, callers, tests and release apply to everything here. This skill adds how to run Claude agents inside the app. reference/playbook.md is the full playbook, with the measurement behind every option — read it before changing the wrapper's contract or explaining it in depth.
The model: the app owns the request, the caller, storage and the UI; the agent owns reasoning and tools. An agent is a directory, .claude/agents/<name>/, holding AGENTS.md (its instructions — its only instruction file), and optionally knowledge/, agent-local skills in .claude/skills/, and .mcp.json. One wrapper function, askAgent, runs any of them. No agent name appears in code.
1. First use: install the wrapper
Search the code for askAgent. If it exists, skip to § 2. Otherwise:
- No app yet? Create the smallest Next.js App Router app in TypeScript (what this skill's files are written for) and nothing the user did not ask for — no database, sign-in or billing for a local prototype.
- Install what is missing:
npm i @anthropic-ai/claude-agent-sdk zod server-onlyandnpm i -D vitest. - Copy, changing only import paths:
| From this skill | To the app |
|---|---|
templates/agent.ts |
src/lib/agent.ts — the wrapper |
templates/route.ts |
src/app/api/agents/route.ts — GET lists agents, POST streams one |
templates/agent.test.ts |
tests/unit/agent.test.ts — the contract, inside npm test, no model call |
templates/agents.live.test.ts |
tests/live/agents.live.test.ts — every agent against the real model |
The tests import @/lib/agent, so Vitest needs the same @ alias as tsconfig.json. The live test gets its own Vitest config (include tests/live/**/*.live.test.ts, testTimeout: 180_000, fileParallelism: false) and a script, agents:live. It costs money, so it stays out of npm test and the gate.
4. Environment: ANTHROPIC_API_KEY, plus every secret an agent's .mcp.json references, each set per environment and never committed (the environments skill). On a developer's laptop the SDK falls back to their own Claude login when the key is absent; a deployed product always uses the key.
5. Keep the contract. Every option in agentOptions() exists because an SDK default failed a measurement (playbook § 9). Change one only after re-measuring it.
2. Add an agent
node <skill dir>/scripts/new-agent.mjs <name> [--knowledge] [--mcp], then fill inAGENTS.md: role, sources, output format, refusals. A fixed sign-off line is the cheapest proof, in a live test, that the instructions are in force.- Knowledge: files in
knowledge/*.md, andAGENTS.mdsays to answer only from them. Agent-local skills:.claude/agents/<name>/.claude/skills/<skill>/SKILL.md; found automatically. - MCP: servers in
.mcp.json, secrets as${VAR}, never literal. Runnode --env-file=.env <skill dir>/scripts/discover-mcp-tools.mjs <name>to list what each server offers, then put in that server'sallowedToolsonly what the agent's job needs. Anything unlisted is denied. The agent's.mcp.jsonholds no secrets and is committed: ifgit check-ignorereports it ignored, the repo's.gitignorehas an unanchored.mcp.jsonrule meant for the root file — anchor it as/.mcp.json. - Routing needs nothing:
listAgents()finds the directory,GET /api/agentsreturns it, and — when users choose between agents — the UI's picker shows it. A UI that hardcodes the list is the bug to fix. - The instruction file is always
AGENTS.md. Asked for another name, createAGENTS.mdand say why: the wrapper loads onlyAGENTS.md, and the SDK loads some other instruction files on its own, which would give the agent two sets of instructions. - When the agent makes a promise worth pinning — it cites, it refuses, it answers only from its files — add a live test for it beside the generic one.
3. Examples — only when asked
examples/ holds two finished agents. Use them when the user asks for examples, or describes one of them: an agent that answers only from local product-management knowledge files (knowledge-agent), or one that researches the live web through Perplexity with citations (research-agent). Copy examples/<name>/ to .claude/agents/<name>/ and examples/<name>.live.test.ts to tests/live/. research-agent needs PERPLEXITY_API_KEY. Otherwise, never copy them.
4. The chat UI
Product-specific, so there is no template. What it must do:
- When users choose between agents, fill the picker from
GET /api/agents. A product may run one front agent and show no picker; the route still checks the name againstlistAgents(). POST { agent, prompt, history }to/api/agentsand read server-sent events:{ text }chunks, then{ done, agent, metadata }or{ error, agent }. The parser is in playbook § 5.- Store the agent on every message and, when there is more than one, show it on each reply, so the reader always knows which agent answered. History is the conversation so far, kept in client state unless the product asks to persist it.
- Render agent output as plain text or sanitized markdown, never raw HTML: it is untrusted text, like anything a user typed. Show an
errorevent instead of spinning forever.
5. Explaining routing and the tool boundary
Routing: UI → POST /api/agents { agent } → the route validates the body and requires the name to be in listAgents() → askAgent → agentOptions() accepts only a plain directory name and resolves .claude/agents/<agent>/ → query() starts the Claude Code engine with that directory as cwd and its AGENTS.md appended to the system prompt → events stream back.
Boundary — each layer measured (playbook § 9):
tools: only the built-ins on the allowlist exist —Read,Glob,Grepby default.Bash,Write,EditandWebFetchdo not.permissionMode: 'dontAsk'withallowedTools: an MCP tool runs only if its server'sallowedToolsin.mcp.jsonlists it.strictMcpConfig: only the agent's own.mcp.jsonservers load — nothing from the developer's or the server's user configuration.skills: only the agent's own skills are listed.- The PreToolUse guard blocks any path outside the agent's directory, and any
.envfile. maxTurns(10 by default) andmaxBudgetUsdcap a run.
6. Before it leaves localhost
- Hosting. The SDK runs the Claude Code engine as a subprocess, so an agent cannot run in a serverless function such as a Netlify Function (playbook § 7). A product whose site deploys there needs a second, long-lived Node host (a container or VM, such as Railway), chosen before the agent goes beyond localhost; write its deploy procedure into the product's docs the first time it is used.
next devneeds nothing. - History is a client claim.
templates/route.tsacceptshistoryfrom the client,assistantturns included, andcomposePromptturns it into a "Previous conversation" preamble — so a client can forge earlier turns, the agent's own included. That is acceptable for a read-only agent, which can only be talked into saying something to the person forging it. Before any tool acts on the caller's behalf, keep the transcript on the server and stop reading it from the request. - Caller and cost: before anyone but the developer can reach the route, resolve the caller there (who they are, which workspace, which permission:
saas-authorization) and set a per-actor limit and amaxBudgetUsd. - Release: run
npm run agents:livebefore any release that adds or changes an agent, its prompt or its model, record the run in the product's test-coverage map (which test proves what, and when it last passed), and describe each agent in its automation doc: what it may read, the exact tools it may call, and what the app enforces around it (shipping-artifacts).
How you can tell it worked
The agent runs on the deployed host, not only on localhost, uses only the tools it was given, and the live agent suite passes against the real model.
Running the scripts
Run them from the app's repository root: <skill dir> is wherever this skill is installed (a plugin, ~/.claude/skills/, or the repo's own .claude/skills/), and the scripts write into the current directory.