PreToolUse Hooks Bypassed by System-Initiated Tool Calls
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
- Create a PreToolUse hook configuration in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node ./.claude/hooks/read_hook.js"
}
]
}
]
}
}
- 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();
- Create a .env.local file with some content
- Reference the file in a message using @.env.local and ask "What are the contents?"
- 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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗