A PreToolUse hook that prevents Claude Code from accessing your credentials.
Claude Code automatically reads .env files. Any API keys, tokens, or secrets in the LLM context are a security risk - prompt injection, accidental logging, or the agent creatively building curl commands with your credentials.
For background:
- Claude Code loads .env secrets without permission (Knostic)
- Claude Code ignores ignore rules meant to block secrets (The Register)
A 150-line Python script that runs before every tool execution and blocks credential access:
| Tool | What's blocked |
|---|---|
| Bash | source .env, cat .env, grep .env, credential variables in curl, auth header expansion |
| Read | Any .env file (allows .env.example, .env.template) |
| Grep | Searching inside .env files |
The agent still gets API access - through wrapper scripts that source .env internally. The LLM sees structured output, never the keys.
┌─────────────┐ ┌──────────────────┐ ┌─────────┐
│ Claude Code │────▶│ security_guard.py │────▶│ Tool │
│ (LLM) │ │ (PreToolUse) │ │ Execute │
└─────────────┘ └──────────────────┘ └─────────┘
│ │
│ BLOCKED if .env
│ │
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ wrapper.sh │────▶│ .env (sourced │
│ (Bash call) │ │ internally) │
└─────────────┘ └──────────────────┘
│
▼
Structured output
(LLM sees this, not the keys)
This repo mirrors the standard Claude Code project layout. You can copy the files directly into your project:
your-project/
├── .claude/
│ ├── hooks/
│ │ └── security_guard.py ← the hook
│ └── settings.json ← hook registration
├── scripts/
│ └── example-api.sh ← wrapper script template
├── .env ← your secrets (never seen by LLM)
└── ...
# Copy the hook
cp .claude/hooks/security_guard.py /path/to/your/project/.claude/hooks/
# Copy the example wrapper (optional)
cp scripts/example-api.sh /path/to/your/project/scripts/Merge the PreToolUse hook into your existing settings (see .claude/settings.json in this repo for the exact format):
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "python3 $CLAUDE_PROJECT_DIR/.claude/hooks/security_guard.py",
"timeout": 5000
}
]
}
]
}
}The hook runs automatically before every tool call. No restart needed.
Instead of letting the agent read .env directly, create wrapper scripts under scripts/:
#!/usr/bin/env bash
# scripts/my-api.sh - credential-isolated wrapper
source "$(dirname "$0")/../.env" # credentials stay in this process
case "${1:-}" in
fetch)
curl -s -H "Authorization: Bearer $API_KEY" \
"https://api.example.com/data"
;;
esacThe agent calls ./scripts/my-api.sh fetch and gets JSON back. It never sees $API_KEY.
See scripts/example-api.sh for a complete template.
The agent sees:
SECURITY BLOCKED: .env sourcing
Use credential-isolated wrapper scripts for API access.
If you need to modify .env, ask the user to do it manually.
All blocks are logged to ~/.claude-security/security-guard.log:
[2026-02-22T08:30:00+0100] BLOCKED | session=abc123 | tool=Bash | reason=.env sourcing | detail=source .env && curl...
This hook is one layer. Combine with:
.claude/settings.jsonpermissions - restrict which tools are auto-allowedscripts/wrapper pattern - the agent gets data without seeing credentials- Audit log - review what was blocked and when
Even if a prompt injection tells the agent to "read the .env file", the hook blocks it. It runs outside the LLM context - the agent can't bypass it.
Add your own patterns to BASH_BLOCK_PATTERNS in .claude/hooks/security_guard.py:
BASH_BLOCK_PATTERNS = [
# ... existing patterns ...
(r"\bmy-custom-secret\b", "custom secret access"),
]Change the log directory:
LOG_DIR = Path.home() / ".my-project" / "security-logs"Disable logging:
LOG_DIR = None- Python 3.10+ (for
str | Nonetype hints, or change toOptional[str]for 3.9) - Claude Code with hooks support
MIT