Summary
Even with route-dispatch.sh's PreToolUse:Agent gate working correctly and returning a blocking response, Claude Code's own harness resolves that hook's result after the subagent tool call has already dispatched and completed — making it structurally impossible for the hook to block anything. This is a Claude Code CLI (v2.1.220) timing behavior, not a bug in this plugin's scripts, but it directly undermines the enforcement route-dispatch.sh (and any other blocking PreToolUse:Agent/PreToolUse:Task hook) is meant to provide.
Related to #80 (which covers a separate, plugin-side bash bug in the same script) but distinct: even after that bug is fixed, this harness-timing issue independently means a correctly-computed block decision arrives too late to matter.
Evidence — real production timestamps, ~/.claude/debug/<session>.txt
Captured live via claude --debug hooks, one full Agent-tool dispatch cycle:
21:18:05.229Z Registering async hook async_hook_34311 (PreToolUse:Agent) with timeout 5000ms
21:18:05.318Z [Stall] tool_dispatch_start tool=Agent toolUseId=toolu_011vd36gY3jspxkT73nW5YrS
21:18:05.400Z [API:timing] dispatching to firstParty model=copilot/standard
21:18:05.401Z [API REQUEST] /v1/messages source=agent:builtin:general-purpose
21:18:08.606Z Stream started - received first chunk (first byte after 3207ms)
21:18:08.691Z [Stall] agent_completion ... durationMs=3370 turns=1 finalStopReason=end_turn
21:18:08.691Z [Stall] tool_dispatch_end tool=Agent toolUseId=toolu_011vd36gY3jspxkT73nW5YrS outcome=ok durationMs=3373
21:18:08.830Z (~139ms AFTER tool_dispatch_end) Checking hook async_hook_34311 (PreToolUse:Agent) - stdout length: 3
21:18:08.831Z Found sync response from async_hook_34311: {}
21:18:08.831Z Hooks: async_hook_34311 (PreToolUse:Agent) produced no response payload — skipping attachment
The subagent's model call was dispatched at 05.400Z and had already fully completed (tool_dispatch_end) at 08.691Z — before Claude Code even checks async_hook_34311's stdout at 08.830Z. The hook's own registration timeout (5000ms) is irrelevant to this: the tool dispatch doesn't wait on the hook resolving at all; the hook is checked as a separate, later bookkeeping step.
Why this matters here specifically
route-dispatch.sh (this repo) is designed as a PreToolUse:Agent/PreToolUse:Task blocking gate — its own header describes it as "a wall, not advice," meant to exit 2 + stderr to block a subagent dispatch that omits an explicit model. Per the above timing, even a correctly-firing exit 2 cannot block dispatch — by the time Claude Code reads that exit code, the subagent has already run to completion against whatever model it inherited.
This means the entire class of "block a subagent tool call based on its declared inputs" hooks is currently unenforceable via PreToolUse:Agent/PreToolUse:Task on Claude Code 2.1.220, independent of anything in this plugin's own scripts.
Scope of this issue
This is very likely a Claude Code CLI issue rather than something fixable in ruvnet-brain's source — filing here to (a) document it for anyone else debugging why their blocking PreToolUse:Agent hooks appear to have no effect, and (b) track that it should also be reported to Anthropic directly, since the actual fix (checking hook results before or during dispatch, not ~140ms after completion) lives in their CLI.
Related
Summary
Even with
route-dispatch.sh'sPreToolUse:Agentgate working correctly and returning a blocking response, Claude Code's own harness resolves that hook's result after the subagent tool call has already dispatched and completed — making it structurally impossible for the hook to block anything. This is a Claude Code CLI (v2.1.220) timing behavior, not a bug in this plugin's scripts, but it directly undermines the enforcementroute-dispatch.sh(and any other blockingPreToolUse:Agent/PreToolUse:Taskhook) is meant to provide.Related to #80 (which covers a separate, plugin-side bash bug in the same script) but distinct: even after that bug is fixed, this harness-timing issue independently means a correctly-computed block decision arrives too late to matter.
Evidence — real production timestamps,
~/.claude/debug/<session>.txtCaptured live via
claude --debug hooks, one full Agent-tool dispatch cycle:The subagent's model call was dispatched at
05.400Zand had already fully completed (tool_dispatch_end) at08.691Z— before Claude Code even checksasync_hook_34311's stdout at08.830Z. The hook's own registration timeout (5000ms) is irrelevant to this: the tool dispatch doesn't wait on the hook resolving at all; the hook is checked as a separate, later bookkeeping step.Why this matters here specifically
route-dispatch.sh(this repo) is designed as aPreToolUse:Agent/PreToolUse:Taskblocking gate — its own header describes it as "a wall, not advice," meant to exit 2 + stderr to block a subagent dispatch that omits an explicitmodel. Per the above timing, even a correctly-firingexit 2cannot block dispatch — by the time Claude Code reads that exit code, the subagent has already run to completion against whatever model it inherited.This means the entire class of "block a subagent tool call based on its declared inputs" hooks is currently unenforceable via
PreToolUse:Agent/PreToolUse:Taskon Claude Code 2.1.220, independent of anything in this plugin's own scripts.Scope of this issue
This is very likely a Claude Code CLI issue rather than something fixable in
ruvnet-brain's source — filing here to (a) document it for anyone else debugging why their blockingPreToolUse:Agenthooks appear to have no effect, and (b) track that it should also be reported to Anthropic directly, since the actual fix (checking hook results before or during dispatch, not ~140ms after completion) lives in their CLI.Related
route-dispatch.sh/verify-interface.sh) that independently prevents this same hook from ever reaching its blocking branch in practice. Both issues need fixing forroute-dispatch.sh's gate to actually work end-to-end: [bug] hook-shim.mjs never pipes the PreToolUse payload to blocking hooks except ground-ruvnet — route-dispatch/ground-before-write/design-wall/protect-brain-state all silently no-op #80's fix makes the hook compute the right decision; this issue is about whether that decision can arrive in time to matter at all.