The Blind Spots in Hook-Based Enforcement
PreToolUse hooks are the most reliable way to enforce rules in Claude Code. They run before every tool call, they fire in subagents, and the model can’t override them. But “most reliable” is not “perfect.” Here are five blind spots we’ve found through building hooks and reading the issues where they don’t quite cover the gap.
1. Memory paths bypass approval silently
#38040 documents a behavior that surprised the reporter: file paths under ~/.claude/projects/*/memory/ auto-bypass Edit and Write approval. There’s no opt-out. Claude can modify memory files without the user ever seeing a permission prompt.
A PreToolUse hook can catch this. If you return a block decision for writes to memory paths, the write doesn’t happen. But you have to know to set it up. The auto-bypass means your default protection is zero for these files, and many users don’t realize memory paths get special treatment until something goes wrong.
If you’re using file-guard or enforce-hooks, add ~/.claude/projects/*/memory/* to your protected patterns. If you’re writing hooks manually, check the file path in your Write/Edit handler:
if echo "$FILE" | grep -qE '\.claude/projects/.*/memory/' 2>/dev/null; then
jq -cn --arg r "Memory file modification requires explicit approval" \
'{"decision":"block","reason":$r}'
exit 0
fi
2. Built-in skills wrap tool calls opaquely
The same issue (#38040) reveals a second gap. Claude Code has built-in skills that perform file operations internally. When a skill runs Write or Edit, it goes through the Skill tool wrapper, not through a direct tool call. PreToolUse hooks fire on the Skill tool invocation, but you don’t see the individual file operations inside it.
This means a hook that checks “is this write targeting .env?” won’t fire, because the tool name is Skill, not Write. The file path is buried inside the skill’s internal execution.
There’s no clean workaround for this right now. The best mitigation is a PostToolUse hook that checks what changed after the skill ran, but that’s after-the-fact detection, not prevention.
3. Context compaction invalidates hook state
#38018 describes a near-miss. The reporter built a domain-knowledge gate hook that tracks which files Claude has read. If Claude tries to modify a file without reading the relevant spec first, the hook blocks it. It worked well until context compaction.
When Claude compacts its context window, it summarizes and discards tokens. Files that were read before compaction are no longer in Claude’s working memory, but the hook’s tracking state still shows them as “recently read.” The hook says “you know about this spec” when Claude actually doesn’t.
The reporter’s hook nearly caused production damage because Claude modified code based on a spec it could no longer see.
This affects any hook that maintains state about what Claude has done in the current session. Our read-once hook tracks recently-read files to prevent redundant re-reads. After compaction, that tracking could prevent a re-read that Claude actually needs because its context no longer contains the file content.
The fix would be a PostCompact hook event that lets hooks invalidate their state when compaction happens. That event doesn’t exist yet. Until it does, stateful hooks need conservative TTLs that assume compaction could happen at any time.
4. Permission-hook interaction bugs
#37745 documents a case where PreToolUse hooks returning permissionDecision: "ask" corrupt the global permission state. If you start Claude Code with --dangerously-skip-permissions, hooks that return “ask” gradually erode that flag until the model starts prompting for approval on everything.
This creates a frustrating dynamic for autonomous users: you want hooks for safety enforcement, but you also want skip-permissions for autonomous operation. The two features interact in a way that breaks both.
The workaround: use block or nothing. Don’t return ask from hooks running in autonomous mode. If a hook needs to gate an action, block it outright rather than requesting human input that won’t come.
5. Prompt injection through tool output
#38046 shows a new injection vector. The /insights command returns raw JSON containing embedded <message> blocks that look like system instructions. Claude executed the injected instruction (printing a marketing message). The reporter correctly notes this is indistinguishable from a real injection attack and could escalate to credential exfiltration or destructive commands.
PreToolUse hooks can’t prevent this. The injection happens through tool output, not tool input. By the time Claude acts on the injected instruction, it’s already in context. A PostToolUse hook on the /insights output could sanitize it, but PreToolUse hooks only see what Claude is about to do, not what it just received.
This is a fundamental limitation of the PreToolUse model: it gates actions but can’t gate the information that influences those actions.
What this means in practice
Hooks are still the best available mechanism for enforcing rules in Claude Code. The issues above are not arguments against using them. They’re arguments for understanding their boundaries.
The pattern across all five blind spots: hooks work well for direct, inspectable tool calls (Bash commands, file writes, git operations) where the intent is visible in the arguments. They work less well for opaque wrappers (Skill tool), stateful reasoning (knowledge gates across compaction), and information-flow attacks (prompt injection through output).
If you’re relying on hooks for safety:
- Enumerate what they cover and what they don’t for your specific setup
- Run
safety-check --verifyperiodically to confirm hooks still fire and block correctly - Don’t assume “hooks installed” means “fully protected”
- Watch the Claude Code issues for new failure modes
The goal is not perfect enforcement. It’s knowing where the gaps are.