[KongchangAI]
· 4 min read· 2,050 words

Claude Code Agent Loop Termination: A Complete Guide to the Seven stop_reason Values

Claude Code Agent Loop Termination: A Complete Guide to the Seven stop_reason Values

Claude Code decides loop continuation based on response content, not stop_reason, with recoverable-error handling paths.

This article examines Claude Code v2.1.220 source code to reveal how the seven stop_reason values are actually handled in the Agent loop. The loop checks assistant message content for tool_use blocks rather than trusting the stop_reason tag. max_tokens and context overflow trigger recovery paths (token escalation, continuation prompts, compaction-retry), while refusal ends the loop immediately. Two gates prevent infinite loops: maxTurns and the Stop hook veto. Completion requires passing content, stop-reason, and hook checks simultaneously.

In the first article of this series, the Claude Code Agent loop was simplified to "keep going if there's a tool call, exit if there isn't." That sketch is intuitive but misses a critical field in the model's response: stop_reason. It tells the caller why the model stopped generating — whether it finished normally, requested a tool, hit the output limit, issued a safety refusal, or overflowed the context window. Each reason may require a different handling strategy.

This article dives into the Claude Code v2.1.220 source code to break down what each of the seven stop_reason values actually means, which signals the loop really relies on, the recovery paths that keep it alive, and the hard safeguards that prevent infinite loops.

Defining the Seven stop_reason Values

An Anthropic API response can contain any of the following seven stop reasons:

stop_reasonMeaning
end_turnThe model considers the response complete and returns control to the user
tool_useThe model has issued a tool call request
max_tokensThe server truncated output because it hit the token limit
stop_sequenceThe generated content matched a caller-provided stop sequence
refusalThe model refused the request for safety reasons
pause_turnThe model requests pausing the current turn for later resumption
model_context_window_exceededThe input exceeded the model's context window limit

The seven values look complete, but Claude Code does not treat them all the same.

Claude Code Agent loop stop_reason analysis

stop_reason is one of the top-level response fields of the Anthropic Messages API, sitting alongside content, role, and usage, and typed as a string or null. In streaming (SSE) mode, it does not arrive with the first event; instead, it is provided in the message_delta event, immediately before message_stop. This means the caller only gets the final value near the end of the stream — which is why caching or incremental reading requires explicitly storing this field.

A companion field is stop_sequence: when stop_reason is stop_sequence, this field returns the actual string that triggered the stop, making it easier for the caller to identify which stop condition was hit. The input_tokens and output_tokens in usage are used to diagnose max_tokens and context overflow, and the two are often consumed together. Understanding when these fields arrive and how they pair up is a prerequisite for correctly writing loop branches.

The Counterintuitive Core Design: Completion Doesn't Depend on stop_reason

Intuitively, the loop's exit condition might be written like this:

if stop_reason == 'end_turn':
    break
elif stop_reason == 'tool_use':
    execute_tools()

But Claude Code doesn't schedule this way. It checks the content of the assistant message directly: if the content array contains a tool_use block, it executes the tool; otherwise it exits the loop.

The reason is that in practice, stop_reason === 'tool_use' does not reliably reflect the message structure. A response might contain a real tool call block yet report end_turn, or report tool_use with no actual content block. Content is the only trustworthy signal source.

Here, the primary use of stop_reason is to drive error messages and recovery interactions — for example, explaining to the user that output was truncated or that the model refused to act — not to serve as the sole basis for dispatch decisions.

Item-by-Item Breakdown: The Handling Path for Each Stop Reason

end_turn and tool_use: The Normal Path

end_turn means the model completed normally, the content contains no tool call, and the loop takes the completion path, returning control to the user. tool_use doesn't rely on the tag itself — as long as there is a tool call block in the content, it executes, appends the result, and issues the next model call.

max_tokens: Recover First, Then Give Up

Hitting the output limit is treated as a recoverable error state. The handling steps are:

  1. First try raising the max_tokens limit and retrying;
  2. If it still hits the ceiling after escalation, append a user message such as [Output token limit hit, continue] to explicitly ask the model to continue the interrupted work;
  3. Allow at most three recovery attempts (MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3);
  4. If recovery still fails, surface the problem to the user.

From the perspective of the outer loop, this is not "done" — it is "not done but still recoverable by the system."

Context Overflow: Compress and Retry

When the context is too large, Claude Code triggers a reactive compaction path: it shrinks the message history and then retries. This is likewise a continue branch, not an immediate termination.

The context window refers to the maximum total number of tokens a model can accept in a single call, including system prompts, message history, tool definitions, and the current input — it is the combined ceiling for input and output. Once exceeded, the server cannot complete inference and will reject or truncate with model_context_window_exceeded. Claude Code's reactive compaction is exactly the handling for this failure: it typically keeps the system prompt and the most recent few turns of conversation, summarizes or discards earlier history, and thereby compresses the request back inside the window before retrying.

The difference between compaction and max_tokens recovery lies in what they target: max_tokens concerns the output-side ceiling, while compaction targets the input-side total. Both reduce available information, so recovery is not lossless — after compaction, the model may lose earlier details. This also explains why hard limits like maxTurns are needed: repeated compact-and-retry could in theory go on indefinitely.

refusal: Report and Stop

Refusals are different. Claude Code constructs an error message — typically suggesting the user try another model via /model — but does not automatically retry. A deliberate refusal by the model is unlikely to change by repeating the same request, so the loop ends here.

stop_sequence and pause_turn: Edge Cases

Claude Code usually does not configure custom stop sequences, so stop_sequence almost always falls into the content-check path, where there is generally no tool call and the turn completes normally.

pause_turn, on the other hand, is a genuine implementation gap. The SDK recognizes this value, but the Claude Code loop has no dedicated branch to handle it. The value is designed for very long or complex turns, whereas Claude Code's main loop typically involves only a few calls per user request — which may explain why explicit handling hasn't been needed yet. If long-running reasoning workflows are introduced in the future, this branch will need a clear policy.

Errors as Recovery Triggers: A Broader Design Principle

The handling of max_tokens and context overflow reveals an underlying design philosophy: as long as it's recoverable, an error should trigger recovery rather than termination. This principle runs through multiple scenarios:

  • max_tokens triggers token-limit escalation, an explicit continuation prompt, and retry;
  • Context overflow triggers compaction and retry;
  • Overload or rate limits can trigger model downgrades and retries;
  • Network failures can be retried with exponential backoff.

The goal is clear: fix what the system can fix, and only expose truly unrecoverable failures to the user. At the state-machine level, each recovery is just a different transition path within the current loop iteration.

maxTurns and Stop Hook: Two Gates Against Infinite Loops

With the recovery paths above, the loop could in theory run forever: the model repeatedly requests tools without progress, output-limit recovery hits the ceiling again after each escalation, and the model immediately overflows context again after compaction. Claude Code sets up two gates for this.

The first is maxTurns, which counts the number of model calls in a single user-initiated loop. Once the configured limit is exceeded, the system forces an exit: it appends [max_turns_reached] to the message history, the SDK returns a { subtype: 'error_max_turns' } result, and the user gets a clear notice that the loop stopped due to a safety limit. Agent execution can iterate many times without human intervention, and maxTurns ensures that lack of supervision doesn't turn into an infinite loop.

The second is the Stop hook. Even if the loop sees no tool call block and should have completed, the Stop hook still has the final veto. If it returns decision: block, the loop must not end and must run another iteration. This supports policies like "don't finish until the tests have run" or "don't finish until relevant changes are committed."

True completion requires passing three gates at once: no tool_use block in the content; a stop reason that doesn't require a recovery path; and no Stop hook blocking completion.

In Claude Code's context, a hook is an external script or command that users attach at specific points in the Agent lifecycle, invoked by the SDK at agreed-upon times within the loop, whose return value can influence the loop's direction. The Stop hook is bound to the moment when "the model requests no more tools and is about to finish," receives the current transcript, and returns JSON per convention — where a decision field of block means vetoing completion, and the accompanying reason is injected as a user message into the next iteration to guide the model to continue acting.

Alongside it are other hook points such as PreToolUse, PostToolUse, and UserPromptSubmit. What makes the Stop hook special is its position at the exit of the automatic loop: it can inject externally determinable policies like "did the tests pass" or "are the changes committed" into what was originally a purely model-driven termination condition, effectively acting as an extra human-programmable gate that, together with content checks and stop-reason checks, forms the necessary conditions for completion.

The Complete Turn-End Decision

Summing up all branches, the decision flow after each model call is:

if content contains a tool_use block:
    execute tools, append tool_results, and continue
elif stop_reason == 'max_tokens':
    recover output limit (escalate → inject continue → retry)
    give up after at most three recoveries
elif stop_reason == 'model_context_window_exceeded':
    run reactive compaction, then retry
elif stop_reason == 'refusal':
    create an error message and end the loop
elif turn_count > maxTurns:
    force an end via the max-turns safeguard
elif Stop hook returns block:
    prevent completion and run another iteration
else:
    complete the loop

The seven API values ultimately converge into five meaningful handling patterns. stop_sequence usually falls back to the content check, and pause_turn has no dedicated loop-layer handling in the version analyzed here. The real decision basis is the combination of four things: content, abnormal stop reasons, turn count, and hooks.

Practical Takeaways

For developers building Agent systems, this mechanism offers several transferable lessons. Completion decisions should be based on response content rather than metadata tags, because unreliable tags are the norm, not the exception. Error handling should distinguish recoverable from unrecoverable: the former deserves recovery logic, the latter should be surfaced quickly. A hard iteration cap is an essential safeguard for any Agent loop, no matter how refined the recovery logic. And the hook mechanism provides space for human policy to intervene by inserting custom rules at the end of the automatic loop.

The next article will unify the approval, hooks, parallel scheduling, stop-reason handling, and recovery mechanisms discussed in the previous four articles into a panoramic view of the QueryEngine main loop state machine.

One-line Summary

Claude Code decides whether the loop continues based on response content rather than stop_reason, and designs recovery paths for recoverable errors.

Paragraph Summary

This article dives into the Claude Code v2.1.220 source code to break down how the seven stop_reason values are actually handled in the Agent loop. The core finding: whether the loop continues does not depend on the stop_reason tag, but directly checks whether the assistant message content contains a tool_use block, because the tag and the actual structure may not match. max_tokens and context overflow are treated as recoverable errors, triggering token escalation plus continuation prompts and compaction-retry respectively, with output recovery limited to three attempts; refusal is the only stop reason that is not automatically retried. To prevent recovery paths from causing infinite loops, the system sets up two gates: maxTurns hard-caps the number of model calls, returning error_max_turns when exceeded; the Stop hook holds the final veto and can force another iteration. True completion requires passing all three checks: content, stop reason, and hooks.

#Claude Code#stop_reason#Agent loop#Anthropic API#maxTurns#Stop hook#context compaction
Share:

Related articles