How to Debug Claude API Tool Calls
Debugging Claude API tool calls usually means one of three things is going wrong: the model isn't calling the tool when it should, it's calling the tool with malformed or wrong arguments, or your code isn't handling the tool_use response correctly before sending the result back. Each of these has a distinct signature and a distinct fix, and most of the time the bug isn't in Claude's reasoning — it's in the schema, the message loop, or the tool result formatting.
This guide walks through how to isolate which category your bug falls into, the specific things to check at each step, and how to set up logging so you catch these issues before they reach production.
Step 1: Confirm Claude is actually attempting a tool call
Before debugging arguments, check whether stop_reason in the response is tool_use. If Claude just returns plain text instead of invoking a tool, the problem is usually one of:
- The tool description is ambiguous. Claude decides whether to call a tool based on your
descriptionfield. Vague descriptions ("handles user data") give the model nothing to match intent against. Be explicit: "Looks up a customer's order status by order ID. Use this whenever the user asks about order status, shipping, or delivery." - The system prompt discourages tool use. Instructions like "answer directly whenever possible" can override your intent. If you want tool-first behavior, say so explicitly.
tool_choiceisn't set correctly. If you need a specific tool called, usetool_choice: {"type": "tool", "name": "your_tool"}instead of relying onauto.
Log the raw stop_reason and content array on every response during development. Don't rely on assumptions about what the model did — print it.
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-4-20250514",
"max_tokens": 1024,
"tools": [{ "name": "get_order_status", "description": "...", "input_schema": {...} }],
"messages": [{"role": "user", "content": "Where is my order #4471?"}]
}'
Inspect the full JSON response, not just a parsed summary. Truncated logging hides malformed tool blocks.
Step 2: Validate the tool input against your schema
If Claude is calling the tool but your code throws errors downstream, the arguments it generated likely don't match your input_schema. Common mismatches:
- Missing required fields. If your schema marks a field as
requiredbut the model omits it in edge cases (ambiguous user input, multi-turn context loss), your handler needs a validation layer rather than assuming the shape is always correct. - Type coercion issues. Claude might return a string where you expect a number, especially for IDs that look numeric. Validate and coerce explicitly rather than trusting raw JSON types.
- Enum mismatches. If you define an enum of allowed values and the model's chosen value doesn't exactly match (casing, whitespace), your downstream code will fail silently or throw.
Add a validation step between receiving the tool_use block and executing your function:
function validateToolInput(toolName, input, schema) {
const errors = [];
for (const key of schema.required || []) {
if (!(key in input)) errors.push(`Missing required field: ${key}`);
}
if (errors.length) {
console.error(`Tool validation failed for ${toolName}:`, errors, input);
}
return errors;
}
This catches malformed calls before they crash your handler, and the logged output tells you exactly which field is the problem instead of a generic exception stack trace.
Step 3: Check the full message loop, not just one turn
Tool use is a multi-turn protocol: Claude returns a tool_use block, you run the function, and you send a tool_result back in the next user message. A huge share of "broken tool calls" bugs are actually loop bugs:
- Wrong
tool_use_id. Thetool_resultblock must reference the exactidfrom thetool_useblock it's answering. Mismatched IDs cause the API to reject the request or, worse, silently confuse context in longer conversations. - Sending the result in the wrong role. Tool results go in a
usermessage, notassistant, even though they're machine-generated. - Dropping the original assistant message. You must include the assistant's
tool_usemessage in the conversation history before sending thetool_result. If you only send the result without the preceding turn, Claude loses the reasoning context.
Here's a minimal correct loop:
const toolUseBlock = response.content.find(b => b.type === "tool_use");
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [{
type: "tool_result",
tool_use_id: toolUseBlock.id,
content: JSON.stringify(functionResult)
}]
});
If your loop skips a step or reorders these, Claude's next response often looks "confused" — repeating the same tool call, ignoring the result, or hallucinating a different answer. That's not a reasoning failure, it's a protocol bug in your integration.
Step 4: Use structured logging across the whole call chain
Once the basic mechanics are correct, the fastest way to catch regressions is structured logging at every hop: the outgoing request, the raw response, the parsed tool call, the validation result, and the final tool result sent back. Store these with a shared request ID so you can replay a single failed conversation turn-by-turn.
If you're running Claude through SubToAPI, every request already returns usage metadata and is logged against your application key, which makes it easier to correlate a bad tool call with a specific endpoint, environment, or team member without building your own logging pipeline from scratch. See /docs/tools for request and response shapes, and /docs/messages for the full message format reference.
Step 5: Reproduce with a minimal prompt
If a tool call fails intermittently, strip the conversation down to the smallest prompt that reproduces it — one user message, one tool definition. This isolates whether the bug is in the tool schema itself or in accumulated context from a longer conversation (stale history, conflicting instructions, or token limits truncating earlier turns).
Checklist for tool call debugging
- Log
stop_reasonand the fullcontentarray on every response - Validate tool input against your schema before executing
- Confirm
tool_use_idmatches exactly betweentool_useandtool_result - Confirm the assistant's tool-use message is preserved in history
- Confirm
tool_resultis sent as auserrole message - Reproduce with a minimal single-turn prompt before debugging multi-turn state
Getting a clean API layer helps here too — check /docs/quickstart if you're setting up request handling for the first time, and /docs/streaming if you're debugging tool calls that happen mid-stream, since partial tool-use blocks need to be buffered until complete before validation.
Questions
Why does Claude call a tool with the wrong arguments? Usually the tool description or schema is ambiguous, or the user's message doesn't contain enough information and the model is guessing. Tighten the description field and mark fields as required only when truly necessary.
Why does my app crash after a tool call? Almost always a missing validation layer. Treat Claude's tool input as untrusted input — validate types and required fields before calling your function, the same way you'd validate any external API payload.
Can I debug tool calls without re-running the whole conversation? Yes — log the full request/response at each turn with a shared request ID, then replay just the failing turn in isolation with a minimal prompt. This is faster than re-running multi-turn conversations end to end.