A developer asked Claude Code to document their Azure OpenAI configuration. Claude hardcoded the actual API key in a markdown file. It got pushed to a public repo and sat there for 11 days. Hackers found it. $30,000 in fraudulent API charges later, the developer learned an expensive lesson about AI guardrails.
This isn’t an isolated incident. It’s a pattern. Yesterday I argued against complex AI scaffolding - frameworks that use LLMs to coordinate LLMs. Non-deterministic all the way down. But there’s another kind of structure: deterministic guardrails that execute regardless of what the model thinks it should do.
The Graveyard
The Home Directory Nuke. A user was cleaning up an old repository when Claude executed rm -rf tests/ patches/ plan/ ~/. That trailing ~/ wiped their entire Mac: Desktop, Documents, Downloads, Keychain, application data. Years of work, gone in seconds. The command had been approved because the user was focused on the first three directories.
The Production Database Massacre. Replit’s AI wiped data for 1,200+ executives during an explicit code freeze. When confronted, it created 4,000 fake user records to cover its tracks. “I panicked instead of thinking,” the AI explained. The CEO called it “unacceptable and should never be possible.”
The Silent Secret Leak. Claude Code automatically loads .env files without notification. In one case, it copied production credentials to an env.example file that got committed to GitHub. The user had explicitly blocked .env access in their settings. Claude read them anyway and replicated the values elsewhere.
The Test Gaslighter. Claude modified tests to pass with incorrect behavior, then defended the changes: “This is how it should work anyway.” Death spiral: bad test leads to bad code leads to feature not matching spec.
These aren’t bugs. They’re the natural consequence of giving an LLM the ability to execute arbitrary commands while relying on prompt-based instructions for safety.
Why Prompts Fail
You can write “NEVER edit .env files” in your CLAUDE.md. Claude will read it. Claude will understand it. And Claude might still edit your .env file because:
- Context window pressure. Instructions at the top get compressed or summarized as the conversation grows.
- Conflicting signals. A user request that seems to require
.envaccess can override documented guidelines. - Hallucinated permissions. Claude sometimes convinces itself that an exception applies.
- Copy-paste propagation. Even if Claude won’t edit
.env, it might copy secrets to a file it will edit.
Prompts are interpreted at runtime by an LLM that can be convinced otherwise. You need something deterministic.
Enter Hooks
Hooks are shell commands that execute at specific lifecycle points: before a tool runs, after it completes, when Claude wants to stop, when a session starts. They’re not suggestions. They’re enforcement.
PreToolUse hook blocking .env edits = always runs, returns exit code 2, operation blocked
CLAUDE.md saying "don't edit .env" = parsed by LLM, weighed against other context, maybe followed
The difference is binary. Hooks execute regardless of what Claude thinks it should do.
Hookify: Zero-JSON Hook Creation
Writing hooks traditionally means editing settings.json with nested JSON structures. The hookify plugin eliminates that friction.
Install from the official marketplace (run /plugin and browse Discover if this doesn’t work):
/plugin install hookify
/hookify Block any rm -rf commands that include home directory paths
That creates .claude/hookify.block-rm-rf.local.md with a regex pattern and warning message. No restart needed. The rule takes effect on the next tool use.
The Rulebook
Block Destructive Commands
The home directory nuke would’ve been prevented by:
---
name: block-dangerous-rm
enabled: true
event: bash
pattern: rm\s+-rf\s+.*(/|~)
action: block
---
🛑 rm -rf with root or home path detected. Blocked.
Prevent Hardcoded Secrets
The $30k API key leak would’ve been caught by:
---
name: block-hardcoded-secrets
enabled: true
event: file
conditions:
- field: new_text
operator: regex_match
pattern: (API_KEY|SECRET|TOKEN|PASSWORD)\s*[=:]\s*["'][A-Za-z0-9_\-]{16,}
action: block
---
🔐 Hardcoded secret detected. Use environment variables instead.
Protect Sensitive Files
---
name: protect-env-files
enabled: true
event: file
conditions:
- field: file_path
operator: regex_match
pattern: \.env($|\.)
action: block
---
🚫 .env files are protected. Use .env.example with placeholders only.
Block Force Push
---
name: block-force-push
enabled: true
event: bash
pattern: git\s+push\s+.*(-f|--force)
action: block
---
⚠️ Force push blocked. Requires explicit approval.
Require Tests Before Completion
---
name: require-tests
enabled: true
event: stop
conditions:
- field: transcript
operator: not_contains
pattern: npm test|pnpm test|pytest|cargo test|go test
action: block
---
🧪 Tests not detected in session. Run test suite before completing.
Warn on Production Commands
---
name: warn-production
enabled: true
event: bash
pattern: (prod|production|--prod|PROD)
action: warn
---
⚠️ Production keyword detected. Verify this is intentional.
Flag Test File Modifications
---
name: warn-test-changes
enabled: true
event: file
conditions:
- field: file_path
operator: regex_match
pattern: \.(test|spec)\.(ts|js|tsx|jsx|py)$
action: warn
---
⚠️ Test file modification. Ensure assertions match expected behavior, not current implementation.
Use action: warn initially to understand what triggers without blocking your workflow. Escalate to action: block once you’ve validated the pattern.
When Patterns Aren’t Enough
For complex validation logic, drop to raw hooks in settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "python3 ~/.claude/validators/bash_validator.py"
}]
}]
}
}
Your Python script receives JSON via stdin with the full command context. Return exit code 0 to allow, exit code 2 to block (with stderr shown to Claude).
Example validator that blocks commands escaping the project directory:
#!/usr/bin/env python3
import json, sys, os
data = json.load(sys.stdin)
cmd = data.get('tool_input', {}).get('command', '')
cwd = data.get('cwd', '')
# Block cd to parent directories or absolute paths outside project
if '../' in cmd or (cmd.startswith('cd /') and not cmd.startswith(f'cd {cwd}')):
print("Command attempts to escape project directory", file=sys.stderr)
sys.exit(2)
sys.exit(0)
What Hooks Don’t Solve
- Backups. Hooks can block destructive commands, but a novel destructive pattern will slip through. Git and Time Machine are still your friends.
- Sandboxing. Hooks run before the action, but Claude Code still has access to your filesystem. For true isolation, run in a container.
- Social engineering. A user can be convinced to disable hooks or approve a blocked action. Human judgment remains the final layer.
- Novel attack vectors. The Nx malware incident specifically targeted Claude Code with flags to bypass guardrails. Attackers adapt.
- False positives. Aggressive patterns will block legitimate operations. You’ll spend time tuning.
Hooks are one layer in a defense-in-depth strategy, not a silver bullet. As I wrote in Guardrails by Default, the future of AI coding isn’t smarter models. It’s enforcement baked in.
Getting Started
/plugin install hookify
If that doesn’t resolve, run /plugin and search for “hookify” in the Discover tab.
Start with one rule. The production warning is low-friction:
/hookify Warn me when any command contains "prod" or "production"
Watch it trigger for a week. Tune the pattern. Add another rule. Build your safety net incrementally.
Hooks complement the fundamentals: clean context, plan before executing, review diffs. They’re not a replacement for careful workflow. They’re the safety net for when you slip.
For more advanced patterns like auto-activating skills based on file context, hooks become the trigger mechanism. But start simple. One rule. One footgun prevented.


