[BUG] PreToolUse hook permission decisions silently discarded when returned in flat format (no hookSpecificOutput wrapper)

Resolved 💬 3 comments Opened Apr 15, 2026 by Todd-ChromedomeConsulting Closed Apr 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code's hook output parser at cli.js line 8045 only reads hookSpecificOutput.permissionDecision for PreToolUse permission decisions. Hook scripts that return the flat top-level format {"permissionDecision": "allow"} have their decisions silently ignored — the hook runs, exits 0, logs its decision, and produces no visible failure, but the permission subsystem never receives the verdict.

The failure is invisible at every observable layer. A user who built a PreToolUse gatekeeper hook with the flat format will see the hook running on every tool call, exit code 0, log entries accumulating as expected — and no warning, no deprecation notice, no telemetry event. And yet every permission decision the hook makes is discarded. The user sees every permission prompt the gatekeeper was designed to suppress and reasonably attributes the approval tax to normal Claude Code behavior. In my case, the bug went undetected for months until a deep-read of cli.js revealed the format mismatch.

Any user who built a PreToolUse permission hook with the flat format is silently running without gatekeeper protection. The fix on the hook side is trivial (~6 lines to wrap the output in hookSpecificOutput with hookEventName: "PreToolUse"), but users cannot apply the fix if they don't know the bug exists.

What Should Happen?

Either (a) the parser accepts both formats (flat and wrapped) for backward compatibility, or (b) Claude Code emits a visible warning (stderr, telemetry, or a one-time notice in the session) when a hook returns a recognized permission-decision field at the wrong nesting level, so users can discover the format issue before spending months with a silently-disabled gatekeeper.

If the hookSpecificOutput wrapper is mandatory, the hook development docs should state this explicitly with a prominent example of the correct shape. The flat format is a natural shape for a hook author to reach for.

Error Messages/Logs

(No error messages — that is the bug. The hook exits 0 with no output, no warnings, no telemetry events, and no indication that the permission decision was discarded. The only observable signal is that permission prompts continue to fire for tool calls the gatekeeper allowed.)

Steps to Reproduce

Create a PreToolUse hook script (shell or Python) that outputs the flat JSON format to stdout:

{"permissionDecision": "allow", "permissionDecisionReason": "testing"}
Configure the hook in settings.json under hooks.PreToolUse:

{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "bash /path/to/hook.sh"
}
]
}
]
}
}
Trigger a tool call that would normally prompt for approval (for example, any Bash command not in the allow list).

Observe: the approval prompt fires. The hook has already decided "allow" and logged its decision, but the permission subsystem did not receive the verdict.

Rewrite the hook output to use the wrapped format:

{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": "testing"}}
Re-trigger the same tool call. The approval prompt no longer fires. The hook decision now reaches the permission subsystem.

The difference between steps 4 and 6 is the only change that makes the gatekeeper functional.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.109

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Architecture: arm64 (Apple Silicon) Node.js: v25.6.1 Install path: /opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js Shell: zsh

Context for the investigation:

This bug was discovered during a structured reliability investigation of my Claude Code workflow. I spent approximately 10-15 hours across multiple sessions diagnosing what I thought was baseline approval friction, before a dedicated deep-read of cli.js revealed that my PreToolUse gatekeeper hook had been emitting the wrong output format since it was built and that every classification decision it made was being silently discarded by the parser at line 8045.

The primary code reference is cli.js line 8045, where the hook output parser reads hookSpecificOutput.permissionDecision and does not fall back to the top-level permissionDecision field. A parallel research finding is that PreToolUse hook decisions short-circuit the normal permission check (including the expansion obfuscation classifier at cli.js L1870 function C68), which means a working gatekeeper would have eliminated most of the approval prompts I experienced. The silently-disabled gatekeeper meant I was paying friction the gatekeeper was specifically designed to eliminate.

I am also filing a separate bug report on hardcoded Bash safety checks that fire on legitimate research workflows. The two bugs interact: with a silently-disabled gatekeeper, there is no intermediate enforcement layer that could absorb the hardcoded check failures, so the user experiences the maximum possible friction.

View original on GitHub ↗

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