You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@deepseek-ai/dsh-hooks-claude-code currently lets a blocking Stop hook force another model request without tracking consecutive blocks. An unconditional hook can therefore keep the same turn alive indefinitely, consuming model requests and tokens until some external limit intervenes. The payload also always reports stop_hook_active: false, so a hook cannot detect that it is running because of its own prior block.
Claude Code's documented behavior marks Stop-hook re-entry with stop_hook_active and overrides the hook after eight consecutive blocks by default. I reproduced the mismatch against official master and prepared a tested fix in a public fork. Since this repository does not expose external pull-request creation, I am sharing the complete patch here according to the repository's contribution workflow.
Reproduction
Configure a Stop command hook that always exits with code 2:
#!/usr/bin/env bashecho"keep working">&2exit 2
Run an agent with nine otherwise-completing mock model responses, then assert that the bridge performs at most nine requests: the initial request plus eight forced continuations.
On official master, the regression failed because the bridge attempted a tenth request and stopped only when the mock response script was exhausted:
AssertionError: expected [ ...10 requests... ] to have a length of 9 but got 10
The focused failing command was:
pnpm exec vitest run packages/hooks/hooks-claude-code/tests/coverage-stop.spec.ts -t 'caps an unconditional Stop hook'
Root cause
The agent/turn-stopping listener runs the Stop point and calls agent.steer() after every blocking result, but it retains no per-agent or per-turn continuation state. stopPayload() also hardcodes stop_hook_active: false. Consequently, each forced stopping boundary is indistinguishable from the first one and no block cap can be enforced.
Proposed fix
The patch:
tracks consecutive blocking Stop results in a WeakMap, scoped to the exact agent and open turn;
sends stop_hook_active: false on the first attempt and true on re-entry;
resets the chain after a non-blocking result or a new turn;
allows the turn to stop after eight consecutive blocks by default and records a warning;
adds a validated positive-integer stopHookBlockCap plugin setting for deployments that need a different bound.
The configuration difference is explicit: this bridge uses stopHookBlockCap; it does not claim to consume Claude Code's CLAUDE_CODE_STOP_HOOK_BLOCK_CAP environment variable. The cap warning is currently logger-visible rather than injected into user chat.
Verification
New regression coverage verifies:
an unconditional Stop hook runs eight times and the turn completes after nine model requests;
re-entry payloads are [false, true] and a later user turn resets to [false, true];
a configured cap of 1 is honored;
zero, negative, fractional, and NaN cap values fail at load.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
@deepseek-ai/dsh-hooks-claude-codecurrently lets a blockingStophook force another model request without tracking consecutive blocks. An unconditional hook can therefore keep the same turn alive indefinitely, consuming model requests and tokens until some external limit intervenes. The payload also always reportsstop_hook_active: false, so a hook cannot detect that it is running because of its own prior block.Claude Code's documented behavior marks Stop-hook re-entry with
stop_hook_activeand overrides the hook after eight consecutive blocks by default. I reproduced the mismatch against officialmasterand prepared a tested fix in a public fork. Since this repository does not expose external pull-request creation, I am sharing the complete patch here according to the repository's contribution workflow.Reproduction
Configure a
Stopcommand hook that always exits with code 2:Run an agent with nine otherwise-completing mock model responses, then assert that the bridge performs at most nine requests: the initial request plus eight forced continuations.
On official
master, the regression failed because the bridge attempted a tenth request and stopped only when the mock response script was exhausted:The focused failing command was:
Root cause
The
agent/turn-stoppinglistener runs theStoppoint and callsagent.steer()after every blocking result, but it retains no per-agent or per-turn continuation state.stopPayload()also hardcodesstop_hook_active: false. Consequently, each forced stopping boundary is indistinguishable from the first one and no block cap can be enforced.Proposed fix
The patch:
WeakMap, scoped to the exact agent and open turn;stop_hook_active: falseon the first attempt andtrueon re-entry;stopHookBlockCapplugin setting for deployments that need a different bound.The configuration difference is explicit: this bridge uses
stopHookBlockCap; it does not claim to consume Claude Code'sCLAUDE_CODE_STOP_HOOK_BLOCK_CAPenvironment variable. The cap warning is currently logger-visible rather than injected into user chat.Verification
New regression coverage verifies:
[false, true]and a later user turn resets to[false, true];NaNcap values fail at load.Full validation completed successfully:
Patch
Lzb-gzist:agent/hooks-stop-loop-guardThe behavior is based on Claude Code's official hook documentation: https://code.claude.com/docs/en/hooks
All reactions