[BUG] @file reference bypasses PreToolUse hooks (e.g. .env read block)

Status Fixed / completed
Reported on v2.1.195
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Jun 29, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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

  1. Create a .env file with sensitive content:

``
SECRET_KEY=super_secret_value
``

  1. 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
```

  1. Make the script executable: chmod +x ~/.claude/hooks/block-env-read.sh
  1. 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"
}
]
}
]
}
}
``

  1. Start a Claude Code session and ask Claude to read the .env file → correctly blocked by the hook with "reading .env files is not allowed".
  1. Start a new prompt and type @.env followed 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 PreToolUse event (e.g. with tool_name: "FileReference") before including @-referenced file contents
  • Add a dedicated hook event like PreFileReference
  • Apply the same hook pipeline that Read would trigger to any @-referenced path

View original on GitHub ↗

3 Comments

Ar9av · 3 months ago

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.

bcherny collaborator · 1 month ago

Thanks for the detailed repro — I dug into this on 2.1.233 (Linux).

What I confirmed:

  • Your repro is real: with a PreToolUse hook (matcher Read) that blocks .env, an @.env mention 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.
  • However, the supported guardrail does cover it: with permissions.deny: ["Read(./.env)"] instead, the @.env mention is blocked — the contents are never injected.

Why: @-file mentions are resolved before the tool loop, so they aren't tool calls and PreToolUse hooks (which run per tool call) don't apply to them. Read permission rules, on the other hand, are applied to @file mentions on a best-effort basis — see the permissions docs: https://code.claude.com/docs/en/permissions

Recommendation: to restrict file reads including @-mentions, use permissions.deny (e.g. Read(./.env)) rather than a PreToolUse hook. 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

github-actions[bot] · 7 days ago

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.