Appearance
Building Domain-Specific Agents
Agentic HandbookCentral thesis: Agentic engineering leads down one path — better agents, then more agents, then custom agents — and the Claude Code SDK lets you control the core four (context, model, prompt, tools) to solve your domain-specific problems better than any out-of-the-box agent.
Overview
Out-of-the-box agents like Claude Code are incredible, but they're built for everyone's codebase, not yours. That mismatch costs hundreds of hours and millions of tokens as your codebase grows. Custom agents flip the equation so your compute works for your domain, your problems, your edge cases — where all the alpha in engineering lives, in the hard specific problems generic agents can't solve.
Custom agents let you scale the core four beyond the defaults, solve domain-specific problems with repeatable workflows, and protect your codebase and teammates from agents calling the wrong tools at the wrong time. They take two tactics to their limit: templating your engineering directly into the agent, and pushing one agent, one prompt, one purpose.
But you don't always need to reinvent the agent. By deploying programmatic agents like Claude Code via the SDK, you can make a few tweaks — hide some tools, append the system prompt — and get better performance without going fully custom. There are extended agents (you keep Claude Code, tweak a little) and complete custom agents (you overwrite the system prompt and bring your own tools).
This lesson builds up through eight demo agents on the Claude Code SDK, from a 150-line Pong script to a multi-agent streaming UI, so you can see how to deploy agents across scripts, terminals, data streams, and full user interfaces.
Key concepts
Better → More → Custom agents
The natural progression of agentic engineering. First master a single agent (prompt + context engineering). Then add more agents — scale compute via sub-agents and new primary agents (delegation). Then, only when you're pushing the edge, build custom agents tailored to your domain. Both truths hold at once: use out-of-the-box agents as much as you can, and reach for custom agents when you need to outperform them.
The Claude Code SDK build recipe
Every custom agent follows the same four-step loop. Whenever you see ClaudeCodeOptions, isolate the core four and ask: how is each managed given this setup?
flowchart LR
A[1. Options
configure the core four] --> B[2. Mount
ClaudeSDKClient with options]
B --> C[3. Query
send the user prompt]
C --> D[4. Manage responses
assistant / result / system blocks]
D -->|resume session_id| C- Options —
ClaudeCodeOptionsdeclares model, system prompt, allowed/disallowed tools, MCP servers, hooks, and resume ID. This is your core four minus the user prompt. - Mount —
query()for one-off prompts;ClaudeSDKClientfor continuous conversations. - Query — send the user prompt via
client.query(...). - Manage — stream and parse response blocks: assistant messages (text, tool-use, tool-result), result messages (session ID, cost), and system messages (available tools).
The SDK ships with built-in prompt caching, so you save money just by using it.
The 8 demo custom agents
| # | Agent | Form factor | Key idea it teaches |
|---|---|---|---|
| 1 | Pong | one-off script | Full system-prompt override — "always respond Pong" proves the system prompt builds the agent |
| 2 | Echo | script (continuous) | Custom in-memory MCP tool via @tool; downgrade model to Haiku; ClaudeSDKClient for follow-ups |
| 3 | Calc | terminal UI | disallowed_tools to strip tools from the window; resume to continue a session |
| 4 | Social Hype | stream handler | Data-streaming agent on the Bluesky firehose; static-variable-as-law in the system prompt; notify tool |
| 5 | QA | terminal UI (expert) | Codebase Q&A expert; external Firecrawl MCP config; hooks blocking .env reads; read-only tools |
| 6 | Try Copywriter | web UI + backend | Agent in a UI; all tools disallowed; one prompt → N variations via a Pydantic response schema |
| 7 | Micro SDLC | multi-agent UI | Plan→build→review→ship task board; per-agent write hooks; mostly extends Claude Code |
| 8 | UltraStream | multi-agent UI | Two agents (streamer + inspector) over a huge log file; fine-grained chunk-reading tools for token efficiency |
Extend vs. custom — decision rules
Build a custom agent when you need programmatic agents, repeat workflows, domain-specific problems solved better than anyone, lower cost with high performance, or permission checks to protect your work and teammates — and above all, when you want to stay out of the loop.
Use out-of-the-box (or extended) agents when you're in the loop prompting back and forth, exploring or prototyping, doing generic engineering the SDK already handles well (80% of situations), or running short-lived, lightweight, non-repeatable tasks. Don't try to rebuild Claude Code — it owns agentic coding. Extend it: hide a couple of tools, append the system prompt, add a hook.
The dividing line: you have a complete custom agent when you overwrite the system prompt, and it's defined further when you bring your own custom tools. The Micro SDLC agent (#7) only appends the system prompt and reuses Claude Code's tools — an extended agent. UltraStream (#8) overwrites everything — not Claude Code at all, just the SDK harness.
disallowed_tools
Everything an agent can see — every tool — ends up in the context window and costs tokens. A bare Echo agent still inherits ~15 baked-in Claude Code tools it doesn't need. disallowed_tools removes them entirely: there's no read option for them anymore, they never reach the context window. Pair with allowed_tools to give a fine-grained agent exactly the tools it needs (the Calc agent has just two). This is core-four tools control, encoded in code so the agent always behaves this way.
Hooks for permissions
Hooks tap into the permission system for governance, control, and safety — essential when deploying agents for teammates. Match on pre_tool_use / post_tool_use; in a pre-tool hook you can block dangerous actions (e.g. block reading .env files on a Read match, or restrict a planner agent so it can only write to the specs/ directory). You do anything you want in the hook and return a concise reason to the agent, which can then pivot.
How to apply it
- Start with the recipe: options → mount → query → manage. Get a Pong-simple agent running first.
- Overwrite the system prompt to define the agent — it's the law that multiplies every user prompt. Put it at the top of your options.
- Add custom tools with the
@tooldecorator and an in-memory MCP server when the agent needs domain actions; write clear descriptions (they tell the agent how to use the tool). - Strip tools with
disallowed_toolsso nothing unnecessary sits in the window; whitelist withallowed_tools. - Right-size the model — drop to Haiku for simple agents, Sonnet/Opus for reasoning.
- Use
ClaudeSDKClient+resumefor continuous conversations; parse result messages for the session ID and cost. - Add hooks for permission checks before deploying agents to a team.
- Choose the form factor: script, terminal UI, stream handler, or backend method behind a UI — deploy where the ROI is.
- Extend before you rebuild: append the system prompt and reuse Claude Code's tools for real engineering work; go fully custom only for domain-specific problems.
Commands & conventions
Minimal Claude Code SDK custom agent (Python):
python
from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions, tool, create_sdk_mcp_server
@tool(name="echo", description="Echo text back, optionally reversed/uppercased")
async def echo(args: dict) -> dict:
text = args["text"]
# back in deterministic-code land — do anything, return the proper format
return {"content": [{"type": "text", "text": text}]}
options = ClaudeCodeOptions(
system_prompt=load_system_prompt("prompts/echo.md"), # full override
model="claude-haiku",
mcp_servers={"echo": create_sdk_mcp_server(name="echo", tools=[echo])},
allowed_tools=["mcp__echo__echo"],
disallowed_tools=["Read", "Write", "Bash", "WebSearch"], # strip baked-ins
resume=current_session_id, # continuous conversation
)
async with ClaudeSDKClient(options=options) as client: # mount
await client.query("echo 'custom agents are powerful' in reverse, uppercase") # query
async for message in client.receive_response(): # manage
... # parse assistant (text / tool_use / tool_result) & result (session_id, cost) blocksPermission hook (restrict a planner agent to writing only specs/):
python
async def planner_write_hook(input_data, tool_use_id, context):
path = normalize_path(input_data["tool_input"]["file_path"])
if not path.startswith("specs/"):
return {"decision": "block", "reason": "Planner may only write to specs/."}
return {}| SDK / CLI element | Purpose |
|---|---|
ClaudeCodeOptions | Configure the core four (context, model, prompt, tools) |
system_prompt | Full override — builds a new agent |
--append-system-prompt | Extend Claude Code rather than overwrite it |
allowed_tools / disallowed_tools | Whitelist / remove tools from the window |
create_sdk_mcp_server + @tool | Build custom tools in-memory |
mcp_config path | Attach an external MCP server (e.g. Firecrawl) |
ClaudeSDKClient vs query() | Continuous conversation vs one-off prompt |
resume / session_id | Continue a session |
hooks (pre_tool_use / post_tool_use) | Permission checks, logging, governance |
Conventions: keep a consistent per-agent codebase shape — prompts/ for system and user prompts, backend/frontend split for UI agents — so it's easy for you, your team, and your agents to read.
Key takeaways
- The path is better agents → more agents → custom agents — you land at custom only when pushing the edge.
- The system prompt is the most important element of a custom agent, with zero exceptions — overwrite it and it's no longer Claude Code.
- Every agent is options → mount → query → manage;
ClaudeCodeOptionsis your core four minus the user prompt. - Everything ends up in the context window — use
disallowed_toolsto strip what the agent doesn't need. - Hooks give you permission checks — restrict writes, block
.envreads, log usage — essential for safe, team-deployed agents. - Extend before you rebuild: append the system prompt and reuse Claude Code's tools for generic engineering work.
- Custom agents shine on domain-specific problems — e.g. chunk-reading tools that let an agent process a log file many times larger than its window.
- Deploy agents in many form factors: scripts, terminals, data streams, and full UIs.
Notable quotes
"The system prompt is the most important element of your custom agents, with zero exceptions."
"They're built for everyone's codebase, not yours."
"Agentic coding is not so much about what we can do anymore. It's about what we can teach our agents to do."
"It's not the models that are the limitation, the bottleneck anymore. It's you and I."