Claude Code reads files, edits code, and runs tests without you touching a keyboard. The Claude API lets you give Claude access to databases, APIs, and custom functions. These look like two different systems. They are not.
Both surfaces run on the same primitive: tool use. Claude signals intent. You execute. You return results. Claude synthesizes. Once we understood this one contract, we got better at both Claude Code and the raw API — because we stopped treating them as separate products and started seeing them as two interfaces to the same mechanism.
What you’ll learn
- How the tool use request/response cycle works at the API level
- How to write tool definitions that Claude actually calls correctly
- How parallel tool calls and
tool_choicecontrol agent behavior - How Claude Code’s built-in tools connect to the same API primitive
- How to configure Claude Code permissions to match your team’s risk tolerance
- What advanced patterns (tool search, programmatic tool calling) solve
Prerequisites
- Familiarity with REST APIs and Python or TypeScript
- Claude Code installed (for the Claude Code sections)
- A Claude API key (for the API sections)
- Basic understanding of JSON Schema is helpful but not required
The contract: Claude signals, you execute
Claude cannot run code on your machine. It cannot call your database. It cannot hit your internal API. It can only produce output — and in the case of tool use, that output is a structured request describing exactly what it wants called, with what arguments.
This is the contract:
- You define what tools are available
- Claude decides whether and which tools to call
- You execute the function on your side
- You return the result
- Claude synthesizes and either calls more tools or finishes
Every agentic Claude workflow — whether via the API or Claude Code — follows this cycle. Claude Code’s Read, Edit, and Bash actions are not special. They are predefined tools that Claude Code declares to the model and executes on your machine, using the same tool_use / tool_result contract.
The four-step API cycle
Here is the complete cycle, illustrated with a single tool.
- 01
Define tools and send the request
Include a
toolsarray in your/v1/messagesrequest. Each tool needs aname,description, andinput_schema.import anthropic import json client = anthropic.Anthropic() tools = [{ "name": "get_weather", "description": ( "Get the current weather in a given location. " "The location must be a city and state in the US, e.g. San Francisco, CA. " "Returns temperature in the specified unit and current conditions. " "Use this when the user asks about current weather. Does not provide forecasts." ), "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit. Defaults to fahrenheit if not specified." } }, "required": ["location"] } }] messages = [{"role": "user", "content": "What's the weather in San Francisco?"}] response = client.messages.create( model="claude-opus-5-5", max_tokens=4096, tools=tools, messages=messages ) - 02
Detect Claude's tool use signal
When Claude decides to call a tool,
stop_reasonis"tool_use"and the response contains one or moretool_usecontent blocks. The relevant part of the response has this shape:# response.stop_reason == "tool_use" # a tool_use block in response.content: # type = "tool_use" # id = "toolu_..." # name = "get_weather" # input = {"location": "San Francisco, CA", "unit": "celsius"}On current models thinking is on by default, so
response.contentcan also containthinkingblocks ahead of thetool_useblock. Find blocks bytype, never by position.Note that
inputis already a parsed object — nojson.loads()needed. This is a key difference from OpenAI’s format, covered at the end of this guide. - 03
Execute the function on your side
Claude cannot run the function. You execute it using the
nameandinputfrom thetool_useblock.if response.stop_reason == "tool_use": tool_block = next(b for b in response.content if b.type == "tool_use") # Your actual implementation goes here weather_data = {"temperature": "18°C", "conditions": "Partly cloudy"} - 04
Return the result and continue
Append the assistant message and a new
usermessage containing atool_resultblock. Thetool_use_idmust match theidfrom Step 2. Appendingresponse.contentunchanged also passes any thinking blocks back, which is what the API expects.messages.append({"role": "assistant", "content": response.content}) messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": tool_block.id, "content": json.dumps(weather_data) }] }) final = client.messages.create( model="claude-opus-5-5", max_tokens=4096, tools=tools, messages=messages ) print(next(b.text for b in final.content if b.type == "text"))
In the user message that carries tool results, the tool_result blocks must come first in the content array; any text goes after them. The API rejects a message with text ahead of the results. When returning multiple tool results (parallel calls), put them all in a single user message — not separate messages.
To signal a tool execution failure, set "is_error": true on the tool_result block. Claude reads this explicitly and can retry with a different approach, ask a clarifying question, or report the failure gracefully.
Writing tool definitions that work
The input_schema constrains what Claude sends. The description field determines whether Claude calls the tool at all — and whether it calls it correctly.
A schema validation error is recoverable. A model that silently ignores the right tool is not.
Weak:
"Get stock price for ticker"
Strong:
"Retrieves the current stock price for a given ticker symbol. The ticker symbol
must be a valid symbol for a publicly traded company on a major US stock exchange
like NYSE or NASDAQ. The tool will return the latest trade price in USD. Use this
when the user asks about the current or most recent price of a specific stock.
It will not provide forecasts or any other information about the company."
The strong version covers what, when (and when not), format expectations, return value, and limitations. We aim for at least 3–4 sentences per tool.
Other signals that improve accuracy
enum constraints for fixed-value parameters. They tell Claude exactly which values are valid; pair them with strict tool use (below) if an out-of-range value would break something downstream.
input_examples (an optional field in the tool definition) for tools with nested objects, optional parameters, or format-sensitive inputs. In Anthropic’s advanced tool use write-up, tool use examples improved accuracy from 72% to 90% on complex parameter handling in their internal testing.
Namespace tool names by service: github_list_prs, slack_send_message, database_query. This prevents ambiguity when you have 10+ tools across multiple services.
Consolidate related operations: one github_pr tool with an action enum (list, create, merge, review) is usually easier for Claude to use well than four narrow tools. Fewer tools with clear roles are easier to reason about.
Add "strict": true to a tool definition to guarantee schema conformance. Claude’s input will match the input_schema exactly — no missing required fields, no type mismatches. We reserve it for tools where a schema violation causes a real downstream failure. See strict tool use for the supported schema features.
Parallel tool calls and tool choice
Parallel calls
Claude can invoke multiple tools in a single response turn without waiting for intermediate results. This is enabled by default.
When a user asks “What’s the weather in San Francisco and New York?”, Claude can return two tool_use blocks in one response. Return all results in a single user message:
messages = [{"role": "user", "content": "What's the weather in SF and NYC?"}]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
tools=tools,
messages=messages
)
# response.content can contain two tool_use blocks
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = fetch_weather(block.input["location"])
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# All results in ONE user message
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
For operations that must run sequentially — for example, reading a file before deciding which file to write — set disable_parallel_tool_use inside tool_choice: tool_choice={"type": "auto", "disable_parallel_tool_use": True}.
Controlling tool choice
The tool_choice parameter controls whether and which tools Claude calls:
| Value | Behavior | When to use |
|---|---|---|
{"type": "auto"} | Claude decides (default when tools are provided) | General use |
{"type": "any"} | Must call at least one tool | Force a tool call |
{"type": "tool", "name": "X"} | Must call exactly this tool | Extraction pipelines |
{"type": "none"} | No tool use | Synthesis-only turns |
Forcing a specific tool with {"type": "tool"} is a reliable way to get structured data from Claude — more predictable than prompt-engineering around JSON output — on models that support forced tool use:
extraction_tool = {
"name": "extract_contact",
"description": "Extract contact information from unstructured text. Call this tool always.",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"phone": {"type": "string"},
"company": {"type": "string"}
},
"required": ["name"],
"additionalProperties": False
},
"strict": True
}
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
tools=[extraction_tool],
tool_choice={"type": "tool", "name": "extract_contact"},
messages=[{
"role": "user",
"content": "John Smith from Acme Corp. Email: john@acme.com, mobile 415-555-0199"
}]
)
tool_block = next(b for b in response.content if b.type == "tool_use")
contact = tool_block.input # already parsed, and schema-conformant with strict
Not every model accepts forced tool use. Per the tool definition docs, Claude Opus 5.5 and Claude Fable 5.1 return a 400 error for any and tool, as does manual extended thinking on older models. On those, use auto with strict tool use, or structured outputs when you need a fixed JSON shape. That’s why this example uses claude-sonnet-5.
Claude Code: the same primitive, pre-configured
Claude Code is Claude with a curated set of tool definitions sent on every request. Every file read, code edit, and test run is a tool_use / tool_result cycle that Claude Code executes for you. You never write that loop — Claude Code runs it.
The built-in tool set changes between releases, so treat this as a map of the main groups rather than a complete list. The tools reference is the current source of truth.
| Category | Examples |
|---|---|
| File operations | Read, Write, Edit, NotebookEdit |
| Search | Bash running find and grep; Glob and Grep on Windows; LSP for definitions and references |
| Execution | Bash (two-minute default timeout, ten-minute ceiling out of the box), PowerShell, Monitor for background output |
| Web | WebSearch, WebFetch |
| Orchestration | Agent (subagent in a separate context), Skill, the Task* task-list tools, EnterPlanMode / ExitPlanMode, AskUserQuestion |
Tool selection discipline
Search is the change most likely to surprise you if you learned Claude Code a while ago. On macOS, Linux, and WSL, Claude Code now leaves Glob and Grep out of the default tool set and searches with find and grep through Bash, which run embedded versions of bfs and ugrep. On Windows, Glob and Grep are still part of the default set.
File edits are where the dedicated tools still clearly win. We steer Claude toward Edit over sed in Bash: an Edit call is an exact string replacement that shows up as a reviewable diff and is matched by Edit(...) path rules.
| Task | Prefer | Over |
|---|---|---|
| Read a file at a known path | Read | Bash(cat ...) |
| Targeted code edits | Edit | Bash(sed ...) |
| Create or overwrite a file | Write | Bash(echo ... > file) |
| Shell state, pipelines, search, interactive tools | Bash | — |
If you see Claude reaching for the shell where a file tool would be clearer, say so in your CLAUDE.md. Claude reads it at the start of every session — CLAUDE.md as a contract covers how we write rules the model actually follows.
An agentic loop, step by step
Take a single prompt: “The checkout flow is broken for users with expired cards. Check src/payments/ for the issue. Write a failing test, then fix it.”
Claude Code turns that into a chain of tool calls. Typically it lists the files under src/payments/, reads the likely candidates, searches for expiry handling, writes a new test file, runs the test through Bash and sees it fail, applies an Edit to the comparison it suspects, and runs the test again until it passes. Each result feeds into Claude’s reasoning for the next step. This is what “agentic” means in practice: the loop continues until the response ends with stop_reason of end_turn instead of tool_use.
For exploring unfamiliar codebases, use the Agent tool: it spawns a subagent in a separate context window, keeping the investigation out of your main session’s token budget. Subagents as context firewalls covers when that pays off.
Claude Code permissions — controlling the loop
Every tool call in Claude Code is checked against permission rules before execution. Rules are evaluated in order: deny, then ask, then allow. The first match in that order decides, so a matching deny always wins over an allow, however specific the allow rule is.
Default approval tiers:
| Tool type | Example | Default behavior |
|---|---|---|
| Read-only | Read, read-only shell commands | No prompt |
| File modification | Edit, Write | Prompts; “don’t ask again” lasts until the session ends |
| Shell execution | Bash | Prompts; “don’t ask again” is permanent per repository and command |
Permission rules live in settings files scoped by reach:
~/.claude/settings.json— user-level, all projects.claude/settings.json— project-level, committed to git, shared with team.claude/settings.local.json— local-only, git-ignored
This is the kind of baseline we start teams on:
{
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git commit *)",
"Bash(git checkout *)"
],
"deny": [
"Bash(git push *)",
"Bash(rm *)",
"Bash(sudo *)",
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}
Commit this file to your repo. Every developer shares the same policy without configuration. Claude runs tests and commits without asking, but cannot push, run rm, or read the secret files.
Wildcard note: Bash(npm run test *) has a space before *, which enforces a word boundary. It matches npm run test src/ (and bare npm run test) but not npm run test-integration. Bash(npm run test*) (no space) would match both.
Read(./.env) deny rules cover Claude’s built-in Read tool and the file commands Claude Code recognizes in Bash, such as cat, head, and tail. They don’t cover a script that opens the file itself, like a Python or Node process. Where that matters, enable the sandbox, which enforces filesystem and network limits at the OS level for shell commands and their children.
Permission modes change the default behavior for a session. Cycle with Shift+Tab or set defaultMode in settings:
default— prompts on first use of each toolacceptEdits— auto-accepts file edits and common filesystem commands in the working directoryplan— explores and reads, but doesn’t edit your source filesauto— auto-approves tool calls with background safety checksdontAsk— auto-denies anything that would otherwise promptbypassPermissions— skips permission prompts (isolated, trusted environments only)
For single-session automation, pre-approve exactly what the run needs with --allowedTools on the CLI:
# Migrate files in batch, pre-approving only Edit and git commit
while read -r file; do
claude -p "Migrate $file from CommonJS to ESM. Return OK or FAIL." \
--allowedTools "Edit,Bash(git commit *)"
done < migration-targets.txt
For permission logic the rule syntax cannot express, use hooks: a PreToolUse hook can inspect any tool call at runtime and block it. Hooks as guardrails covers how we write them.
Advanced patterns
Three patterns address specific scaling problems. Their status differs: programmatic tool calling is documented as generally available on the Claude API, while the SDK tool runner is still labeled beta. Check each feature’s model-compatibility table before you build on it.
Tool search solves the large-tool-library problem. In Anthropic’s advanced tool use write-up, a five-server MCP setup with 58 tools consumed about 55K tokens of definitions before the conversation started. With the tool search tool, you mark tools with defer_loading: true and Claude searches the catalog to load only what it needs. Anthropic reported an 85% reduction in token usage, and on their MCP evaluations accuracy for Claude Opus 4 rose from 49% to 74%. Our practical rule: keep the handful of tools you call on almost every request loaded, and defer the rest. This is context budgeting applied at the tool definition layer, not just the conversation layer.
Programmatic tool calling solves the serial round-trip problem. Standard tool use requires one model round-trip per invocation. For bulk operations — checking budget compliance for 20 employees, for example — that means 20 serial round-trips with all intermediate data accumulating in context. Programmatic tool calling lets Claude write code that calls your tools inside a code execution container, so one execution run replaces the round-trips and Claude only receives the filtered final result. It requires the code execution tool at version code_execution_20260120 or later, and each tool opts in with allowed_callers. Current Opus and Sonnet models support it; Claude Haiku 4.5 does not.
SDK tool runner eliminates the multi-turn loop boilerplate. The official SDKs, including Python and TypeScript, include a client.beta.messages.tool_runner helper (toolRunner in TypeScript) that manages the send → receive → execute → return cycle automatically. In Python, decorate functions with @beta_tool — the decorator derives the tool schema from the function signature and docstring.
OpenAI format reference
Developers porting agents from OpenAI’s Chat Completions API encounter structural differences that produce subtle bugs:
| Aspect | Claude | OpenAI Chat Completions |
|---|---|---|
| Schema key | input_schema | parameters |
| Detect tool call | stop_reason == "tool_use" | Check message.tool_calls |
| Arguments type | Parsed object (block.input) | JSON string (needs json.loads()) |
| Tool results role | "user" message | "tool" message role |
| Tool choice format | {"type": "tool", "name": "X"} | {"type": "function", "function": {"name": "X"}} |
| Error signal | "is_error": true on tool_result | No dedicated error flag |
The parsed input object eliminates a class of bugs caused by malformed JSON strings. The is_error flag allows Claude to reason explicitly about tool failures rather than receiving a generic error string.
In production, most of the tool-use failures we see aren’t in the loop itself. They sit in the edges around it: vague tool descriptions, permission rules nobody reviewed, and tool libraries that grew until they ate the context window. Getting those right is most of the work of taking an agent to production, and it’s the core of our Build engagements.
Next steps
- Headless Claude Code — run the same loop non-interactively in CI and scripts
- Parallel worktrees — run multiple tool-equipped Claude sessions side by side
- Hooks as guardrails —
PreToolUseandPostToolUsehooks for runtime permission logic - Tool use documentation — the official reference, including server tools and the complete API specification