Rubric
Contents — domains, guide and mocks

Building agents with the Agent SDK

CCDV-F 1.211 min read · checked 21 September 2026

Task statementAgent Construction with Claude (5.3%) — the Claude Agent SDK, custom agent loops and harnesses, self-hosted versus Anthropic-hosted deployment, and hooks for deterministic actions

Four ways to run an agent

Who should run the agent loop?
  • You, in full detail
    Client SDKMessages API, your own loop
  • A library you embed
    Agent SDKPython or TypeScript, your process
  • A person at a terminal
    Claude Code CLIinteractive or headless -p
  • Anthropic, hosted
    Managed Agentshosted harness and sandbox
The question behind all four is who runs the loop and whose machine the tools execute on. Everything else follows from that.

What the Agent SDK actually is

The Claude Agent SDK is Claude Code as a library. It gives you the same tools, agent loop and context management that power Claude Code, programmable in Python and TypeScript, running in a process you operate. Both SDKs bundle a native Claude Code binary, so the library you install is also the harness that runs.

The capabilities it brings are the ones you would otherwise have to write: built-in tools (Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch and the orchestration tools), a permission system, hooks, subagents, MCP connections, sessions you can resume or fork, and automatic compaction when the context window fills. Skills, slash commands and memory files load from .claude/ directories exactly as they do in Claude Code.

A complete agent, Pythonpython
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

async def main():
    async for message in query(
        prompt="Find and fix the failing tests in the auth module",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Edit", "Bash", "Grep"],  # auto-approved
            setting_sources=["project"],   # load CLAUDE.md, skills, hooks
            max_turns=30,                  # backstop against a runaway loop
            max_budget_usd=5.0,            # spend cap, subagents included
        ),
    ):
        if isinstance(message, ResultMessage):
            # Only "success" carries the final text in .result
            print(message.subtype, message.session_id, message.total_cost_usd)

asyncio.run(main())

Read what that loop hands back. The SDK streams messages — a SystemMessage with subtype init at session start, an AssistantMessage per content block, a UserMessage carrying each tool result — and ends with a ResultMessage. Its subtype is the termination state: success, error_max_turns, error_max_budget_usd, error_during_execution. Only success carries the result text, so checking the subtype before reading the answer is not defensive style, it is correctness.

Writing your own harness

A harness is the code around the model: it sends the request, executes tools, appends results and decides when to stop. Writing your own against the Messages API is entirely reasonable — it is how you get a loop in a language the Agent SDK does not cover, or one shaped around your own state machine. The Client SDKs also offer a beta tool runner that drives the loop for you if you would rather not.

The same loop, by handpython
messages = [{"role": "user", "content": task}]

while True:
    resp = client.messages.create(
        model=MODEL, max_tokens=4096, tools=TOOLS, messages=messages,
    )
    messages.append({"role": "assistant", "content": resp.content})

    if resp.stop_reason != "tool_use":
        break                       # end_turn, max_tokens, refusal — handle each

    results = [
        {"type": "tool_result", "tool_use_id": b.id, "content": run_tool(b)}
        for b in resp.content if b.type == "tool_use"
    ]
    messages.append({"role": "user", "content": results})

That is eighteen lines and it works. What it does not yet have is everything the Agent SDK ships with: permission checks before a tool runs, compaction when the window fills, session persistence and resumption, subagents, parallel execution of read-only tools, cost accounting. Building a harness means choosing to own that list.

What you own in each option

Your own harness

  • You read stop_reason and match every tool_use_id
  • You write permissioning, retries and trimming
  • You persist sessions and handle resumption
  • Full control of every byte sent to the API

Agent SDK

  • The loop ends when Claude replies with no tool calls
  • Permissions, hooks and compaction are built in
  • session_id resumes or forks a conversation
  • You still run the process and hold the API key
Nothing here is free; the work moves rather than disappearing. Pick the column whose leftovers you are happy to maintain.

Self-hosted versus Anthropic-hosted

“Self-hosted” here means the agent loop and its tools run on infrastructure you operate — your container, your laptop, your VPC — whichever library you used. The CLI, the Agent SDK and a hand-written harness are all self-hosted in that sense. Anthropic-hosted means Managed Agents: a pre-built, configurable harness that runs in Anthropic's infrastructure, where you define an agent (model, system prompt, tools, MCP servers, skills), define an environment, start a stateful session, and stream events over server-sent events.

Client SDK loopAgent SDKManaged Agents
Who runs the loopYouThe SDK, in your processAnthropic
Where tools executeYour codeYour machine or containerA cloud sandbox, or a self-hosted sandbox you run
State between turnsWhatever you storeSessions you can resume or forkStateful sessions with a persistent filesystem
Best forCustom loops, fine-grained controlEmbedding a coding-capable agent in your appLong-running and asynchronous work with little infrastructure

Hooks: the deterministic part of an agent

A hook is a callback that fires at a fixed point in the loop and whose return value the harness obeys. PreToolUse fires before a tool executes and can allow, deny, ask or rewrite the call. PostToolUse fires after and can rewrite the output or append context. UserPromptSubmit can inject context into a prompt. Stop, SubagentStart, SubagentStop, PreCompact and others cover the rest of the lifecycle. Hooks run in your application's process, not in the model's context window, so they cost no tokens.

Deny a tool call outrightpython
async def protect_env_files(input_data, tool_use_id, context):
    path = input_data["tool_input"].get("file_path", "")
    if path.split("/")[-1] == ".env":
        return {"hookSpecificOutput": {
            "hookEventName": input_data["hook_event_name"],
            "permissionDecision": "deny",
            "permissionDecisionReason": "Cannot modify .env files",
        }}
    return {}   # empty object == allow unchanged

options = ClaudeAgentOptions(hooks={
    "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
})

Note the matcher. "Write|Edit" means the callback only fires for those tools; a hook with no matcher fires for every event of its type. When the hook denies, Claude receives the rejection as the tool result and typically tries another approach or explains that it cannot proceed — the model is told, not silently ignored.

A hook intercepting a tool call

Claude
Harness
PreToolUse hook
Tool
Step 1: Claude to Harness: Write to .env
Step 2: Harness to `PreToolUse` hook: tool name + input
Step 3: `PreToolUse` hook to Harness: deny + reason
Step 4: Harness to Claude: rejection as tool result
Step 5: Claude to Harness: Different approach
Step 6: Harness to Tool: Allowed call runs
The tool never runs. Compare this with a system prompt instruction, which the model may follow and may not — and which you cannot prove to an auditor.

Precedence when decisions disagree

strongest decision at the top

  1. denyblocks, whatever else said
  2. deferends the query to decide later
  3. askroutes to a human decision
  4. allowruns the call
When several hooks or permission rules apply to one call, the strictest wins. One deny anywhere blocks the operation.

Traps the wrong answers are built from

Tempting but wrongDo this instead
Enforcing a safety rule in the system promptEnforce it in a PreToolUse hook that returns deny; keep the prompt for steering.
Reading ResultMessage.result without checking subtypeBranch on the subtype first — only success carries the final text.
Writing a bespoke harness to get features the SDK already hasWrite your own loop for control you actually need, not for permissions, compaction and sessions.
Choosing hosted or self-hosted on developer convenienceDecide on where the data and the execution are allowed to live, then pick the option that fits.
Doing slow work inside a blocking hookReturn an async output for pure side effects such as logging, so the loop continues.

You should now be able to

  • Describe what the Claude Agent SDK provides over a hand-written Messages API loop.
  • Write a minimal agent with query() and read the ResultMessage correctly.
  • Choose between a custom harness, the Agent SDK and Anthropic-hosted Managed Agents for a given constraint.
  • Register a PreToolUse hook that denies a tool call, and explain why that beats a prompt instruction.
  • Name the main hook events and the order in which conflicting decisions are resolved.

Practice questions

Original questions written for this lesson, in the exam’s style. Answer first, then open the reasoning — every option is explained, including why the wrong ones are tempting.

  1. Question 1

    An agent at a logistics firm occasionally runs rm -rf inside a shared working directory. The team's proposed fix is a stronger system prompt plus an example of what not to do.

    What should they do instead?

    1. AAdd a PreToolUse hook on Bash that inspects the command and returns deny.
    2. BMove the instruction from the system prompt to the first user message for more weight.
    3. CAdd a PostToolUse hook that logs destructive commands for review.
    4. DLower the effort level so the agent takes fewer risky actions.
    Show answer and reasoning
    1. ACorrect. A hook runs in your process before the tool executes, so the destructive command never reaches the shell regardless of what the model decided.
    2. BIncorrect. Placement changes emphasis, not enforceability; the model can still emit the call.
    3. CIncorrect. Logging is valuable for audit but happens after the files are gone.
    4. DIncorrect. Effort controls reasoning depth and cost, not which commands are permitted.
  2. Question 2

    A healthcare startup wants an agent that works through long document-processing jobs asynchronously. Its compliance team requires that patient files never leave infrastructure the company controls.

    Which deployment approach fits the constraint?

    1. AManaged Agents with the default Anthropic-managed cloud sandbox.
    2. BThe Claude Code CLI run interactively by an operator each night.
    3. CThe Agent SDK in the company's own container, or Managed Agents with a self-hosted sandbox.
    4. DA hand-written Messages API loop, because only custom code can be compliant.
    Show answer and reasoning
    1. AIncorrect. The default sandbox runs on Anthropic infrastructure, which is exactly what the constraint rules out.
    2. BIncorrect. It would keep execution local but is an interactive terminal tool, not an unattended service for long asynchronous jobs.
    3. CCorrect. Both keep tool execution and files on infrastructure the company operates; the choice between them is about how much harness they want to run.
    4. DIncorrect. Compliance follows from where execution and storage happen, not from whether the loop was hand-written.
  3. Question 3

    A developer's Agent SDK job prints an empty summary to users roughly once a day. The logs show the run ended with subtype of error_max_turns.

    What is the correct interpretation and response?

    1. AThe model refused the task; the prompt needs rewriting to avoid the refusal.
    2. BThe API returned an error; the request should be retried with backoff.
    3. CThe context window overflowed; compaction should be disabled.
    4. DThe loop hit its turn cap unfinished; resume the session or report an incomplete run.
    Show answer and reasoning
    1. AIncorrect. A refusal appears as a stop_reason of refusal, not as the error_max_turns result subtype.
    2. BIncorrect. This is the loop's own termination state, not a transport or API failure.
    3. CIncorrect. Compaction is what prevents overflow; disabling it would make long runs worse, and overflow is not what this subtype reports.
    4. DCorrect. error_max_turns carries no result text, but it does carry session_id, so the run can be resumed rather than shown as an empty answer.

Sources

Drafted with AI assistance and checked against the sources above; expert review is in progress. Spotted an error? Tell us and it gets fixed, dated and listed on how this is written.