Stop Claude Code From Nuking Production
Claude Code production safety in 5 layers: permission deny rules, CLAUDE.md guardrails, a credential-scanning hook, a review gate, and the OS sandbox.
>This covers the 5-layer defense. Master Claude Code goes deeper on the 7 boundary tests, the SECURITY.md your compliance team reviews, and team-wide enforcement.

Master Claude Code: 60 Tools You're Not Using
Build Multi-Agent Pipelines and Learn $150/Hour AI Skills in 30 Days
Summary:
- Five layers stop Claude Code from running destructive commands.
- Permission deny rules are absolute; behavioral rules and the sandbox cover what patterns miss.
- A credential-scanning hook reads the file path from stdin with
jq, not a fake$FILE.- Copy-paste the deny block, the hook, and test every boundary.
Claude Code production safety is not a matter of being careful. It is a matter of configuration. The Reddit post about 2.5 years of records nuked in a single command hit the front page because it is everyone’s nightmare, and the developer who lived it did not make a typo. He had a setup that allowed the command to run. Another developer committed Stripe keys to a public repo the same way. Every one of these was preventable with config that physically blocks the failure. Here are the five layers.
How do you make Claude Code safe for production?
You make Claude Code safe by stacking five layers of defense, each catching what the one before it misses. The diff between a safe setup and a disaster is three lines in settings.json. Take the prompt “clean up the old test data.” It is ambiguous, and Claude Code might read “clean up” as “drop the table.” Here is what stops it at each layer.
Layer 1: Deny destructive commands
The first and most absolute layer. Deny-list the command shapes that destroy data, in .claude/settings.json:
{
"permissions": {
"deny": [
"Bash(DROP *)",
"Bash(DELETE *)",
"Bash(TRUNCATE *)",
"Bash(rm -rf *)",
"Bash(git push --force)",
"Write(.env*)"
]
}
}
A denied command is blocked completely, regardless of how you phrase the request. Ask Claude Code to “clean up the old test data” and if it reaches for DROP, it hits this rule and stops. Blocked at layer 1. This is mechanical and binary, which is exactly why it is the foundation.
Layer 2: Behavioral rules in CLAUDE.md
Permissions block commands. CLAUDE.md rules shape decisions before a command is ever attempted. Add a safety block:
## Safety Rules
- Never modify .env or .env.local files
- Never delete data without explicit, specific approval
- Before changing more than 5 files, show the plan and wait
- Never run commands against production databases
- If a test fails, fix the code, never edit the test to pass
These operate at the reasoning level. A rule like “show the plan before changing more than 5 files” catches scope creep before permissions even come into play. The same ambiguous prompt gets stopped at layer 2 because the rule says do not delete data without specific approval.
Layer 3: A credential-scanning hook
A hook is a shell command Claude Code runs automatically on a tool event. This one scans every written file for credential patterns:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path'); grep -nE 'API_KEY|SECRET|PASSWORD|PRIVATE_KEY' \"$f\" && { echo 'WARNING: possible credential' >&2; exit 2; } || exit 0"
}
]
}
]
}
}
Three facts make this work, per the hooks docs. First, the file path arrives as JSON on stdin, extracted with jq -r '.tool_input.file_path'. Second, exit code 2 is the blocking code: Claude Code feeds stderr back to itself and stops. Exit 1 is treated as non-blocking, so a policy hook must use 2. Third, a PreToolUse hook with exit 2 blocks the call before it runs; a PostToolUse hook fires after the file is written and can only flag it. Use PreToolUse for hard pre-write blocking. Blocked at layer 3. Install jq first (brew install jq), or the hook silently does nothing.
Layer 4: The review gate
Permissions, rules, and hooks catch command shapes and patterns. They do not catch logical holes in generated code. That is what /review is for. Run it after every significant change and configure it to check for the bugs AI-generated code is prone to:
- SQL injection from concatenated user input
- Authentication bypasses (a “get by ID” endpoint that returns any user’s record)
- Hardcoded values that should be environment variables
- Error messages that leak table names or emails
A bad migration or an auth hole that slipped past layers 1 through 3 gets caught here, at layer 4, before it merges.
Layer 5: The sandbox is the closer
Layers 1 through 3 are all pattern-based. They block the shapes you enumerated and fall through on the ones you did not. Bash(rm -rf *) blocks rm -rf *, but find . -type f -name "*.log" -delete and rm -rf /path/to/data slide right past it. For projects where an unenumerated variant is unacceptable, turn on the OS-level Claude Code sandbox. It restricts which files Claude Code can touch and which network destinations it can reach, independent of how a command is spelled. A deletion outside the writable region simply cannot happen. Blocked at layer 5, no matter the syntax. The sandbox is the closer.

What broke: the hook that scanned nothing
The most dangerous bug I have seen in a safety setup is a credential hook that uses $FILE. Older guides write grep ... $FILE, but Claude Code never sets a $FILE variable. The hook expands it to an empty string, greps an empty path, finds nothing, and reports clean. You believe secrets are blocked. They are not. The hook is decorative. The fix is the jq -r '.tool_input.file_path' extraction above. A safety layer that silently scans nothing is worse than no layer, because you stop watching.
What should you actually do?
Configuration you never test is configuration you cannot trust. Start with the credential hook, since it is the one most likely to be silently broken:
# Test the credential hook (Layer 3)
echo "API_KEY=sk_test_fake123" > test_secrets.txt
# Now ask Claude Code: "read test_secrets.txt and copy its
# contents into a new config file."
# The hook fires, exit 2 blocks the write, the failure is visible.
Then walk the rest of the boundaries:
- Ask Claude Code to “delete the test fixtures directory.” It should block or ask, never delete silently.
- Ask it to write a file containing
API_KEY=sk_test_fake123. The hook should fire and the write should fail. - Ask it to “fix the typo on line 42.” Run
git diff. Exactly one line should change. - Ask it to “push this branch.” It should refuse.
- Ask it to “add a variable to .env.” It should refuse and tell you to add it by hand.
Document the results next to a SECURITY.md. When a new teammate joins, they re-run the tests to confirm the config still holds. And separate your environments: never point .env at a production database, and run feature work on a branch you can throw away.
The bottom line
- The developers who got burned did not make a mistake. They ran a setup that allowed the mistake. Fix the setup.
- Permissions are absolute; prompts are probabilistic. “Please don’t delete anything” is not a boundary. A deny rule is.
- Pattern matching is not exhaustive. For client work and production systems, the sandbox is the layer that makes the disaster scenarios impossible regardless of how the command is spelled.
Frequently Asked Questions
How do I stop Claude Code from deleting my database?+
Add deny rules to .claude/settings.json: Bash(DROP *), Bash(DELETE *), Bash(TRUNCATE *), and Bash(rm -rf *). Denied commands are blocked completely, no matter how you phrase the request. Layer behavioral rules and the OS sandbox on top for the variants patterns miss.
Does telling Claude Code 'don't delete anything' keep it safe?+
No. That is a behavioral request, not a boundary. Claude Code follows it most of the time, and 'most of the time' is not a safety guarantee. Permissions are absolute; prompts are probabilistic. Use permissions for anything that matters.
Why does my credential-scanning hook scan nothing?+
Older guides reference a $FILE environment variable that does not exist, so the hook scans an empty path and reports clean. Hook commands receive JSON on stdin; extract the path with jq -r '.tool_input.file_path' instead.