What you will be able to do
- Implement the stop_reason-driven loop that dispatches client tool calls
- Format tool_result blocks correctly, including multi-part content
- Signal execution errors with is_error and tell them apart from invalid tool calls
- Handle server tool responses, pause_turn, and turns that mix server and client tools
- Gate tool execution with canUseTool, allow rules, and deny rules in the Agent SDK
1.The harness loop: dispatching client tool calls
Claude can't run your code, so every client tool call is a round trip. Claude asks, your harness runs the tool, and you report back. The standard harness is a while loop keyed on stop_reason. The loop exits on any other stop reason (end_turn, max_tokens, stop_sequence, refusal), and your application should handle each of those.
Checkpoint 1 of 6· Put it in order
Put the steps of the client-tool agentic loop in order
- 1.Send a new request with the original messages, the assistant's response, and a user message holding the tool_result blocks
- 2.Send a request with your tools array and the user message
- 3.Repeat from step 2 while stop_reason is tool_use
- 4.Claude responds with stop_reason tool_use and one or more tool_use blocks
- 5.Execute each tool and format the outputs as tool_result blocks
The docs give this exact sequence. The assistant's response has to go back into the history before the user message that carries the results.
“Send a new request containing the original messages, the assistant's response, and a user message with the tool_result blocks.”Source: platform.claude.com
For each tool_use block, dispatch on name, pass in input, and keep the id. Blocks from the computer use or browser use toolsets also carry a toolset_name, so dispatch those on both fields. If you'd rather not write the loop yourself, the SDK's Tool Runner runs your tools and sends the results back automatically.
tool_use = next(block for block in response.content if block.type == "tool_use")
print(f"Claude called {tool_use.name} with {json.dumps(tool_use.input)}")
# Run the tool, then send the result back in a tool_result block.
weather = "15 degrees Celsius, partly cloudy" # your weather lookup goes here
messages += [
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": tool_use.id, "content": weather}
],
},
]2.Formatting tool_result blocks
A tool result goes in a user message as a tool_result block. tool_use_id is required and matches the id of the request it answers. content is optional. It can be a plain string, or a list of text, image, document, or search_result blocks, so a tool can return a caption and a screenshot together. You may leave content out entirely. Results for toolset members are stricter: they must echo the same toolset_name, and the API rejects a member result without it.
Checkpoint 2 of 6· Check yourself
Claude made two tool calls in one turn. How does the API know which tool_result answers which call?
Every tool_use block has a unique id, and the tool_result refers back to it with tool_use_id.
“A unique identifier for this particular tool use block. This will be used to match up the tool results later.”Source: platform.claude.com
Sources2
3.Error handling: execution failures versus invalid calls
There are two kinds of failure. In an execution error, the tool itself fails, for example the weather API returns HTTP 500. Return the error message as the content with is_error: true, and Claude will work the failure into its reply to the user. In an invalid call, Claude's request is malformed, for example a required parameter is missing. That usually means the definition didn't give Claude enough to go on. During development, the fix is a more detailed description. At runtime, you can return an is_error result that names the problem, and Claude will retry 2–3 times with corrections before apologizing. Strict tool use stops schema violations before they happen.
Checkpoint 3 of 6· Fill the gap
Which field tells Claude that this tool execution failed?
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "ConnectionError: the weather service API is not available (HTTP 500)",
" ? ": true
}
]
}is_error: true on a tool_result marks the content as an error message instead of a successful result.
Source: platform.claude.comSources2
4.Server tools at runtime: pause_turn and mixed turns
Server tools run their own loop inside Anthropic's infrastructure. In the response, they appear as server_tool_use blocks whose ids start with srvtoolu_, each followed by its result block. You never send a tool_result for them, and server tool errors are handled transparently, so there's no is_error to manage. Two exceptions hand control back to you. First, the server loop has an iteration cap. When Claude hits it, you get pause_turn: re-send the paused response as-is with the same tools, and repeat with a cap on continuations. Second, Claude may call a server tool and a client tool in the same parallel group. Then you get tool_use with a server_tool_use block that has no matching result. Send only the client tool_result blocks and keep the same tools array. The API then runs the deferred server tool.
Checkpoint 4 of 6· Match them up
Match each response to how your harness should continue
Tap a term, then the definition that fits it.
pause_turn means the server loop stopped early. tool_use means Claude is waiting on your client tools. Server calls that already have results need nothing from you.
“Detect the state by looking for a server_tool_use block whose id has no matching result block in the response.”Source: platform.claude.com
5.Approval patterns: deciding which calls may run
Claude only ever sends a request, and your code runs every client tool. That makes your harness the natural place to approve or block calls. The Agent SDK builds this in with permission modes, hooks, and declarative allow/deny rules. Hooks run custom code that can allow, deny, or modify a tool request. Allow rules such as allowed_tools=["Read", "Grep"] auto-approve the tools they list. Unlisted tools still exist, and their calls fall through to the permission mode and the canUseTool callback. A disallowed_tools entry is a different thing: it removes the tool definition from the request entirely. When no earlier step settles a call, canUseTool asks the user at runtime.
// Return allow or deny based on user's response
if (response.toLowerCase() === "y") {
// Allow: tool executes with the original (or modified) input
return { behavior: "allow", updatedInput: input };
} else {
// Deny: tool doesn't execute, Claude sees the message
return { behavior: "deny", message: "User denied this action" };
}Checkpoint 5 of 6· Exam question
A security review requires that a specific destructive shell pattern, `rm -rf *`, always be blocked regardless of any other configuration, including future changes to permission mode. Which rule configuration satisfies this requirement, based on how the SDK's permission evaluation order treats deny rules?
Correct answer: A — Add `"Bash(rm -rf *)"` to `disallowed_tools`, since scoped deny rules are checked before the permission-mode step and block matching calls even under `bypassPermissions`
- A. Correct. Deny rules are evaluated early in the permission flow, before the permission-mode step, and a scoped rule like `Bash(rm -rf *)` blocks matching calls in every mode, including `bypassPermissions`, while leaving other `Bash` calls available and subject to normal evaluation.
- B. A bare tool name like `"Bash"` in `disallowed_tools` removes the entire `Bash` tool from Claude's context, blocking all Bash commands, not just the dangerous pattern; this is broader than the stated requirement to block only the destructive pattern.
- C. `allowed_tools` entries approve calls; there is no mechanism to mark an allow-rule entry as forbidden, and allow rules cannot be used to deny a call regardless of comments or intent.
- D. `plan` mode blocks write/edit approvals only for the current session's configuration and is not a permanent, cross-session denial mechanism; it is a mode setting, not a persistent deny rule.
Checkpoint 6 of 6· Check yourself
You set disallowed_tools=["Bash"]. What does Claude experience?
A disallowed tool is removed from the request, so Claude can't attempt it at all. A denial from canUseTool, by contrast, happens after Claude has already made the call.
“The Bash tool definition is removed from the request. Claude does not see the tool and cannot attempt it.”Source: code.claude.com
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Server tools need a tool_result from you, and is_error handling just like client tools.Why is that wrong?
The API runs server tools itself and puts their result blocks in the same response. Server tool errors are handled transparently.
Covered in Server tools at runtime: pause_turn and mixed turns
2.When a server tool hasn't run yet, the response is always pause_turn, so you re-send it as-is.Why is that wrong?
If Claude also called a client tool, the API returns tool_use without running the server tool, and you continue by sending the client tool_result blocks.
Covered in Server tools at runtime: pause_turn and mixed turns
3.Leaving a tool out of allowed_tools blocks Claude from using it.Why is that wrong?
allowed_tools only auto-approves. Unlisted tools still exist and fall through to the permission mode and canUseTool. To remove a tool, use disallowed_tools.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Client-executed tools (both user-defined and Anthropic-schema) require your application to drive a loop.”
↩︎ The harness loop: dispatching client tool calls“A paused turn means the work isn't finished”
↩︎ Server tools at runtime: pause_turn and mixed turns“The model never executes anything on its own.”
↩︎ Approval patterns: deciding which calls may run“Send a new request containing the original messages, the assistant's response, and a user message with the tool_result blocks.”
↩︎ Checkpoint - 2.
“dispatch those blocks on both fields.”
↩︎ The harness loop: dispatching client tool calls“These content blocks can use the text, image, document, or search_result types.”
↩︎ Formatting tool_result blocks“a member result that omits it is rejected”
↩︎ Formatting tool_result blocks“Claude will retry 2-3 times with corrections before apologizing to the user.”
↩︎ Error handling: execution failures versus invalid calls“Your best bet during development is to try the request again with more-detailed description values in your tool definitions.”
↩︎ Error handling: execution failures versus invalid calls“Unlike client tools, you do not need to handle is_error results for server tools.”
↩︎ Exam trap 1“A unique identifier for this particular tool use block. This will be used to match up the tool results later.”
↩︎ Checkpoint - 3.
“To skip writing this round trip yourself, use Tool Runner: the SDK executes your tools and sends the results back automatically.”
↩︎ The harness loop: dispatching client tool calls - 4.
“Its id field uses the srvtoolu_ prefix to distinguish it from client tool calls”
↩︎ Server tools at runtime: pause_turn and mixed turns“capping the number of continuations as you would any retry loop.”
↩︎ Server tools at runtime: pause_turn and mixed turns“When that happens, the API does not run the server tool. It returns immediately so that you can run the client tool first”
↩︎ Exam trap 2“A response that leaves a client tool_use block waiting on you never has a stop_reason of pause_turn”
↩︎ Prediction“Detect the state by looking for a server_tool_use block whose id has no matching result block in the response.”
↩︎ Checkpoint - 5.
“Control how your agent uses tools with permission modes, hooks, and declarative allow/deny rules.”
↩︎ Approval patterns: deciding which calls may run“canUseTool callback: prompt users for approval at runtime, when no earlier step resolves the call.”
↩︎ Approval patterns: deciding which calls may run“Other tools not listed here still exist, and calls to them that need approval fall through to the permission mode and canUseTool.”
↩︎ Exam trap 3“The Bash tool definition is removed from the request. Claude does not see the tool and cannot attempt it.”
↩︎ Checkpoint