PostToolUse hook `continue: false` silently ignored via Agent SDK control protocol
Summary
PostToolUse hooks returning continue: false via the Agent SDK (Python) control protocol are silently ignored by the CLI. The hook fires, the SDK correctly converts continue_ to continue and sends it back to the CLI subprocess via stdin, but the CLI does not terminate execution. No error, no warning, no indication that the signal was dropped.
This affects any Agent SDK consumer that relies on PostToolUse hooks to enforce runtime invariants (context budget limits, critical failure early termination, etc.).
Environment
- claude-agent-sdk (Python): >=0.1.40
- Claude Code CLI: tested across multiple recent versions
- macOS (Darwin 25.3.0)
Steps to Reproduce
- Register a PostToolUse hook via the Python Agent SDK that returns
{"continue_": False, "stopReason": "Budget exceeded"}:
from claude_agent_sdk import ClaudeSDKClient, HookMatcher
async def budget_hook(input_data, tool_use_id, context):
# Some condition check...
return {
"continue_": False,
"stopReason": "Context budget exceeded",
}
async with ClaudeSDKClient(model="claude-haiku-4-5-20251001") as client:
await client.send_prompt(
prompt="Create a file",
hooks={
"PostToolUse": [
HookMatcher(matcher=None, hooks=[budget_hook]),
],
},
)
async for msg in client.receive_response():
print(msg) # Execution continues — never terminates
- The hook fires. Debug output confirms the return value is
{"continue_": False, "stopReason": "..."}.
- The SDK's
_convert_hook_output_for_cli(query.py:34) correctly convertscontinue_tocontinue.
- The response is sent to the CLI subprocess via stdin as:
{
"type": "control_response",
"response": {
"subtype": "success",
"request_id": "req_N_xxxx",
"response": {
"continue": false,
"stopReason": "Context budget exceeded"
}
}
}
- The CLI subprocess receives it and continues execution. The agent loop does not terminate.
Expected Behavior
The CLI should honor continue: false from PostToolUse hooks and terminate the agent loop, consistent with the hooks documentation:
continue (default: true) — If false, Claude stops processing entirely after the hook runs. Takes precedence over any event-specific decision fields.
Actual Behavior
The CLI silently ignores continue: false. Execution continues as if the hook returned {}. No error, no log entry, no indication the signal was received and dropped.
SDK Code Path Verified
The Python SDK side is correct. Traced the full path:
- CLI sends
control_request(subtype:hook_callback) to SDK via stdout Query._handle_control_requestdispatches to hook callback (query.py:285)- Python hook function runs, returns
{"continue_": False, ...} _convert_hook_output_for_cliconverts to{"continue": false, ...}(query.py:34)- Wrapped as
SDKControlResponseand written to CLI stdin (query.py:322-330) - CLI receives the response — what happens next is inside the CLI binary, where the signal is dropped
Workaround
Use a shared mutable dict between the hook closure and the message loop. The hook sets a flag; the caller checks it on each received message and breaks out of receive_response():
shared_state = {"budget_exceeded": False}
async def budget_hook(input_data, tool_use_id, context):
if over_budget():
shared_state["budget_exceeded"] = True # Side-channel flag
return {"continue_": False, "stopReason": "Budget exceeded"} # Hopeful
return {}
async for msg in client.receive_response():
if shared_state["budget_exceeded"]:
break # Reliable termination from Python side
This works but moves enforcement out of the hook layer into harness orchestration, which is a less clean separation of concerns.
Related Issues
- #3514 — PreToolUse hooks with
continue: false/preventContinuation: trueignored (auto-closed by stale bot, not fixed) - #4669 —
permissionDecision: "deny"in PreToolUse hooks ignored (auto-closed) - #27314 — PostToolUse
systemMessagesilently dropped for async hooks (open) - #27244 — Feature request for hook-based session termination exit code (open)
- claude-agent-sdk-python#265 — Hook denial causes API Error 400
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗