Bible Network Crypto DeFi Onchain RWA AI Agent Stablecoin Chain SAFU CryptoTax DeFAI AGI Claude Me Claude Skill Claude Design Claude Cowork
Independent Media
Not affiliated with any project
Learn Claude Skills. Do Everything Better.
claudeskill-me.com
LATEST
Your Hook Says "Blocking Error" But the File Still Changed? PostToolUse and PreToolUse Don't Actually Block the Same Way  ·  Enabled Auto Mode and Thought You Were Safe? Permission Modes and Sandbox Boundaries Are Two Different Layers — Conflating Them Is How Things Break  ·  Messages API Adds On-Demand Compaction in Beta: Developers Decide When to Compact, Not the System  ·  Claude Cowork and Chat Officially Merge, Launching Claude Docs and Claude Slides Alongside It  ·  Why a Single "hi" Can Burn 20,000+ Tokens in Claude Code: Breaking Down the Fixed Startup Overhead  ·  How to Write Your First Custom Slash Command in Claude Code: A Working Example From Scratch
practice

Your Hook Says "Blocking Error" But the File Still Changed? PostToolUse and PreToolUse Don't Actually Block the Same Way

30-Second Version · For the impatient
An error message on screen can only tell you Claude received a signal worth attending to — it can't tell you the risk has been eliminated.

Full Explanation +
01 · Why did this happen?

If my use case is genuinely just leaving a record or warning after the fact rather than truly blocking the operation, is this PostToolUse behavior a bug?

Strictly speaking, no — it's closer to misleading interface text than a functional error. The reporter's own framing agrees: they weren't asking for the blocking mechanism itself to be fixed, but suggesting the displayed text change from "blocking error" to something more accurate like "hook warning" or "hook reported error." If your use case is genuinely after-the-fact logging or notification, PostToolUse's current behavior already fits your needs — it's just that the wording on screen makes it easy to misjudge as "already blocked."

02 · What is the mechanism?

If even PreToolUse has documented failure cases like issues #23284, #13744, and #80039, can hooks be trusted for real security gating at all?

These cases do point to something worth taking seriously: hooks shouldn't be treated as the sole line of defense, especially for security-related blocking needs. The more practical approach is to treat the hook as one layer within a multi-layered defense, paired with other mechanisms like sandbox-level permission restrictions, so that if one layer fails, others are still in place.

The more critical habit is this: any blocking rule you write should be triggered once after deployment to verify it, confirming with your own eyes that the target operation genuinely didn't happen — rather than treating the mere appearance of an error message in the terminal as proof of verification. The common thread across all three reported cases is "exit 2 did fire, but the operation still went through" — that gap can only be caught through actual testing.

03 · How does it affect me?

Between exit codes and JSON permissionDecision output, which should I use in practice?

The official documentation's priority rule already answers part of this: on blockable event types, exit 2 takes priority over a simultaneous JSON permissionDecision: "allow" output. That makes the exit code the more direct and harder-to-override signal.

If your hook logic is simple — just needing "allow" or "Block" as outcomes — a bare exit code (exit 0 to pass, exit 2 to block) is usually sufficient and less error-prone. JSON output's value lies in expressing more nuanced information (like attaching a message for Claude to read), but if you use both output styles together, remember the exit code takes priority — debug by checking the exit code first, not just whether your JSON logic looks correct.

04 · What should I do?

I'm not an engineer — I just copy-pasted a hook configuration example from the internet. How do I check whether it's actually working?

Three checks that don't require reading the code in detail: first, open the config file and confirm the event name says PreToolUse and not PostToolUse — if your goal is to "prevent" an action but the example uses PostToolUse, blocking is mechanically impossible regardless of anything else. Second, find the exit code number in the blocking logic and confirm it's 2, not 1 — this is the most common source of copy-paste mistakes. Third, and most important: deliberately trigger the exact scenario the rule is meant to Block, then actually open the affected file or check the command's result to confirm with your own eyes whether it was genuinely affected — don't treat the mere appearance of an error message in the terminal as proof of verification.

Full Content +

You wrote a hook on the PostToolUse event that returns exit 2 whenever it detects a file that shouldn't have been touched. The terminal prints a red line: "blocking error." You breathe a sigh of relief, assuming the edit just got blocked — then you open the file and the content is already changed. This isn't a misconfiguration on your part. It's that the PostToolUse event type was never capable of blocking anything in the first place.

This gap has already been reported as a clear GitHub issue (#19009): a user's PostToolUse hook returned exit 2 on an Edit operation, and the interface did display "blocking error" text — but that edit had already been written to disk. The reporter's suggestion is direct: rather than let users assume the message means the action was blocked, the label should read "hook warning" or "hook reported error" instead, since that more accurately reflects what actually happened.

The problem isn't a misused exit code — it's that the event fires too late to matter

To understand why this happens, you need to separate PreToolUse and PostToolUse by their execution timing. PreToolUse fires before the tool actually runs. At that point, Claude hasn't touched the filesystem or any external state yet, so exit 2 genuinely means something here — it can signal Claude to abandon the operation entirely before it happens.

PostToolUse fires at a completely different point — the tool has already run, the file has already been written, the command has already been sent, the side effect has already occurred. Returning exit 2 at this point can only relay the stderr content back to Claude, letting it know "something here may have been wrong" — there's no mechanism to automatically roll back content that's already on disk. The "blocking error" label is actually describing "this message was flagged as something Claude needs to attend to," not "this action was intercepted."

Even PreToolUse isn't foolproof

If your next thought is "then I'll just move all my blocking logic to PreToolUse," there are a few documented cases worth knowing first. Issue #23284 reports a PreToolUse hook configured with a Bash matcher for git push commands — even after returning exit 2, the push still went through. Issue #13744 points to a different gap: the same PreToolUse event, the same exit 2, worked correctly against the Bash tool but failed to actually stop Write/Edit tool operations.

Trickier still is issue #80039, which is Windows-specific: a particular command format involving nested-quote parsing causes both exit 2 handling and stderr surfacing to fail simultaneously, while other hooks fire correctly within the same session. That points to the failure living in a specific command-parsing path rather than a blanket failure of the hook mechanism on that platform — so diagnosing it means checking whether that particular command format triggers a parsing issue on that platform, not just whether "hooks work on this platform" in general.

Another common, lower-level mistake: confusing exit 1 with exit 2

Before diving into platform or event-type nuances, it's worth ruling out a more common — and more self-inflicted — issue first. The community writeup "5 Claude Code Hook Mistakes That Break Your Automation" (by yurukusa) documents a frequently occurring slip: writing blocking logic that was meant to use exit 2 as exit 1 instead. exit 1 only logs the error — it produces no actual blocking effect whatsoever. If your hook is meant to stop an operation but uses exit 1, blocking simply won't happen, regardless of whether the event type was chosen correctly or not.

Exit codes versus JSON output — which is more reliable

The official docs (code.claude.com/docs/en/hooks) offer a structured alternative: a hook can return JSON via hookSpecificOutput, including a permissionDecision field that expresses "allow" or "deny" for the operation, rather than relying purely on a bare exit code. But the same documentation is explicit about priority — on event types that can genuinely Block, exit 2 takes priority over a simultaneous JSON permissionDecision: "allow" output. In other words, even if your JSON logic decides to allow the operation, returning exit code 2 still causes Claude to treat it as something requiring blocking. That priority rule itself is simple, but it also means that if a hook mixes both output styles, debugging should start by checking what the exit code actually returned, not just whether the JSON logic looks correct.

Treat hooks as one layer of defense, not the only one

If even PreToolUse has documented failure cases, can hooks still be relied on for security gating at all? The more practical answer is: yes, but not as the sole line of defense. Blocking-type security needs are better paired with other mechanisms (sandbox-level permission restrictions, for instance), treating the hook as one layer among several rather than the entire defense. More importantly, any blocking rule you write should be triggered once in practice, so you can personally confirm the blocked action genuinely didn't happen — rather than treating the mere appearance of an error message in the terminal as proof the rule was verified.

Three questions to ask before your next hook setup

Before copying someone else's hook configuration example, confirm three things: whether the event name is actually PreToolUse and not PostToolUse; whether the blocking logic uses exit 2 and not exit 1; and, most critically, whether you've actually triggered the rule once to check if the target file or command was genuinely affected. An error message on screen can only tell you "Claude received a signal worth attending to" — it can't tell you "the risk has been eliminated." That gap is exactly what's worth verifying for yourself.

Sources: PostToolUse exit 2 shows "blocking error" but edit already applied - Issue #19009, PreToolUse exit 2 does not block git push - Issue #23284, PreToolUse exit 2 blocks Bash but not Write/Edit - Issue #13744, Windows: exit 2 and stderr handling broken for nested-quote commands - Issue #80039, Hooks reference - Claude Code Docs
Diagram
PreToolUse 與 PostToolUse 的阻擋能力對照同樣顯示錯誤訊息,PreToolUse 有機會真正阻止動作,PostToolUse 只能事後回報PreToolUse vs PostToolUse — What exit 2 Actually DoesPreToolUseFires BEFORE the tool runsexit 2 CAN genuinely blockthe operation from happeningbut documented failures exist:#23284 (git push), #13744 (Write/Edit),#80039 (Windows nested-quote parsing)PostToolUseFires AFTER the tool already ranexit 2 only relays stderr to Claudecannot undo the change already made"blocking error" label is misleading(issue #19009) — good for after-the-factwarnings only, not preventionThe real diagnostic questionNot "did an error message appear?"But "did it appear before or after the action occurred?"Claude Skill Me · claudeskill-me.com
Feel free to share. Please credit the source.
Ask a Question
Please enter at least 10 characters
Related Articles
Enabled Auto Mode and Thought You Were Safe? Permission Modes and Sandbox Boundaries Are Two Different Layers — Conflating Them Is How Things Break
advanced · Sep 28
Subagents Aren't Smarter Mini-Claudes — They Solve Isolation, Not Capability
advanced · Aug 31
Why Your Claude Code Skill Silently Stops Triggering: The 15,000-Character Description Budget No One Warns You About
practice · Sep 02
CLAUDE.md Said So, but Claude Still Skipped It? Use Hooks to Turn a Request into a Guarantee
practice · Aug 25
Related News
More Related Topics