PreToolUse Hooks Bypassed by System-Initiated Tool Calls

Resolved 💬 3 comments Opened Oct 12, 2025 by eagle6525 Closed Jan 9, 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?

PreToolUse hooks configured in .claude/settings.json are not applied to system-initiated tool calls that occur when users reference files using the @file syntax. This allows the hook security controls to be bypassed.

  • ✅ PreToolUse hooks DO block tool calls when the assistant explicitly invokes them
  • ❌ PreToolUse hooks DO NOT block tool calls that are system-initiated via @file references
  • System-initiated reads happen before the assistant responds, as shown in <system-reminder> tags

Security Implications

This bypass means PreToolUse hooks cannot reliably enforce access controls. Users who configure hooks to prevent reading sensitive files (credentials, secrets, private keys, etc.) will find that these protections can be circumvented simply by using the @file reference syntax.

What Should Happen?

PreToolUse hooks should intercept and block ALL tool calls matching the configured matcher pattern, regardless of whether they are initiated by the assistant or by the system in response to user file references.

Error Messages/Logs

Steps to Reproduce

  1. Create a PreToolUse hook configuration in .claude/settings.json:
  {
    "hooks": {
      "PreToolUse": [
        {
          "matcher": "Read|Grep",
          "hooks": [
            {
              "type": "command",
              "command": "node ./.claude/hooks/read_hook.js"
            }
          ]
        }
      ]
    }
  }
  1. Create a hook script that blocks reads to .env files (.claude/hooks/read_hook.js):
  async function main() {
    const chunks = [];
    for await (const chunk of process.stdin) {
      chunks.push(chunk);
    }

    const toolArgs = JSON.parse(Buffer.concat(chunks).toString());

    const readPath =
      toolArgs.tool_input?.file_path || toolArgs.tool_input?.path || "";

    if (readPath.includes('.env')) {
      console.error("You cannot read the .env file");
      process.exit(2);
    }
  }

  main();
  1. Create a .env.local file with some content
  2. Reference the file in a message using @.env.local and ask "What are the contents?"
  3. Observe that the system reads the file successfully before the hook can block it

Actual Results

The conversation includes system reminders like:
<system-reminder>
Called the Read tool with the following input: {"file_path":"/path/to/.env.local"}
</system-reminder>

<system-reminder>
Result of calling the Read tool: "file contents..."
</system-reminder>

The hook never executes for these system-initiated reads.

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.0.14

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

When the assistant explicitly calls the Read tool (not via @file reference), the hook works correctly and blocks the operation with:
Read operation blocked by hook:

  • [node ./.claude/hooks/read_hook.js]: You cannot read the .env file

This confirms the hook configuration is correct, but it only applies to assistant-initiated calls.

View original on GitHub ↗

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