[BUG] PreToolUse hooks exit code ignored - operations proceed after hook failure

Resolved 💬 3 comments Opened Jan 30, 2026 by yvinapex Closed Jan 30, 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?

When a PreToolUse hook exits with a non-zero exit code (indicating the operation should be blocked), Claude Code logs "PreToolUse:[ToolName] hook error" but proceeds with the operation anyway instead of aborting it. This completely defeats the purpose of security/validation hooks.
The hook correctly:

Detects the tool being used
Identifies violations (e.g., protected files, unauthorized paths)
Prints error messages to stderr
Exits with code 1 to block the operation

However, Claude Code ignores the exit code and executes the tool successfully despite the hook blocking it.
This is a critical security issue because PreToolUse hooks are intended for security validation, path restrictions, permission enforcement, and compliance checks. If hooks cannot actually block operations, they provide false security.

What Should Happen?

When a PreToolUse hook exits with a non-zero exit code, Claude Code should:

Log the hook error
Abort the tool operation immediately
Display the stderr output from the hook to the user in the chat
NOT proceed with the blocked operation
Provide clear feedback that a hook blocked the operation

The tool should never execute if any PreToolUse hook returns a non-zero exit code.

Error Messages/Logs

Claude Code Output (shows hook error but proceeds anyway):
● Update(.claude\skills\SharePoint\Config.md)
  ⎿  PreToolUse:Edit hook error
  ⎿  Added 4 lines
      14      local_path: "C:\\Users\\YvonVincent\\OneDrive - Apex Automation Ltd\\Documents\\Claude Test2"
      15      url: "https://apexautomationltd.sharepoint.com/personal/yvon_vincent_apexautomation_ca/Documents/Claude Test2"
      16      libraries: ["*"]  # All libraries/folders
      17 +  - name: "Test Path"
      18 +    local_path: "C:\\Test\\Path"
      19 +    url: ""
      20 +    libraries: ["*"]  # All libraries/folders
Hook Debug Log (shows hook correctly blocked the operation):
[2026-01-30 11:07:26] Tool: Edit
[2026-01-30 11:07:26] Checking write operation on: C:\Users\YvonVincent\.claude\skills\SharePoint\Config.md
[2026-01-30 11:07:26] Checking if protected: C:\Users\YvonVincent\.claude\skills\SharePoint\Config.md
[2026-01-30 11:07:26]   MATCH: Direct path comparison
[2026-01-30 11:07:26] BLOCKED: Edit on C:\Users\YvonVincent\.claude\skills\SharePoint\Config.md
Hook stderr output (what should be shown to user but isn't):
╔═══════════════════════════════════════════════════════════════╗
║                     ACCESS DENIED                              ║
╚═══════════════════════════════════════════════════════════════╝

This file is protected and can only be modified by humans.

Protected file: C:\Users\YvonVincent\.claude\skills\SharePoint\Config.md
Tool attempted: Edit

If you need to modify this file, please edit it manually.
The file is read-only for Claude Code to prevent unauthorized
changes to security configurations.
Tool call that was sent to hook:
json{
  "session_id": "e380c7ed-7f7d-46c5-b398-59b83c1c0b39",
  "hook_event_name": "PreToolUse",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "C:\\Users\\YvonVincent\\.claude\\skills\\SharePoint\\Config.md",
    "old_string": "...",
    "new_string": "...",
    "replace_all": false
  }
}

Steps to Reproduce

Step 1: Create a test hook that blocks all Edit operations
Create file: C:\Users\[USERNAME]\.claude\hooks\block-edit-test.py
python#!/usr/bin/env python3
import sys
import json

try:
tool_call = json.loads(sys.stdin.read())
tool_name = tool_call.get('tool_name', '')

if tool_name.lower() == 'edit':
print("ERROR: All Edit operations are blocked by test hook", file=sys.stderr)
sys.exit(1) # Block the operation

sys.exit(0) # Allow other operations
except Exception as e:
print(f"Hook error: {e}", file=sys.stderr)
sys.exit(1)
Step 2: Configure the hook in settings.json
Add to ~/.claude/settings.json:
json{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "python C:/Users/[USERNAME]/.claude/hooks/block-edit-test.py"
}
]
}
]
}
}
Step 3: Restart Claude Code
Completely exit and restart Claude Code to load the new settings.
Step 4: Test the hook
Create a test file test.txt with some content, then ask Claude Code:
Please edit test.txt and add the word "hello"
Step 5: Observe the incorrect behavior
What actually happens:

Claude Code displays: ⎿ PreToolUse:Edit hook error
Claude Code proceeds to edit the file anyway
The file is successfully modified despite the hook returning exit code 1
The stderr message from the hook is not shown to the user

What should happen:

Claude Code should abort the Edit operation
Claude Code should display the stderr message ("ERROR: All Edit operations are blocked by test hook") to the user
The file should NOT be modified

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

1.3.5

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Terminal.app (macOS)

Additional Information

Real-world use case:
Protecting sensitive configuration files (like skill configs, API keys, security settings) from accidental or unwanted modifications by Claude Code while still allowing read access.
Impact:

Users who implement security hooks believe their files are protected
Files get modified despite hooks blocking them
False sense of security leads to potential data loss or security breaches
Compliance and audit requirements cannot be enforced

Workarounds:
Until this is fixed, the only reliable protection is OS-level file permissions:
cmdicacls "path\to\file" /deny "%USERNAME%:(W,WD,AD,WEA,WA)"
However, this is far less flexible than hooks and requires manual intervention to edit files.
Note on other hooks:
Other Python hooks (like path-guard.py for path restrictions) execute successfully, so the issue is specifically with exit code handling, not hook execution itself.

View original on GitHub ↗

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