PreToolUse deny (exit 2) silently unenforced under the Agent SDK — concrete impact on projects using it for security-critical guards

Status Open
Maintainer reply None cached
Activity 1 comment · opened Aug 29, 2026

This adds a concrete-impact data point to the existing reports of
PreToolUse 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 2 deny 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 (a
canUseTool 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?

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗