[Bug] Stop hook repeatedly triggers with "full milestone completion" demand in loop sessions

Status Open
Reported on v2.1.281
Maintainer reply None cached
Activity 3 comments · opened Sep 28, 2026

Bug Description

│ │ What happened: In a long autonomous /loop-style session (goal: complete a project milestone), the Stop hook fired ~10 times in a row with near-identical fee…Stop-hook keeps demanding "full milestone completion" despite repeated, verified evi…

Environment Info

  • Platform: darwin
  • Terminal: Superconductor
  • Version: 2.1.281
  • Feedback ID: cd4c2311-63d1-420b-910a-2c69eb220ad9

Errors

[{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)","timestamp":"2026-09-25T00:58:59.590Z"},{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)","timestamp":"2026-09-25T00:59:00.580Z"},{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)","timestamp":"2026-09-25T01:02:17.859Z"},{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)","timestamp":"2026-09-25T14:24:27.330Z"},{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)","timestamp":"2026-09-26T04:11:10.878Z"},{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74061)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)","timestamp":"2026-09-26T04:11:11.080Z"},{"error":"Error: Tool bash not found\n    at se (/$bunfs/root/chunk-vdm4xd3c.js:19:13554)\n    at ir (/$bunfs/root/chunk-vnnj4fsp.js:18:61317)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74081)\n    at In (/$bunfs/root/chunk-vnnj4fsp.js:18:73965)\n    at Fh (/$bunfs/root/chunk-vnnj4fsp.js:18:74850)\n …

Note: Content was truncated.

View original on GitHub ↗

3 Comments

faizjamal0111-svg · 16 days ago

If this is your own Stop hook, or one from a plugin that runs a milestone check, the input it receives has a field made for exactly this loop: stop_hook_active is true when Claude Code is already continuing because a stop hook blocked. A check that can't be satisfied should look at it and let the turn end:

import json, sys

event = json.load(sys.stdin)
if event.get("stop_hook_active"):
    sys.exit(0)  # already pushed back once; don't demand it again

Claude Code also caps this at 8 consecutive continuations and then overrides the next block (CLAUDE_CODE_STOP_HOOK_BLOCK_CAP changes the number), which is likely why it ended after around that many rounds instead of looping forever.

The other half: a Stop hook that demands "full milestone completion" will loop whenever the model can't prove completion in a form the hook recognises. It holds up better if the hook checks something mechanical (tests pass, a file exists, a command exits 0) and returns that specific failure as the reason, instead of a general demand the model can only answer with more words.

musichen · 16 days ago

@faizjamal0111-svg Thanks, this is a good, precise fix and matches exactly what we hit. Worth naming the actual source, since it's not a custom hook: it's the bundled ralph-loop@claude-plugins-official plugin (v1.0.0), invoked via /ralph-loop, running in a Superconductor-managed Claude Code terminal session.

Trail, for anyone else chasing this:

  • ~/.claude/settings.json -> enabledPlugins["ralph-loop@claude-plugins-official"]: true, Stop hook chain resolves to agent-deck hook-handler plus the plugin's own hook.
  • Plugin's hook registration: ~/.claude/plugins/cache/claude-plugins-official/ralph-loop/1.0.0/hooks/hooks.json -> Stop -> bash "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh".
  • The actual logic: ~/.claude/plugins/cache/claude-plugins-official/ralph-loop/1.0.0/hooks/stop-hook.sh.

Two things stood out reading it against your explanation:

  1. No stop_hook_active check anywhere in the script. It reads HOOK_INPUT=$(cat) at the top, pulls session_id out of it for session isolation, but never inspects stop_hook_active. Every invocation is treated as a first block, so it always re-emits {"decision":"block","reason":$prompt} unconditionally whenever the promise tag doesn't match, with no way to notice it already pushed back.
  2. The completion check is a literal string match, not a mechanical one:

``bash
PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; ...')
if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
# only then does it let the turn end
`
So "done" means the model reproduced the exact configured string inside a
<promise>` tag, not "a test passed" or "a file exists." With a generic promise like "full milestone completion," that's the self-report problem you flagged, just built into the officially bundled plugin rather than a user's own hook.

What actually ended our loop was CLAUDE_CODE_STOP_HOOK_BLOCK_CAP (default 8, per your comment), not the script's own logic - max_iterations in the loop's state file was set past that.

Given this is the officially distributed plugin, seems worth patching stop-hook.sh itself: an early stop_hook_active exit, and maybe steering /ralph-loop's docs toward mechanical completion promises (a command exit code, a file check) instead of a prose tag the model self-reports.

faizjamal0111-svg · 16 days ago

Thanks for tracing it to the plugin. One correction to my own advice, now that we know it's ralph-loop: an early stop_hook_active exit would end every Ralph loop after one extra pass, because blocking the stop is how the plugin loops. So I wouldn't patch that in as is.

What works with the plugin as it stands:

  • Pass --max-iterations N to /ralph-loop with an N you choose. stop-hook.sh then ends the loop itself when it gets there, instead of the harness cutting it off.
  • Make the completion promise a short token tied to a check, for example telling it in the prompt to output <promise>TESTS_GREEN</promise> only after npm test exits 0, rather than a phrase the model has to judge for itself.
  • If you want loops longer than the block cap, raise CLAUDE_CODE_STOP_HOOK_BLOCK_CAP on purpose.

The same issue is open upstream as anthropics/claude-plugins-official#5312. Your trail would be a useful second report there, especially that the block cap ended the loop before max_iterations did.