Claude Code reads .env files by default and no single built-in control fully prevents it.
This guide layers multiple defences so that sensitive files are protected even if one mechanism is bypassed.
| Vector | Risk |
|---|---|
Built-in Read tool |
Claude directly reads .env via its file tool |
Bash (cat, grep, etc.) |
Shell commands bypass permissions.deny rules |
| System reminders | File-change notifications leak contents into Claude's context |
| Environment variable inheritance | Secrets loaded at startup are visible in the process environment |
| Prompt injection (CVE-2025-55284) | Malicious content tricks Claude into exfiltrating secrets via DNS |
Note:
.claudeignoreis not a real feature - Claude reads files listed in it anyway.
.gitignoreprotects your Git history, not your secrets from AI tools.
Create or edit .claude/settings.json in your project root:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./.env.local)",
"Read(./.env.production)",
"Read(./secrets/**)",
"Read(./**/*.pem)",
"Read(./**/*.key)",
"Read(./**/*id_rsa*)"
]
}
}
⚠️ Syntax gotcha: Absolute paths require//not/.
Always use relative paths starting with./to avoid silent failures.
What this blocks: Claude's built-in Read, Grep, Glob, and LS tools.
What it does not 100% block: Bash commands like cat .env.
Hooks intercept tool calls before they execute. Exit code 2 blocks the action; exit 0 allows it.
mkdir -p ~/.claude/hooks
touch ~/.claude/hooks/block-sensitive-reads.sh
chmod +x ~/.claude/hooks/block-sensitive-reads.sh#!/bin/bash
# ~/.claude/hooks/block-sensitive-reads.sh
# Blocks shell-level access to sensitive files.
# Claude passes the tool input as JSON on stdin.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.command // ""')
BLOCKED_PATTERNS=(
'\.env'
'\.env\.'
'secrets/'
'\.pem'
'\.key'
'id_rsa'
'credentials'
'\.netrc'
'aws/credentials'
'kubeconfig'
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qiE "$pattern"; then
echo "Blocked: access to sensitive files via shell is not permitted." >&2
exit 2
fi
done
exit 0{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/block-sensitive-reads.sh"
}
]
}
]
}
}
⚠️ Limitation: Hooks are best-effort. A creative shell one-liner
(python3 -c "print(open('.env').read())") can still slip through.
This is why Layer 3 is essential.
/sandbox uses macOS Seatbelt or Linux bubblewrap to enforce deny rules at the OS level,
closing the Bash bypass vector entirely.
Inside a Claude Code session, run:
/sandbox
With sandboxing active, deny rules block all access - including shell commands - not just Claude's built-in tools. This is the single most impactful step you can take.
/sandboxis per-session. If you close and reopen Claude Code, re-enable it.
Copy this to your project and work through it top to bottom.
[ ] 1. Added permissions.deny rules to .claude/settings.json
[ ] 2. Created and registered the PreToolUse hook script
[ ] 3. Enabled /sandbox at the start of each Claude Code session
[ ] 4. Confirmed .env is in .gitignore (protects Git history)
[ ] 5. Rotated any secrets that were already in a project Claude Code accessed
| Layer | Mechanism | Blocks Read tool | Blocks Bash | OS-enforced |
|---|---|---|---|---|
| 1 | permissions.deny |
✅ | ❓ (sometimes) | ❌ |
| 2 | PreToolUse hook | ✅ | ✅ (best effort) | ❌ |
| 3 | /sandbox |
✅ | ✅ | ✅ |
Recommended stack: All three layers together.