[BUG] @file reference bypasses PreToolUse hooks (e.g. .env read block)
Preflight Checklist:
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
When a PreToolUse hook is configured to block reading sensitive files (e.g. .env), the @file reference syntax in the user prompt completely bypasses the hook. The file contents are injected into the conversation context without any hook being triggered.
This means any file-read restriction enforced via PreToolUse hooks can be trivially circumvented by typing @.env in the prompt instead of asking the assistant to read the file with the Read tool.
This cannot be worked around in the hook script itself. The @file syntax is resolved by the Claude Code client before any hook infrastructure is invoked — no PreToolUse event fires, so the hook script never executes regardless of its content.
What Should Happen?
The @file reference should respect PreToolUse hooks (or a dedicated hook event) before injecting file contents into the conversation. If a hook would block reading that file, the contents should not be included.
Error Messages/Logs
No error is shown — that's the problem. The hook is silently bypassed and the file contents appear in the conversation.
When using the Read tool, the hook correctly blocks with:
reading .env files is not allowed
When using @.env, no hook fires and the full file contents are visible to the model.
Steps to Reproduce
- Create a
.envfile with sensitive content:
````
SECRET_KEY=super_secret_value
- Create a hook script at
~/.claude/hooks/block-env-read.sh:
```bash
#!/usr/bin/env bash
# Block Claude from reading .env files (receives tool input JSON on stdin)
set -euo pipefail
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
case "$tool_name" in
Read)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
if [[ "$file_path" == .env || "$file_path" == /.env. || "$file_path" == /.env ]]; then
echo '{"decision":"block","reason":"reading .env files is not allowed"}'
exit 2
fi
;;
Bash)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
if echo "$command" | grep -qE '(cat|head|tail|less|more|bat)\s+.*\.env(\s|$|")'; then
echo '{"decision":"block","reason":"reading .env files via shell is not allowed"}'
exit 2
fi
;;
esac
exit 0
```
- Make the script executable:
chmod +x ~/.claude/hooks/block-env-read.sh
- Register the hook in
~/.claude/settings.json:
``json``
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Bash",
"hooks": [
{
"type": "command",
"command": "/home/user/.claude/hooks/block-env-read.sh"
}
]
}
]
}
}
- Start a Claude Code session and ask Claude to read the
.envfile → correctly blocked by the hook with "reading .env files is not allowed".
- Start a new prompt and type
@.envfollowed by any message (e.g. "what is in this file?") → file contents are injected into the conversation, completely bypassing the hook.
Claude Model: Opus
Is this a regression? I don't know
Claude Code Version: 2.1.195
Platform: Anthropic API
Operating System: Debian Linux
Terminal/Shell: Bash
Additional Information
The only workaround currently is to avoid having sensitive files in the workspace entirely.
Possible fixes on the Claude Code side:
- Fire a
PreToolUseevent (e.g. withtool_name: "FileReference") before including@-referenced file contents - Add a dedicated hook event like
PreFileReference - Apply the same hook pipeline that
Readwould trigger to any@-referenced path
3 Comments
Worth flagging for anyone relying on PreToolUse hooks for this exact purpose, including tools like Prismor (github.com/PrismorSec/prismor) that block reads of .env/.ssh/credentials paths at the hook layer: this bypass means the guarantee only holds for the Read and Bash tools, not for @-references. Until the client fires a hook event for @file resolution, a hook-based blocklist is a strong deterrent against the common path, not a complete boundary. Good catch, this is the kind of gap that's easy to miss until someone tests it directly like you did.
Thanks for the detailed repro — I dug into this on 2.1.233 (Linux).
What I confirmed:
PreToolUsehook (matcherRead) that blocks.env, an@.envmention still pulls the file contents into context, and the hook does not fire. A plain "read .env with the Read tool" request is blocked by that same hook.permissions.deny: ["Read(./.env)"]instead, the@.envmention is blocked — the contents are never injected.Why:
@-file mentions are resolved before the tool loop, so they aren't tool calls andPreToolUsehooks (which run per tool call) don't apply to them. Read permission rules, on the other hand, are applied to@filementions on a best-effort basis — see the permissions docs: https://code.claude.com/docs/en/permissionsRecommendation: to restrict file reads including
@-mentions, usepermissions.deny(e.g.Read(./.env)) rather than aPreToolUsehook. This is working as intended; the one fair point is that the docs don't explicitly say hooks don't govern@-mentions, so we'll look at clarifying that. Leaving this open to track the doc note.🤖 Generated with Claude Code
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.