Agent SDK vs Messages API, Tool Runner, Claude Code, Managed Agents
A team building an internal tool that reviews pull requests, runs the test suite, and posts a summary comment reaches for "Claude" and finds five different starting points: the raw Messages API, a beta tool_runner helper, the Claude Agent SDK, the Claude Code CLI itself, or a hosted product called Managed Agents. They all call the same models. None of the marketing copy makes it obvious which one saves you six weeks of infrastructure work and which one quietly limits what you can customize later. Anthropic's own developer docs actually draw this comparison directly — a table titled "Compare the Agent SDK to other Claude tools" — because the confusion is common enough to earn its own doc section.
This article works through that comparison mechanistically rather than as a feature checklist: for each layer, what does Anthropic's code run for you, what do you still have to write yourself, and who owns the process the agent actually executes in. One example — an internal PR-review agent we'll call DiffBot, working against pull request #482 in a billing-service repository — runs through every layer, including one intentional failure (DiffBot tries to read a secrets file it shouldn't) and how each layer would catch or miss it. By the end you should be able to place a new project on this stack from one question: what am I refusing to build myself?
Map the developer tool surface
Every option here calls the same models and produces the same kind of output: text, or a request to use a tool. What actually differs is two separable questions. First, who runs the agentic loop — the repeated cycle of send a request, read whether the model asked for a tool, run that tool, send the result back, and decide whether to continue. Second, who owns the execution environment the tools run in — your laptop, your server, your CI runner, or a sandbox Anthropic provisions and holds open for you. Every layer in this article answers both questions differently, and that pair of answers is a better decision axis than "which one is more powerful," because more built-in behavior always means less of your own control over exactly how it behaves.
Running example for this article: DiffBot, an internal agent whose job is to review a pull request in billing-service, run the existing test suite, and post one comment summarizing risk. Input: PR #482 adds POST /v1/refunds. Expected success: DiffBot reads the diff, runs pytest, and posts a comment that names the endpoint, the tests that ran, and any concern. Intentional failure: the same commit also touches billing-service/secrets/rotation.yaml — an unrelated file caught in the PR by accident — and while investigating context, DiffBot reasons it needs to read that file too. We'll trace what happens to that read request at each layer, because it's the cleanest way to see what each one actually enforces versus what it merely permits.
| Layer | Who runs the loop | Who owns the sandbox | Best for |
|---|---|---|---|
| Messages API | Your code, one call at a time | Wherever your code runs | One-off calls or a fully custom loop |
| Tool Runner | SDK helper, in your process | Wherever your code runs | Automate the loop, keep full control of tools |
| Agent SDK | SDK harness, in your process | Wherever your process runs | Full agent behavior, embedded in your app |
| Claude Code | Same harness, as a CLI product | Your terminal / CI runner | Interactive dev, one-off tasks, headless scripts |
| Managed Agents | Anthropic's infrastructure | Anthropic-managed or self-hosted sandbox | Long-running or async agents, no infra to host |
Quick reference
- "Agent loop" here means specifically: request → check for tool_use → execute → append result → request again, until the model replies with plain text.
- The five layers are not mutually exclusive in a company: a CI script might use the Messages API for a single classification call while a separate internal tool runs on the Agent SDK.
- Claude Code and the Agent SDK share the same underlying agent loop, tools, and permission system — Claude Code is that harness shipped as a terminal product, not a different implementation.
- For the broader vocabulary these layers sit inside — prompt vs context vs harness as concepts — see Prompt vs Context vs Harness.
Remember this
Rank the five layers by two questions, not one: who executes the loop, and who owns the sandbox the tools run in — power and lock-in both increase together as Anthropic takes over more of each.
The Messages API: you own the loop
The Messages API is direct model access: you send a list of messages and a tool schema, and the model replies with text or a tool_use block naming a tool and its input. That's the entire contract. There is no built-in concept of "keep going until done" — if the model asks for a tool, your code has to notice, execute the tool yourself, format a tool_result block, append it to the message list, and call the API again. If DiffBot needs to read the diff, then run tests, then post a comment, that's three separate tool_use/tool_result round trips your code drives explicitly, one while loop iteration at a time.
This is the right layer when the agentic loop is not actually the hard part of the problem. A serverless function that makes exactly one tool call per invocation, a system with an unusual retry or cost-control policy that a generic loop would fight against, or a codebase that already has its own orchestration (a workflow engine, a state machine) and just needs Claude to fill in one step — all of these want the Messages API directly, because a helper loop would be one more layer to work around rather than a saving. The cost is that context management, error shaping, and stopping conditions are entirely yours: nothing compacts a growing transcript, nothing retries a truncated response, and nothing stops DiffBot from looping forever if you forget a termination check.
For DiffBot's intentional failure — the attempted read of secrets/rotation.yaml — the Messages API enforces nothing. It has no concept of a permission system; read_file is just a tool name in a schema, and if your read_file function opens whatever path it's given, DiffBot reads the secret. Any safety here is a check your own tool-execution code has to write, every single time, for every tool.
Quick reference
- Nothing loops automatically — a single-call app can stop after one response; a multi-step agent needs you to write the
while has tool_useloop yourself. - Context compaction, exponential backoff on rate limits, and truncated-response retries are all your responsibility at this layer.
- Path/permission checks like
is_allowed_pathabove exist only because you wrote them into the tool-execution function — the API has no opinion. - This is the layer every other option in this article is built on top of; reading the [manual tool-use loop docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) is worthwhile even if you never use it directly, since it's what the higher layers are automating.
Remember this
The Messages API gives you exactly the model call and nothing else — every loop iteration, retry, and safety check is code you write and can therefore get wrong.
Tool Runner: the loop without the harness
client.beta.messages.tool_runner() (TypeScript: client.beta.messages.toolRunner()) is a beta SDK helper, available in the Python, TypeScript, C#, Go, Java, PHP, and Ruby SDKs, that automates exactly the loop from the previous section: it runs your tools when Claude calls them, handles the request/response cycle, and manages conversation state, so you stop writing the while loop and the manual tool_result formatting by hand. You still define the tools — a read_file function, a run_tests function — using SDK helpers like Python's @beta_tool decorator or TypeScript's betaZodTool(), and the runner calls them for you when the model asks.
What it does not add is anything about what those tools are allowed to do, where they run, or who approves a risky call before it executes. There's no permission system, no built-in file/bash/web tools, no subagents, and no sandbox — DiffBot's read_file tool in the Tool Runner is exactly the same function you'd write for the raw Messages API, just invoked by the SDK's loop instead of your own. If that function opens secrets/rotation.yaml because you didn't add a path check, the Tool Runner runs it exactly as readily as it runs a legitimate diff read; the runner automates calling your tool, not judging it. The one place it gives you a hook is generate_tool_call_response() (Python) / generateToolResponse() (TypeScript), which lets you inspect a tool's result — including whether it errored — before the runner sends it back to Claude, which is where you'd bolt on an ad hoc approval check if you wanted one.
Quick reference
- Iterating the runner (
for message in runner) gives you every intermediate turn; callinguntil_done()/await runnerskips straight to the final message when you don't need the play-by-play. - A thrown exception inside your tool becomes a
tool_resultwithis_error: trueautomatically — Claude sees the failure and can retry or change approach without your loop crashing. - Client-side compaction in the Python/TypeScript/Ruby runners is deprecated in favor of server-side [context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing), available across every SDK regardless of the runner.
- For patterns on making the tools themselves resilient to malformed or repeated calls — a concern the runner doesn't solve — see Tool Calling Reliability Patterns.
Remember this
Tool Runner removes the loop bookkeeping, not the trust boundary — a tool you write for it is exactly as safe or unsafe as the same function called by hand, because the runner never sees your tool's internals.
Claude Agent SDK: the harness in your process
The Claude Agent SDK (@anthropic-ai/claude-agent-sdk for TypeScript, claude-agent-sdk for Python) is a different kind of layer than the previous two: instead of giving you a loop to run your own tools through, it gives you the actual agent loop, built-in tools, and permission system that power Claude Code, as a library you embed in your own process. Both SDKs bundle a native Claude Code binary and drive it over a local protocol, so Read, Write, Edit, Bash, Glob, Grep, WebSearch, and WebFetch all exist out of the box — DiffBot doesn't need a hand-written read_file function; it gets a real filesystem tool with the same semantics Claude Code itself uses.
The part that matters for DiffBot's intentional failure is the permission system, and specifically its evaluation order: a request to use a tool is checked against hooks first, then deny rules, then ask rules, then the active permission mode, then allow rules, and only then falls through to your own canUseTool callback. Deny rules win regardless of mode — a disallowed_tools=["Read(secrets/**)"] rule blocks DiffBot's read of secrets/rotation.yaml even if the session is running in acceptEdits mode, and even under bypassPermissions, which otherwise auto-approves everything else. That ordering is a real design decision, not an implementation detail: it means the one rule you write to keep an agent out of a directory holds even when you've deliberately loosened everything else for speed, which is exactly the property you want from a permission system and exactly the property the Tool Runner and Messages API don't give you for free.
Subagents, added through AgentDefinitions, and MCP client support round out what the SDK adds versus Tool Runner: a bounded, separately-scoped sub-task (see Subagents Explained) and a standard way to plug in external tool servers (see MCP vs A2A vs ACP) both come for free. What you still own is everything about deployment: the SDK runs inside your process, on your infrastructure, and only ships for Python and TypeScript — driving it from another language means running the CLI as a subprocess with -p --output-format json rather than calling a native library.
Quick reference
- Permission modes span a real range:
defaultrequires yourcanUseToolcallback for anything unmatched,plannever auto-approves edits,acceptEditsauto-approves file writes, andbypassPermissionsapproves almost everything except explicit deny/ask rules and hooks. - A bare deny entry like
disallowed_tools=["Bash"]removes the tool from the model's context entirely; a scoped one likeBash(rm *)keepsBashavailable and blocks only matching calls — the two behave differently, not just at different granularity. - Subagents inherit the parent's permission mode by default; a parent running
bypassPermissionsgrants that same unrestricted mode to every subagent it spawns unless the subagent's own definition overrides it. - Sessions, skills, commands, and memory load from
.claude/and~/.claude/exactly as they do in Claude Code — an SDK agent and a Claude Code session can share the same project configuration.
Remember this
The Agent SDK's permission system checks deny rules before it checks the permission mode — a single deny rule holds even when you've turned everything else to auto-approve for speed.
Claude Code: the SDK as a finished product
Claude Code is not a separate implementation of the agent loop — it's the same harness the Agent SDK exposes as a library, packaged as a terminal product with its own UX on top: interactive sessions you can resume, /compact and other slash commands, and a config-loading convention (CLAUDE.md, .claude/settings.json) the SDK reuses. Anthropic's own comparison puts it plainly: reach for Claude Code when you're doing interactive development or running one-off tasks from a terminal, and reach for the Agent SDK when you're building an agent without implementing the tool loop yourself, as a library inside your own application. The internals of that loop, its context compaction, and its permission modes are covered in depth in How Claude Code Works; the session lifecycle — resuming, branching, and worktrees — is covered in Managing Sessions in Claude Code.
For DiffBot specifically, this is often the layer people skip past without noticing it's an option: if the review doesn't need to be embedded in another application — no custom UI, no webhook trigger from your own service — you don't need SDK code at all. A CI step can run Claude Code headless (claude -p "Review PR #482 in billing-service: read the diff, run pytest, post a summary" --output-format json) and parse the JSON result, getting the exact same permission system, built-in tools, and deny-rule enforcement as the SDK example above, with zero lines of Python or TypeScript to maintain. The trade is UX and branding, not capability: Claude Code's terminal interface and its name are fixed, whereas a product built on the Agent SDK is expected to carry its own branding — Anthropic's SDK terms explicitly prohibit calling a third-party product "Claude Code," reserving that name for the terminal product itself.
The decision between the two, in practice, rarely comes down to the agent loop — it's identical either way. It comes down to whether a human is meant to sit in a terminal (Claude Code) or whether the review needs to be triggered by your own code and its result consumed programmatically inside your own product (Agent SDK).
Quick reference
- Headless mode (
-p+--output-format json) is also the documented way to drive the same agent loop from a language the SDK doesn't ship for — you shell out to the CLI as a subprocess instead of calling a library. - Config loading is shared: CLAUDE.md,
.claude/settings.jsonpermission rules, skills, and slash commands work the same whether the session started fromclaudein a terminal orquery()in an SDK script. - Branding is a real constraint, not just style: SDK-based products may say "Claude Agent" or "{YourAgentName} Powered by Claude," but not "Claude Code" or Claude Code's visual identity.
- If DiffBot's only trigger is "a person types a command," building it on the Agent SDK instead of just running Claude Code interactively is added engineering effort with no corresponding capability gain.
Remember this
Claude Code and the Agent SDK run the identical loop — the choice between them is who or what triggers the session, a human in a terminal versus your own application code, not what the agent can do.
Managed Agents: when Anthropic also hosts the sandbox
Claude Managed Agents (beta, managed-agents-2026-04-01) is a separate hosted REST API, not a mode of the Agent SDK — Anthropic's docs list it alongside the Messages API as one of "two ways to build with Claude," precisely because it removes the thing every earlier layer still required: a process of yours that stays running. It's built around four resources: an Agent (model, system prompt, tools, MCP servers, skills), an Environment (a cloud sandbox Anthropic provisions, or a self-hosted sandbox on your own infrastructure for data-residency or compliance needs), a Session (one running task inside that environment), and Events (the messages exchanged with it, delivered over server-sent events). You create each resource with a REST call, then stream a session's events instead of holding a socket or a worker process open yourself.
For DiffBot, this layer earns its keep specifically for long or asynchronous work: a review that has to wait on a slow CI run, gets steered mid-execution ("also check the migration file"), or runs on a schedule rather than on demand. Anthropic provisions the sandbox, runs bash and file tools inside it, persists the filesystem and conversation history across the session, and streams agent.message / agent.tool_use / session.status_idle events back to you — none of it is your infrastructure to keep alive. Because sessions are stateful and long-running by design, Managed Agents is a genuine trade, not a strict upgrade: it's currently outside Zero Data Retention and HIPAA BAA eligibility, which matters if DiffBot ever touches anything more sensitive than a public repo's diff, and self-hosting the sandbox is the documented way to keep execution inside your own network boundary while still using the managed session/event layer.
Quick reference
- The four resources map cleanly onto the earlier layers: Agent ≈ your system prompt and tool config, Environment ≈ the sandbox the Agent SDK would otherwise run in your own process, Session ≈ one query() call, Events ≈ the streamed messages you'd iterate in the SDK.
- Self-hosted sandboxes let you keep tool execution inside your own network for data that can't leave it, while still using Anthropic's session and event infrastructure — a middle point between fully managed and fully self-run.
- Scheduled deployments run a Managed Agent on a cron schedule, which is the closest match for "review every PR opened overnight" without a worker process of yours polling for new PRs.
- Because it's stateful server-side by design (persisted history, sandbox state, outputs), Managed Agents is not currently eligible for Zero Data Retention or a HIPAA BAA — check current data-retention scope before routing anything regulated through it.
Remember this
Managed Agents trades a process of yours (server, worker, sandbox) for a REST session you stream — the honest cost of that trade today is reduced data-retention eligibility, not a capability gap.
Key takeaway
Every layer in this article runs the same kind of model call underneath; what actually separates them is how much of the loop, the tool implementations, the permission decisions, and the hosting Anthropic writes for you versus what you write yourself — and each step up that ladder trades some of your control for less code to maintain. Start from the Messages API only when the loop genuinely isn't the hard part. Reach for Tool Runner when you want the loop automated but every tool is bespoke to your system. Reach for the Agent SDK when you need built-in tools, a real permission system, and subagents inside your own application. Use Claude Code as-is when a human or a CI step triggering it interactively is enough. Reach for Managed Agents when the task outlives a request/response cycle and you'd rather not host the sandbox yourself.
Practice (25 min): Build DiffBot at two layers and compare what each one enforces. First, with the Agent SDK: write a ClaudeAgentOptions with allowed_tools=["Read", "Bash", "Glob"], disallowed_tools=["Read(secrets/**)"], and permission_mode="acceptEdits", then ask it to review a small local repo with a secrets/ folder present. Expected result: DiffBot reads source files and runs tests freely, and a transcript entry shows a denied Read(secrets/...) call. Now intentionally break it by removing the disallowed_tools entry and rerunning the same prompt — confirm the read now succeeds, proving the deny rule (not the model's judgment) was what blocked it. Recover by re-adding the rule and adding a PreToolUse hook that also blocks the pattern, so the same protection holds even under bypassPermissions. Pass when you can point to the specific line — the deny rule or the hook — that would have to change for DiffBot to read the secret, rather than trusting the model to decline on its own.
Related Articles
Explore this topic