This article examines the public mirror’s March 31, 2026 source snapshot. The mirror is not an official Anthropic source release. Implementation claims concern this fixed snapshot, not necessarily the current product release; official documentation establishes product or API contracts.

The Anthropic API exposes tool use as message blocks: the assistant emits tool_use, the client runs something, and the next user message returns tool_result. Claude Code adds the checks and execution machinery needed by a coding agent.

Reading goal: follow the tool proposal until local effects occur, distinguish concurrent from serial work, and separate UI progress from the messages sent to the model. The runtime owns these decisions even though the model chooses a tool.

type ToolUseContext = {
  tools; mcpClients; permissionContext; messages;
  getAppState; abortController; options;
}

tool_use
  -> findToolByName()
  -> validate input
  -> permission / hook checks
  -> tool.call()
  -> tool_result
Source shape: the model proposes a tool call, while the runtime owns lookup, validation, permissions, execution, and result pairing.

1. The model sees tool schemas, not local functions

Before a turn reaches the provider, the runtime assembles the available tools and converts them into request parameters. That list can include built-in tools, MCP tools, and agent tools. The model receives names, descriptions, and schemas. It does not receive arbitrary access to local JavaScript functions.

The interactive REPL’s getToolUseContext() reads tools, MCP clients and resources, permission context, and the selected model from the current store. Using a stale render closure would miss an MCP connection established since that render. The REPL then passes messages, system and user context, canUseTool, and this runtime context into query().

2. ToolUseContext is the bridge

ToolUseContext carries the state needed after the model chooses a tool: command tables, tool lists, MCP clients, agent definitions, permission context, messages, replacement state, and the rendered system prompt. That bundle is why a tool can make decisions using the current session rather than acting like an isolated callback.

shape-level:
REPL turn = messages + system/user context + ToolUseContext + canUseTool
model request = messages + tool schemas + system prompt
runtime-only state = AbortController + store + file cache + permission queues
Tool schemas describe callable actions; process state and permission queues remain in the client.

2.1 Tool-use blocks drive the follow-up

query() creates assistantMessages, toolResults, and toolUseBlocks. It does not rely solely on stop_reason === 'tool_use'. During streaming it yields the assistant message, collects its actual tool-use blocks, and marks that a follow-up is required.

With streaming execution enabled, those blocks enter StreamingToolExecutor as they arrive; safe tools may overlap, while an unsafe queued tool waits for current work. After the model stream ends, the loop drains remaining results. Otherwise it passes the collected blocks to runTools(). These are alternative scheduling paths, not two consecutive stages.

Both paths produce updates. The loop yields each message to the caller, uses normalizeMessagesForAPI() to collect API user messages, and adopts updated context when supplied. No tool-use block means the loop need not enter another tool follow-up.

3. Tool calls are scheduled

runTools() groups calls according to whether they can run safely in parallel. Read-only operations can often be concurrent; side-effecting operations need ordering. The distinction is about correctness first and speed second. A coding agent that edits files and runs shell commands must not reorder effects casually.

Tool batch routing into concurrent and serial execution lanes
Batch routing keeps safe concurrency while preserving order for effectful work.

3.1 Only consecutive safe calls share a batch

partitionToolCalls() looks up each tool, parses its input, and asks isConcurrencySafe(parsedInput.data). Invalid input or a thrown safety check is treated conservatively as unsafe. The decision depends on the input, not just the name: Bash can qualify when its command passes the read-only constraints.

Input:   Read A, Grep B, Edit C, Read D
Batch 1: A || B → apply modifiers in A, B order
Batch 2: C      → apply its modifier immediately
Batch 3: D      → observe state after C
D cannot move ahead of C.
Concurrency is local to a batch; it does not prioritize all reads ahead of writes.

3.2 Execution and context updates have different ordering

The concurrent helper uses CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY, falling back to 10 for an unset, unparseable, or zero value. This limit belongs to the batch implementation and should not be assumed for the streaming executor. Returned context modifiers are queued by tool-use id and applied in original block order after the batch completes. Serial work applies each modifier immediately before proceeding.

That gives returned modifiers a stable application order. It does not make direct shared-store or filesystem changes transactional, nor does it provide automatic rollback. In-progress ids are removed when each invocation completes.

4. One tool call passes several checks

runToolUse() handles unknown tools, aborts, validation, permission wrapping, progress updates, and final result packaging. The internal call path is longer than a function invocation because every local action must be explainable back to the model and the user.

Tool call checks: lookup, schema validation, permission, execution, progress, and result
A tool call becomes real only after the runtime accepts its name, arguments, policy, and execution mode.

4.1 Lookup and input failures return an explanation

runToolUse() searches the visible tool pool, then allows a fallback only for a deprecated alias in the base tools. An unknown tool returns an error tool_result instead of silently breaking the loop. Aborted calls likewise return a visible failure.

checkPermissionsAndCallTool() first applies the Zod input schema, then the tool’s validateInput() semantic checks. A deferred tool whose schema was not sent can also return a hint to discover the tool through search. These checks explain why a syntactically shaped request may still fail before execution.

4.2 Hooks precede the permission decision

PreToolUse may emit progress or context, modify input, propose a permission decision, or stop the call. resolveHookPermissionDecision() retains settings deny/ask rules. Hook allow may use a rule-and-tool fast path when interaction is satisfied and requireCanUseTool is not set; other cases enter normal permission handling. Only an allow decision reaches tool.call(). A common sandbox and automatic retry are not universal stages.

This checkRuleBasedPermissions() helper still parses the input and runs tool.checkPermissions(), preserving tool denials, content-specific ask rules, and safetyCheck asks. It skips the full mode and default-allow flow; its checks extend beyond settings rules.

4.3 Observable input and actual call input are distinct

The runtime keeps callInput separate from a shallow clone used to expose derived fields to hooks and permission checks. Backfilling a path alone must not change the original path embedded in a tool result. An explicit hook or permission updatedInput can change the actual call input. The initial schema and semantic validation precede PreToolUse; the source does not guarantee that every replacement reruns the full semantic validation.

5. Result pairing protects the loop

The provider protocol expects every returned tool result to match a previous tool-use id. Claude Code preserves that pairing while converting local success, denial, validation error, interruption, and execution failure into model-readable blocks. This is how the loop continues without pretending that every tool call succeeded.

shape-level:
assistant: [text, tool_use(id="A", name="Read", input={...})]
runtime:   progress... then user(tool_result(tool_use_id="A", content="..."))
next API:  assistant(tool_use A), user(tool_result A)
The result is a user-message block paired by id, not just text printed to a terminal.

5.1 UI progress and API results are different views

streamedCheckPermissionsAndCallTool() combines progress with final updates in one async iterable. query() yields those messages to the UI, then normalizes them for API use. Not every displayed progress line becomes model context.

Unknown tools, invalid inputs, hook stops, permission refusals, cancellation, and execution errors need visible outcomes as well as successes. In the rejection path, attached images are placed beside a text-only error result at the top content level; that special handling is not a rule that all successful image results are split out.

5.2 A summary supplements the result

Optional tool-use summaries run only under their feature and context conditions, including no abort and no subagent. They do not replace the provider’s paired tool_result. Stable ids tell the model which proposal succeeded or failed, while summaries and progress serve additional presentation needs.

6. The invariant

The model can ask; the runtime must decide. That sentence explains most tool code in Claude Code. If a path changes tool schemas, execution ordering, permission checks, or result pairing, it changes what the model may cause on the local machine.

Sources

The source-code claims in this article are based on the public mirror and the linked official documentation. For server-side behavior and private feature switches, the article states only what the client sends or receives.