How to stop Claude Code from running destructive commands
To stop Claude Code from running destructive commands, layer four controls: deny and ask rules in .claude/settings.json for commands you never want run without a person, a PreToolUse hook that inspects every Bash command and blocks dangerous ones by exiting with code 2, the Bash sandbox so commands can't write outside your project or reach the network freely, and credentials that can't do damage in the first place. No single layer is enough on its own. Together they turn "the agent ran DROP TABLE" from a disaster into a blocked tool call.
This guide shows each layer with working config, checked against Anthropic's Claude Code documentation as of October 31, 2026.
Why this matters
Coding agents run real shell commands with whatever access your terminal has. If your shell can reach the production database, so can the agent.
The best-known example is from July 2025. Jason Lemkin, founder of SaaStr, was testing Replit's AI agent when, by his account, it deleted data from a production database during a declared code and action freeze, despite instructions not to proceed without human approval. Fortune reported that the data covered more than 1,200 executives and over 1,190 companies. Replit's CEO called it "unacceptable" and said the company would separate development and production databases automatically.
It isn't the only one: our roundup of AI coding agent incidents covers 18 public cases, three of them with Claude Code. The Replit case wasn't Claude Code, but the lesson is general: an instruction in a prompt is not a control. The agent can misread it, forget it or decide something else matters more. Controls that work are enforced by the tool or the operating system, outside the model.
Layer 1: deny and ask rules in settings.json
Claude Code reads permission rules from settings files. The settings docs list four:
| File | Scope |
|---|---|
~/.claude/settings.json | You, in every project |
.claude/settings.json | Everyone on the project (commit it) |
.claude/settings.local.json | You, in one project (not committed) |
| Managed settings | The whole organization, can't be overridden |
Each can hold allow, ask and deny lists. A rule names a tool and, optionally, a pattern. Here's a starting point for .claude/settings.json:
{
"permissions": {
"deny": [
"Bash(terraform destroy *)",
"Bash(kubectl delete *)",
"Bash(git push --force *)",
"Read(./.env)",
"Read(./.env.*)"
],
"ask": [
"Bash(terraform apply *)",
"Bash(git push *)",
"Bash(psql *)"
]
}
}
How the rules combine, from the permissions docs: "Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome." A deny always wins, even over a narrower allow. And when several settings files set permissions.deny, Claude Code combines the lists rather than picking one, so a team-wide deny in the project file can't be dropped by a personal allow.
The catch: Bash rules match the command text. The docs are explicit that a rule like Bash(curl *) "isn't a security boundary around the program": it won't catch /usr/bin/curl or sh -c 'curl ...'. A deny rule for terraform destroy won't stop terraform apply -destroy, a wrapper script, or a Makefile target that runs the same thing. Treat these rules as a seatbelt for the commands Claude usually produces, not a wall.
Layer 2: a PreToolUse hook that blocks dangerous commands
Hooks are your own scripts that Claude Code runs at fixed points. A PreToolUse hook runs "before a tool call executes" and "can block it", per the hooks reference. Because it's code, it can check things a pattern can't: SQL inside a quoted string, a flag anywhere in the command, a DELETE with no WHERE.
How a PreToolUse hook works
- Claude Code sends the hook a JSON object on stdin. For Bash, the command is in
tool_input.commandand the tool name intool_name. - Exit code 2 blocks the call, and whatever the hook printed to stderr is fed back to Claude as the reason.
- Exit 0 lets the call go ahead.
- Any other exit code is a non-blocking error: the command still runs. The docs warn that exit 1, the usual Unix failure code, does not block: "If your hook is meant to enforce a policy, use
exit 2."
The hook script
Save this as .claude/hooks/block-destructive.py and make it executable (chmod +x):
#!/usr/bin/env python3
"""PreToolUse hook: block destructive shell commands before Claude Code runs them."""
import json
import re
import sys
# Patterns for commands that destroy data or infrastructure. Tune these to your stack.
BLOCKED = [
(r"\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+(/|~|\$HOME|\*)(\s|$)", "recursive delete of a root, home or wildcard path"),
(r"\b(drop|truncate)\s+(table|database|schema)\b", "dropping or truncating a table, database or schema"),
(r"\bdelete\s+from\s+\w+\s*(;|$|\")", "DELETE without a WHERE clause"),
(r"\bterraform\s+(destroy|apply\s+.*-auto-approve)", "terraform destroy or auto-approved apply"),
(r"\bkubectl\s+delete\b", "kubectl delete"),
(r"\bgit\s+push\s+.*(--force|-f)\b", "force push"),
(r"\bgit\s+reset\s+--hard\b", "git reset --hard"),
]
data = json.load(sys.stdin)
if data.get("tool_name") != "Bash":
sys.exit(0)
command = data.get("tool_input", {}).get("command", "")
for pattern, reason in BLOCKED:
if re.search(pattern, command, re.IGNORECASE):
print(f"Blocked by project hook: {reason}. Ask a human to run this, or find a safer approach.", file=sys.stderr)
sys.exit(2)
sys.exit(0)
We tested it against sample inputs: it blocks psql "$DATABASE_URL" -c "DROP TABLE users", rm -rf ~, terraform destroy, kubectl delete deployment api and DELETE FROM orders, and allows rm -rf ./build, terraform plan, kubectl get pods and DELETE FROM orders WHERE id = 3.
Register the hook
Add it to .claude/settings.json next to your permissions:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.py"
}
]
}
]
}
}
The matcher picks which tools trigger the hook; Bash means only shell commands. CLAUDE_PROJECT_DIR is set by Claude Code to the project root, so the path works from any directory. Commit both files and everyone on the team gets the same guard.
Ask instead of block
Some commands should reach a person rather than be refused. Instead of exiting with 2, a PreToolUse hook can exit 0 and print JSON with a decision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "This migration touches the orders table."
}
}
"deny" cancels the call and tells Claude why, "ask" shows the normal permission prompt, and "allow" skips the prompt. Per the hooks guide, an "allow" from a hook doesn't override deny and ask rules, including managed ones, so a hook can't accidentally open a hole your settings closed.
Hook limits to know
- A regex hook is still matching text. It's much better than a prefix rule, but a determined or confused agent can write a script to a file and run it. Hooks catch the common case; the next two layers catch the rest.
- A hook that times out or crashes doesn't block by default. The hooks reference says not to "count on a stalled hook to act as a gate." Keep hooks fast and simple, and test them with sample JSON the way we did above.
- Hooks run with your user's permissions, outside the sandbox. Review hook scripts like any code that runs on your machine.
Layer 3: turn on the Bash sandbox
Claude Code has an operating-system sandbox for Bash commands, and the sandboxing docs note that it's off by default. Turn it on with /sandbox in a session or "sandbox": { "enabled": true } in settings. It runs on macOS, Linux and WSL2.
With it on, commands can write only to the working directory, a temp directory and folders you add, and network traffic goes through a proxy whose allowed domains start empty. Unlike permission rules, this doesn't depend on how a command is spelled: /usr/bin/curl, sh -c and a script all hit the same wall.
Two things the sandbox doesn't do, per the same page. Reads are allowed across most of the machine by default, including ~/.ssh and ~/.aws/credentials, and commands inherit Claude Code's environment variables, including any secrets in them. So if DATABASE_URL in your shell points at production, a sandboxed psql can still use it if you allow the database host. Which brings us to the layer that matters most.
Layer 4: don't hand the agent production credentials
Every layer above filters what the agent tries. This one limits what it can do if something slips through.
- Keep production secrets out of the agent's shell. Don't start Claude Code in a terminal where
DATABASE_URL,AWS_PROFILEor a kubeconfig context points at production. Use a separate shell or profile for production work. - Give the agent a read-only database role when it needs real data. In PostgreSQL, a role with only
SELECTcan't drop or delete anything, whatever SQL it sends (GRANT docs). Our guide to giving an AI coding agent read-only database access adds timeouts, column limits, replicas and MySQL:
CREATE ROLE agent_readonly LOGIN;
GRANT CONNECT ON DATABASE app TO agent_readonly;
GRANT USAGE ON SCHEMA public TO agent_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_readonly;
- Protect critical infrastructure in code. In Terraform,
lifecycle { prevent_destroy = true }on a resource makes any plan that would destroy it fail (Terraform lifecycle docs). Put it on databases, storage buckets and anything else you can't recreate. - Keep backups you've tested restoring. The Replit data came back because a recovery path existed.
Don't undo it with bypass mode
All of this assumes permissions are on. Claude Code's bypassPermissions mode, which you get with --dangerously-skip-permissions, skips permission prompts. The permissions docs say to use it only "in isolated environments like containers or VMs."
If you run a team, set permissions.disableBypassPermissionsMode to "disable" in managed settings, which users can't override. The same docs describe disableAutoMode for the auto mode, where a classifier approves routine actions instead of you.
A checklist
- Deny rules in
.claude/settings.jsonfor commands that should never run unattended, ask rules for the ones a person should approve. - A PreToolUse hook on
Bashthat exits with 2 on destructive patterns, tested with sample input. - The Bash sandbox turned on, with only the network domains the project needs.
- No production credentials in the agent's environment; read-only roles where it needs real data.
prevent_destroyon infrastructure you can't rebuild, and backups you've restored at least once.- Bypass mode disabled in managed settings for teams.
What these layers can't see
Everything above judges a command by its text or by where it writes. None of it knows that ALTER TABLE orders DROP COLUMN status breaks a billing service in another repo, or that the bucket a script deletes holds last night's backups. Pattern lists can't tell a harmless table from a critical one.
That gap is what we're building VineScale to close. It's a gate that knows your production databases and infrastructure, and warns about or blocks an agent's action based on what it would actually break. The gate for Claude Code is in free beta now. It works best on top of the four layers above, which take less than an hour to set up.
Frequently asked questions
Can Claude Code delete a production database?
Yes, if it can reach one. Claude Code runs shell commands with whatever access your terminal has, so if DATABASE_URL or your cloud profile points at production, a command like DROP TABLE or terraform destroy can run there. Deny rules, a PreToolUse hook and read-only credentials stop that.
How do I block a specific command in Claude Code?
Add a deny rule such as "Bash(terraform destroy *)" to permissions.deny in .claude/settings.json. Deny rules are checked before ask and allow rules, so a deny always wins. Because Bash rules match the command text, back them up with a PreToolUse hook for variants the pattern misses.
Why doesn't my PreToolUse hook block the command?
Most often it exits with code 1. Claude Code treats any exit code other than 0 or 2 as a non-blocking error and runs the command anyway. A hook that enforces a policy must exit with code 2, and what it prints to stderr is passed back to Claude as the reason.
Is --dangerously-skip-permissions safe to use?
Only in an isolated environment such as a container or VM, which is what Anthropic's permissions docs say. It skips permission prompts, so nothing stops to ask you before a command runs. Teams can turn it off for everyone by setting permissions.disableBypassPermissionsMode to "disable" in managed settings.
Does the Claude Code sandbox protect my production database?
Not on its own. The sandbox limits where commands can write and which network hosts they reach, but commands still inherit your environment variables. If the database host is allowed and a production connection string is in your shell, a sandboxed command can still use it. Keep production credentials out of the agent's environment.