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
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 queues2.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.

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.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.

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)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.