What you will be able to do
- Classify a tool as user-defined, Anthropic-schema or server-executed, and say what your application owes each one
- Build the stop_reason-driven loop that dispatches tool_use blocks and returns matching tool_result blocks
- Handle pause_turn and turns that mix server and client tools
- Report tool failures with is_error, and add an approval step before tools run in the Agent SDK
1.Client-side vs. server-side tools
Where a tool's code runs decides how much work your application does. Every tool falls into one of three groups:
| Group | Examples | Schema written by | Executed by | You send tool_result? |
|---|---|---|---|---|
| User-defined client tool | your own, e.g. get_weather | You | Your application | Yes |
| Anthropic-schema client tool | memory, bash, text_editor, computer, browser | Anthropic | Your application | Yes |
| Server-executed tool | web_search, web_fetch, code_execution, tool_search | Anthropic | Anthropic | No |
Most tool-use traffic is user-defined tools that call application-specific logic. Anthropic-schema tools run the same way: you get a tool_use block, run the operation and send back a tool_result. So why not write your own equivalent? Because these schemas are trained-in. Claude was optimized on thousands of successful trajectories that use them, so it calls them more reliably and recovers from errors better than it would with a custom tool that does the same thing. Server tools differ in kind: you enable them in the request, and Anthropic's servers do everything else.
Sources1
2.Dispatching tool calls in the agentic loop
Client-executed tools require your application to drive a loop, because Claude can't run your code. Every call is a round trip: the model asks, you execute, you report back. When Claude wants a client tool, the response has stop_reason "tool_use" and one or more tool_use blocks. Each block carries three fields:
- id, which you use to match up the result
- name, the tool being called
- input, an object that conforms to the tool's input_schema
{
"id": "msg_01Aq9w938a90dw8q",
"model": "claude-opus-5-5",
"stop_reason": "tool_use",
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll check the current weather in San Francisco for you."
},
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "San Francisco, CA", "unit": "celsius" }
}
]
}Your harness dispatches on the tool name. It takes the name, id and input from the block, runs the matching function in your codebase, and replies with a user message containing a tool_result block. That block's tool_use_id must equal the block's id. content is optional. It can be a string, or a list of text, image, document or search_result blocks. Computer use and browser use member blocks also carry a toolset_name, so dispatch those on both fields and echo toolset_name in the result.
The standard loop runs on stop_reason:
1. Send the request with your tools. 2. On "tool_use", run each tool. 3. Append the assistant response and a user message of tool_result blocks. 4. Repeat.
Any other stop reason ("end_turn", "max_tokens", "stop_sequence" or "refusal") ends the loop, and your application handles it.
3.When server tools hand control back
Server tools run their own loop inside Anthropic's infrastructure. One request can trigger several searches before a response comes back. The response contains a server_tool_use block (id prefix srvtoolu_) followed by its result block, and you never build a tool_result for it. Two cases bring your application back into the loop.
pause_turn. The server loop has an iteration limit. A paused turn means the work isn't finished. Re-send the conversation with the paused response as-is, and include the same tools. If a tool is missing, the API returns a validation error. A continued turn can pause again, so cap the number of continuations as you would in any retry loop.
Mixed turns. Claude may call a server tool and a client tool in the same parallel group. In that case the API does not run the server tool and returns immediately. stop_reason is "tool_use", and the server_tool_use block has no result block yet. There is no other marker, so look for a server_tool_use id with no matching result.
No. Re-sending as-is is the pause_turn pattern. This is a mixed turn. Run run_command, then send a user message whose content is only its tool_result, and keep the same tools array. If the resume request no longer defines web_fetch, it fails with a 400. The API then runs the deferred web_fetch, and its web_fetch_tool_result is the first block of the next response.
4.Error handling
The documentation describes three kinds of error. Each is handled differently.
The tool fails during execution, for example with a network error. Return the error message as the tool_result content and set is_error to true. Claude includes the error in its reply, for example by telling the user the weather service is unavailable.
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "ConnectionError: the weather service API is not available (HTTP 500)",
"is_error": true
}
]
}Claude's call is invalid, for example a required parameter is missing. This usually means Claude didn't have enough information to use the tool correctly. During development, the fix is more detailed descriptions in your tool definitions. At runtime, you can return an is_error tool_result that names the problem, such as "Error: Missing required 'location' parameter". Claude retries 2-3 times with corrections before apologizing to the user.
A server tool fails. Claude handles these errors transparently and tries to give an alternative response. You don't handle is_error results for server tools. Web search, for example, can report codes such as too_many_requests, max_uses_exceeded and query_too_long.
Sources2
5.Approval patterns before a tool runs
Some calls shouldn't run just because Claude asked. In the Claude Agent SDK, you register a can_use_tool callback (canUseTool in TypeScript). It fires when Claude wants a tool that no permission rule or permission mode has auto-approved. The callback receives three things:
- the tool name, such as Bash, Write or Edit
- the input Claude is passing; for Bash this includes command, and for Write it includes file_path and content
- context, including optional suggestions
It returns allow or deny.
# Get user approval
response = input("Allow this action? (y/n): ")
# Return allow or deny based on user's response
if response.lower() == "y":
# Allow: tool executes with the original (or modified) input
return PermissionResultAllow(updated_input=input_data)
else:
# Deny: tool doesn't execute, Claude sees the message
return PermissionResultDeny(message="User denied this action")Allow runs the tool, either with the original input or with a modified one passed through updated_input. Deny stops the tool from executing and shows your message to Claude, so it can change course instead of failing blindly. The callback sees the full input before anything runs, so this is where you check the actual command or file path. The same callback also handles AskUserQuestion when Claude asks a clarifying question; if you specify a tools array, it must include AskUserQuestion. The Python example also registers a dummy PreToolUse hook, which the documentation labels a required workaround to keep the stream open for can_use_tool.
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.
Sources4
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.After Claude runs web_search, your application must send a tool_result block back, just as it does for a client tool.Why is that wrong?
Anthropic runs server-executed tools and pairs their result blocks automatically. You enable the tool and read the answer.
Covered in Client-side vs. server-side tools
2.A server_tool_use block with no result means the turn paused, so re-send the assistant content as-is.Why is that wrong?
If a client tool was called in the same group, the turn is a mixed turn. You continue it by sending the client tool_result blocks, not by re-sending the response.
Covered in When server tools hand control back
3.When your tool throws during execution, the harness should abort the loop and show the exception to the user.Why is that wrong?
Return the failure to Claude as a tool_result with is_error set, and Claude includes it in its reply.
Covered in Error handling
Practise it for real
Run one full client-tool round trip with get_weather, including a failed execution.
1.Call messages.create with the get_weather definition in tools and the user message "What's the weather like in San Francisco?".
Why: Client tools are declared in the tools top-level parameter.
You should see: stop_reason is "tool_use" and there is a tool_use block named get_weather whose input includes location.
2.Append the assistant response, then a user message with a tool_result whose tool_use_id equals that block's id and whose content is "15 degrees".
Why: tool_use_id is how the result is matched to the request.
You should see: Claude uses the result to finish answering the original question.
3.Repeat, but answer with content "ConnectionError: the weather service API is not available (HTTP 500)" and "is_error": true.
Why: Execution failures go back to Claude instead of breaking the loop.
You should see: Claude tells the user it could not retrieve the weather.
4.Wrap the request and result steps in a loop that keeps going while stop_reason == "tool_use".
Why: This is the standard shape of the client-side agentic loop.
You should see: The loop ends on "end_turn" or another non-tool_use stop reason.
Stuck? Get a nudge
If Claude never calls the tool, check that the user message actually needs current weather. Tools don't fit questions the model can answer from training.
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“The primary axis along which tools differ is where the code executes.”
↩︎ Client-side vs. server-side tools“these schemas are trained-in”
↩︎ Client-side vs. server-side tools“The canonical shape is a while loop keyed on stop_reason”
↩︎ Dispatching tool calls in the agentic loop“A paused turn means the work isn't finished”
↩︎ When server tools hand control back“You never construct a tool_result block for these tools.”
↩︎ Exam trap 1 - 2.
“tool_use_id: The id of the tool use request this is a result for.”
↩︎ Dispatching tool calls in the agentic loop“so dispatch those blocks on both fields.”
↩︎ Dispatching tool calls in the agentic loop“Your best bet during development is to try the request again with more-detailed description values in your tool definitions.”
↩︎ Error handling“Claude will retry 2-3 times with corrections before apologizing to the user.”
↩︎ Error handling“Unlike client tools, you do not need to handle is_error results for server tools.”
↩︎ Error handling“you can return the error message in the content along with "is_error": true”
↩︎ Exam trap 3 - 3.
“When that happens, the API does not run the server tool. It returns immediately so that you can run the client tool first”
↩︎ When server tools hand control back“Detect the state by looking for a server_tool_use block whose id has no matching result block in the response.”
↩︎ When server tools hand control back“stop_reason is "tool_use", not "pause_turn".”
↩︎ Exam trap 2 - 4.https://code.claude.com/docs/en/agent-sdk/user-inputOfficial docs
“Deny: tool doesn't execute, Claude sees the message”
↩︎ Approval patterns before a tool runs“If you specify a tools array, include AskUserQuestion for this to work.”
↩︎ Approval patterns before a tool runs“Required workaround: dummy hook keeps the stream open for can_use_tool”
↩︎ Approval patterns before a tool runs