[BUG] Stop hook returning {"ok": false} causes infinite loop until session limit (consumed full session, ~50 min)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
A user-defined Stop hook that returns {"ok": false, "reason": "..."} triggers an infinite feedback loop when Claude is legitimately waiting on an asynchronous background agent (a subagent launched with run_in_background: true).
In my case the loop ran for ~50 minutes and consumed my entire session quota before the external async event finally fired and broke the cycle.
Why the loop is unbreakable from Claude's side:
- The active skill (
/testRunner) explicitly forbids tool calls while waiting for background agents to complete — Claude must stay idle until automatic completion notifications arrive. - After every assistant turn, the
Stophook fires, evaluates that the user's master task ("summarize all 4 test suites") is incomplete, and returns{"ok": false}. - Claude is forced to produce another turn — but cannot poll, cannot invoke any tool, and cannot satisfy the hook's completion criterion until the external notification lands.
- Every text-only response the model produces is graded as "incomplete" by the hook → the loop never ends until the async event arrives (or the session token cap is hit).
This is the same family as #3573, #10205 and #20221, but specifically triggered by the interaction between user-defined Stop hooks and async background subagents.
The hook used a reason payload in Spanish that re-described the pending task each iteration, which the model was tempted to summarize again — making each turn structurally identical to the previous one and the loop completely deterministic.
What Should Happen?
The Stop-hook subsystem should provide at least one of these guardrails:
- Honor
stop_hook_activeautomatically. Once Claude has been forced to continue once due to a Stop hook, subsequent Stop hooks within the same logical task should not be allowed to indefinitely re-block the same response cycle. The flag exists in the docs; it should be enforced by the harness, not left to user-hook implementers. - Hard cap on Stop-hook continuation iterations per task (e.g. 5 or 10). After N forced continuations on the same incomplete state, the harness should override the hook and let the turn end.
- Async-agent awareness in the hook context. Expose to the hook the list of pending background subagents (their IDs and statuses) so user hooks can short-circuit and return
ok: truewhile async work is in flight, instead of grading the turn as incomplete. - Skill-side opt-out. Skills designed as async multi-turn (like
/testRunnerhere) should be able to declare "Stop-hook-exempt while awaiting notifications" so the harness skips the hook for those waiting turns.
At minimum: the session token cap should never be reachable purely through Stop-hook-induced loops. That is a billing/UX failure mode.
Error Messages/Logs
No stack trace — the loop is functionally silent. Each iteration looked like this in the transcript:
assistant: "Esperando e2e."
[Stop hook fires]
hook -> {"ok": false, "reason": "El usuario solicitó /testRunner ... falta el resultado del backend e2e ... La tarea solicitada no está completa."}
assistant: "Esperando e2e."
[Stop hook fires]
... (repeated >100 times over ~50 min) ...
[task-notification] backend e2e completed -> loop finally breaks
Steps to Reproduce
- Configure a
Stophook in~/.claude/settings.jsonthat evaluates whether the user's request is fully answered, and returns{"ok": false, "reason": "<description>"}whenever it judges the task incomplete. The hook does NOT checkstop_hook_active. - Define a custom skill that is async-by-design — it launches several subagents with
run_in_background: true, and instructs Claude to remain idle (no polling, no tool calls) until ALL completion notifications arrive, then summarize. - Invoke the skill so that at least one of the background agents takes a long time (in my case the e2e suite hits real Gmail/Outlook APIs — normally ~2 min, but the Stop-hook loop kept the session alive ~50 min).
- Observe: each turn Claude produces a brief idle-status text. The Stop hook grades it as incomplete and forces another turn. Loop continues indefinitely until either:
- the slowest background agent finally completes and the model can produce the consolidated summary the hook is waiting for, OR
- the session token cap is reached.
In my case option (a) eventually happened, but only after the hook had already burned through the bulk of my session quota.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.126 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
Account email: muelonmuelon12@gmail.com
Date of incident: 2026-05-03
Approximate loop duration: ~50 minutes
Related already-reported issues (this is the same class but with a fresh root-cause flavor — async subagent waiting, not GitHub Actions or worktree):
- #3573 — Claude Code GitHub Actions integration gets stuck in infinite loop when a Stop hook fails
- #10205 — Claude Code enters infinite loop when hooks are enabled
- #20221 — Prompt-based SubagentStop hooks send feedback but don't prevent termination
Why I'm filing this as a new issue rather than commenting on the above:
The failure mode here is specifically the interaction between a user Stop hook and an asynchronous background subagent that the active skill is waiting on — none of the linked issues cover this combination. The fix space (async-agent-aware hooks, iteration cap, automatic stop_hook_active enforcement) is broader than any single existing report.
Billing impact:
The entire session quota was consumed by the loop. Filing a separate request via support@anthropic.com for credits, but flagging here because the bug itself is what made the billing event unavoidable.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗