Tutorials Logic, IN info@tutorialslogic.com

Claude Tool Use: Build a Read-Only Order Lookup Loop

A tool request is a request—not permission

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.

The safe starting point: Begin with one narrow, read-only lookup. The tool handler verifies the signed-in customer, returns a minimal result, and never exposes the underlying database row to the model. A tool description tells Claude what it may request; your handler decides what it is actually allowed to do.

The tool loop your server owns

Claude chooses whether to ask. Your application controls every operation that follows.
  1. Claude proposes a call The response contains a structured tool-use block.
  2. Server validates Check identity, parameters, ownership, and allowed action.
  3. Tool runs narrowly Fetch or prepare only the permitted data.
  4. Result returns to Claude Send a small result so it can answer the customer.

Design the tool around a single capability

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.

  • Name tools by specific capability.
  • Describe both allowed and forbidden behavior.
  • Separate read from write.

Write descriptions that help Claude choose correctly

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.

  • Preserve assistant tool_use content.
  • Return a matching tool_result.
  • Set loop, time, and cost budgets.

Treat authorization as part of every call

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.

  • Authorize from session identity.
  • Require confirmation for consequential writes.
  • Audit allowed and denied requests.

Prepare writes separately from confirmed writes

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.

  • Use safe error codes.
  • Retry only idempotent operations.
  • Test partial failure and cancellation.

Send back a small tool result

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.

  • Return the minimum useful result.
  • Use stable public error codes.
  • Prepare writes before asking for confirmation.

Build the first read-only tool loop

Let Claude Request an Order Lookup

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.

Let Claude Request an Order Lookup
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)
  • Your application should allow this tool only for an authenticated support session.
  • Next, pass the checked result back in a tool_result block before asking Claude for the final reply.

A Safe Read-Only Tool Handler

The handler ignores any model claim about ownership and uses the authenticated customer identity provided by the backend.

A Safe Read-Only Tool Handler
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"))
Output
{'ok': True, 'status': 'shipped'}
  • Return the same safe result for missing and unauthorized resources to avoid enumeration.

Bound a Tool Loop

A loop needs a hard stop even when the model keeps requesting more work.

Bound a Tool Loop
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))
Output
True
False
  • Production code also tracks cost and user cancellation.

Shape a Safe Tool Result

The response contains the shipment status, not the full customer order record.

Shape a Safe Tool Result
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"])
Output
shipped
  • Keep the internal record and the model-visible result as different objects.
Before giving a model access to live data

Tool boundary review

4 checks
  • I can explain the assistant/tool_result message sequence.
  • I authorize tools with backend identity.
  • I separate reads from writes.
  • I bound loops and return safe errors.

Tool-use shortcuts that create excessive access

  • Giving the model a generic shell or database tool.
  • Trusting a user ID supplied in tool arguments.
  • Retrying non-idempotent writes after a timeout.

A capability-design lab

Turn one business operation into a narrow tool

0 of 2 completed

Questions about tools, permissions, and confirmation

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.

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.