PostToolUse hook `continue: false` silently ignored via Agent SDK control protocol

Resolved 💬 3 comments Opened Mar 2, 2026 by basher83 Closed Mar 5, 2026

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

  1. 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
  1. The hook fires. Debug output confirms the return value is {"continue_": False, "stopReason": "..."}.
  1. The SDK's _convert_hook_output_for_cli (query.py:34) correctly converts continue_ to continue.
  1. 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"
    }
  }
}
  1. 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:

  1. CLI sends control_request (subtype: hook_callback) to SDK via stdout
  2. Query._handle_control_request dispatches to hook callback (query.py:285)
  3. Python hook function runs, returns {"continue_": False, ...}
  4. _convert_hook_output_for_cli converts to {"continue": false, ...} (query.py:34)
  5. Wrapped as SDKControlResponse and written to CLI stdin (query.py:322-330)
  6. 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: true ignored (auto-closed by stale bot, not fixed)
  • #4669 — permissionDecision: "deny" in PreToolUse hooks ignored (auto-closed)
  • #27314 — PostToolUse systemMessage silently 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

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗