DEV Community

Zhengxin
Zhengxin

Posted on

Claude Code Agent Loop Deep Dive (1): From Tool Declarations to Pre-Execution Approval

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 Read exists in the current session?
  • Does an LLM request for Read execute 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
}
Enter fullscreen mode Exit fullscreen mode

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 in tool_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.input must 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" } }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. The user interface, through Allow or Deny.
  2. A PermissionRequest hook, defined by a user or team in settings.json and implemented through a command or HTTP endpoint.
  3. 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 tools section declares what can be called in the session. The model uses descriptions to decide whether a tool fits.
  • A tool_use response 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; ResolveOnce prevents 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 — hasPermissionsToUseTool flow
  • src/hooks/toolPermission/handlers/interactiveHandler.ts — interactive approval Promise
  • src/hooks/toolPermission/PermissionContext.ts — ResolveOnce claims
  • src/utils/permissions/PermissionUpdate.ts — alwaysAllow persistence
  • src/utils/settings/types.ts — permissions schema in settings.json
  • src/types/permissions.ts — permission-rule sources
  • src/tools/AgentTool/runAgent.ts — subagent permission reset

Top comments (0)