Tool use is a contract: you describe available operations and their schemas, Claude returns a structured request, and your application decides whether and how to execute it. Claude does not independently run a client tool. Tool results are then sent back in a subsequent message so Claude can continue the task or produce a final answer.
The model should see the smallest tool set that can complete the task. Separate read-only lookups from side effects, reject unsupported combinations, cap tool calls per request, and require human confirmation for consequential writes.
A strong tool description says what the tool does, when it should be used, what it cannot do, and what the inputs mean. Name tools by capability, such as `orders_get_status`, rather than vague verbs such as `process`.
Split read and write operations. A model can safely request `get_order_status` more often than `issue_refund`, and the latter should require explicit user confirmation plus policy checks.
When Claude returns a tool_use block, retain the assistant content and append a user message containing the matching `tool_result` block. The result should be concise, structured, and safe to show to the model. Do not include stack traces, credentials, or raw internal records.
Bound the loop by max iterations, per-tool budgets, deadline, and cancellation. If a tool keeps failing, return a clear final failure rather than retrying forever.
The authenticated actor—not the model-provided text—determines authorization. Resolve resource ownership server-side, enforce tenant isolation, and log every write attempt with policy result and correlation ID.
For a destructive or externally visible action, show the user a prepared action summary and require a confirmation token that your backend verifies.
Tools fail due to unavailable services, invalid arguments, race conditions, and stale data. Define error codes that let Claude explain the situation without exposing internals. Retry only idempotent operations and only under a bounded policy.
Test malicious tool inputs, cross-tenant IDs, duplicate requests, timeouts, and partial downstream failures.
A tool result is part of the conversation, not a dump of your database row. Return the few fields Claude needs to answer the question and keep internal identifiers, audit records, and error details on the server.
For writes, build a preview first. The user can confirm the preview; then your application performs the operation under its normal authorization and idempotency rules.
This request gives Claude one read-only capability. The response may include a tool_use block; it does not query the order system by itself.
import os
from anthropic import Anthropic
client = Anthropic()
order_status_tool = {
"name": "get_order_status",
"description": "Look up the shipping status for one order. Never cancel or change an order.",
"input_schema": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
"additionalProperties": False,
},
}
response = client.messages.create(
model=os.environ["CLAUDE_MODEL"],
max_tokens=300,
tools=[order_status_tool],
messages=[{"role": "user", "content": "Where is order o-100?"}],
)
for block in response.content:
if block.type == "tool_use":
print(block.name, block.input)
The handler ignores any model claim about ownership and uses the authenticated customer identity provided by the backend.
ORDERS = {"o-100": {"customer_id": "c-7", "status": "shipped"}}
def get_order_status(order_id: str, authenticated_customer_id: str) -> dict:
order = ORDERS.get(order_id)
if order is None or order["customer_id"] != authenticated_customer_id:
return {"ok": False, "code": "not_found"}
return {"ok": True, "status": order["status"]}
print(get_order_status("o-100", "c-7"))
{'ok': True, 'status': 'shipped'}
A loop needs a hard stop even when the model keeps requesting more work.
MAX_TOOL_ROUNDS = 3
def may_continue(round_number: int, deadline_reached: bool) -> bool:
return round_number < MAX_TOOL_ROUNDS and not deadline_reached
print(may_continue(2, False))
print(may_continue(3, False))
True
False
The response contains the shipment status, not the full customer order record.
def public_order_result(order: dict) -> dict:
return {
"ok": True,
"order_id": order["id"],
"shipment_status": order["shipment_status"],
}
print(public_order_result({"id": "o-100", "shipment_status": "shipped"})["shipment_status"])
shipped
A capability-design lab
0 of 2 completed
Narrow tools are easier to describe, authorize, test, log, and revoke.
A stable, minimal error code and retry guidance—never credentials, stack traces, or unrelated records.
Explore 500+ free tutorials across 20+ languages and frameworks.