In the opening article, I introduced the five-line skeleton of an agent loop: call the LLM, check for tool_use, execute requested tools, and stop when there is no tool call.
This article examines the first question inside every iteration: how does the LLM know which tools it can call, and what still happens after it asks to call one?
More concretely:
- Why does an LLM know that
Readexists in the current session? - Does an LLM request for
Readexecute immediately? - If not, what occurs in between?
- Who decides whether the call is allowed?
Tools: the third section of an API request
The loop sends a messages array on every LLM call. But a complete Messages API request has three important sections:
POST /messages
{
system: "...", ← system prompt
tools: [...], ← available tools
messages: [...] ← conversation history
}
All three are sent to the LLM together. messages grows on every iteration, while tools and system are comparatively stable during a session.
The model only calls tools listed in tools. During training, it learns that a tool_use.name must be chosen from the declared menu. If Read is not listed, the model has no reason to know that it exists.
What a tool declaration contains
tools is an array, and each item defines one tool with three fields:
-
name: the string that appears intool_use.name. -
description: what the tool does, when to use it, when not to use it, and its boundaries. -
input_schema: a JSON Schema for the accepted parameters;tool_use.inputmust conform to it.
The model’s choice of whether to call a tool depends heavily on description. A clear description helps it choose the proper situation; a weak one invites misuse or omission.
In short, the tools section is the LLM’s tool menu. Once assembled, it is included in every API call.
For Anthropic’s API-level format, see the official Tool use documentation.
A tool request is not yet tool execution
After the LLM sees the menu and receives “Please inspect auth.py,” it may respond with a tool-use block like this:
{
role: "assistant",
content: [
{ type: "tool_use", id: "toolu_A", name: "Read", input: { file_path: "auth.py" } }
]
}
The minimal loop says to execute the tool, append the result, and continue. In the real product, there can be one more step in between:
Permission approval
Reading an ordinary file can often pass automatically. But an LLM request such as the following should not run without a decision:
-
Bash rm -rf /some/dir: destructive deletion; -
Edit /etc/passwd: modifying a sensitive system file; - a new kind of Bash command that the user has not yet approved.
In such situations, Claude Code does not execute the tool directly. It presents an approval prompt and waits for the user to allow or deny the action.
This is the single deliberate exception to the rule that users are absent from the middle of the loop. Without it, the loop could autonomously act on a destructive request before the user could react.
Three mechanisms cover three different necessary exceptions to autonomous execution:
| Mechanism | How the loop is affected |
|---|---|
| Permission approval | Blocks the loop until the user decides |
| Interrupt | The user actively stops a running loop, for example with Ctrl-C |
maxTurns |
The runtime stops automatically at a hard iteration ceiling |
Six sources of approval rules
Prompting on every Read would be unbearable, so Claude Code needs to remember policy: what may proceed automatically, what must always ask, and what is never allowed.
| Source | Priority | Meaning | Example |
|---|---|---|---|
abortController already aborted |
Highest | Reject immediately | After Ctrl-C, remaining approvals are skipped |
denyRule |
Very high | Deny without asking |
Bash(rm -rf *) is always prohibited |
askRule |
High | Ask every time |
Bash(git push) always requires confirmation |
| Automatic classifier | Medium | System judges a clearly safe action | Read-only Grep or Read commonly pass |
alwaysAllow |
Medium | A remembered user approval |
Read(*) permits file reads |
defaultMode |
Low | The system-wide fallback policy | Reads allowed; writes require approval |
These policies can come from several configuration scopes:
- CLI arguments: valid for a session launched with those arguments;
- session state: “always allow” decisions made in the current conversation;
-
.claude/settings.local.json: user-specific settings for this project, not committed to Git; -
.claude/settings.json: project settings shared through Git; -
~/.claude/settings.json: user-wide settings; - managed settings: organization-wide policy that an individual user cannot override.
Before every tool execution, the runtime evaluates the rules in order. Once a layer determines the result, no lower-priority rule is needed.
How an approval blocks the loop
When no automatic rule can decide, the control flow is conceptually this:
LLM emits tool_use
↓
Runtime prepares the tool and checks permissions
↓
No rule matches: user approval is required
↓
Loop blocks: Promise is pending
↓
UI renders an approval dialog
↓
User chooses Allow or Deny: Promise resolves
↓
Loop continues; the tool may execute
The loop does not poll to see whether the user clicked. It awaits a Promise. If the user thinks for ten seconds, the loop does nothing for ten seconds.
That also means no LLM API call is in progress during approval. The user’s deliberation time does not add API cost.
Three approval sources race together
Interactive user input is not the only way to resolve an approval. The result can arrive from three sources:
- The user interface, through Allow or Deny.
- A
PermissionRequesthook, defined by a user or team insettings.jsonand implemented through a command or HTTP endpoint. - An AI classifier that judges whether an operation is obviously safe.
All three begin at the same time. The first result wins; later results no longer matter. This is an intentional race:
- user input is authoritative but usually takes seconds;
- hooks are programmable and may take milliseconds or seconds;
- a classifier can respond quickly but may be conservative.
Running them sequentially would make the user wait for the sum of their delays. Racing them makes the approval flow wait only for the first viable answer.
Concurrency requires a guard against resolving the same Promise twice. Claude Code uses a ResolveOnce mechanism: the first source claims the resolution, and subsequent attempts fail harmlessly. A race condition, often a bug, becomes a product feature for responsiveness.
Subagents do not inherit session approvals
Suppose a user has selected “always allow Bash” in the main conversation. If the main agent starts a subagent, does the subagent inherit that session-level trust?
No. Claude Code clears the parent conversation’s session approvals when the subagent starts. It retains launch-level CLI settings, while the subagent has its own allowedTools policy.
That is deliberately conservative. An alwaysAllow decision expresses trust in the main conversation; a subagent is another autonomous model context, not automatically an equally trusted continuation. It may need to ask again, but defaulting to less trust is the safer security posture.
Summary
- The API request’s
toolssection declares what can be called in the session. The model uses descriptions to decide whether a tool fits. - A
tool_useresponse is still only a request. Permission approval can intervene before execution. - Approval is the deliberate exception to an otherwise self-running loop.
- Rules are resolved across six sources, from aborts and explicit denials down to a default mode.
- The loop waits through a Promise, so user deliberation does not consume API calls.
- User input, hooks, and an automatic classifier race for the earliest resolution;
ResolveOnceprevents double resolution. - Subagents do not inherit session-level approvals, preserving a conservative trust boundary.
The next article examines Hooks: the more general programmable insertion points around the loop. Permission approval is a specialized intervention; hooks are the general answer to “how can users insert custom logic into an autonomous agent loop?”
References
Primary implementation locations (Claude Code v2.1.220):
-
src/utils/permissions/permissions.ts—hasPermissionsToUseToolflow -
src/hooks/toolPermission/handlers/interactiveHandler.ts— interactive approval Promise -
src/hooks/toolPermission/PermissionContext.ts—ResolveOnceclaims -
src/utils/permissions/PermissionUpdate.ts—alwaysAllowpersistence -
src/utils/settings/types.ts—permissionsschema insettings.json -
src/types/permissions.ts— permission-rule sources -
src/tools/AgentTool/runAgent.ts— subagent permission reset
Top comments (0)