PreToolUse deny (exit 2) silently unenforced under the Agent SDK — concrete impact on projects using it for security-critical guards
This adds a concrete-impact data point to the existing reports ofPreToolUse exit-2 denial not blocking tool execution in headless/SDK
sessions (#36071 and duplicates, all closed not-planned or stale).
We run a project that relies on PreToolUse hooks for genuinely
safety-critical guards — e.g. one that blocks a Write/Edit/Bash call from
landing in the wrong git worktree once a session has committed to working
in a specific one. In a Claude Agent SDK-hosted session (not the
interactive CLI), we confirmed:
- The hook fires and computes the correct
exit 2deny verdict (verified
by piping the identical JSON payload into the hook script by hand).
- The real tool call it was supposed to block succeeds anyway, with no
visible block, no error, and no indication to the agent or the operator
that enforcement was skipped.
- Non-blocking hook events in the same session (
PostToolUse,
SessionStart) fire normally — only the deny path is affected.
This matches the async-race root cause described in #36071: the tool
executes before the hook's deny decision arrives. The practical
consequence for anyone relying on PreToolUse for enforcement rather than
mere logging/notification is that the guard silently does nothing —
the failure is invisible from inside the session, since the hook appears
to have run.
Given #36071 was closed by the stale-issue bot rather than a maintainer
decision, we wanted to re-surface this with the specific enforcement-guard
angle, since "hooks are slightly racy in headless mode" and "a documented
safety mechanism silently does nothing under the SDK, with no error
surfaced" are different severities. Is there a supported way (acanUseTool callback, a permissionMode, or similar) for a headless SDK
host with no interactive terminal to get synchronous enforcement today? If
not, is that considered a gap worth reopening, or is git-level enforcement
(pre-commit/pre-push, confirmed unaffected) the intended answer for this
case?
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗