How to block destructive commands in Claude Code

To block destructive commands in Claude Code, add Bash deny rules such as Bash(rm *) to a settings file. Deny rules are checked before allow rules, so no allow entry overrides them. Deny rules match the command text Claude writes, so add a PreToolUse hook for more coverage, and deploy both as managed settings so a developer cannot remove them.
Add deny rules to project settings
Deny rules live under the permissions key. Claude Code reads the shared project file at .claude/settings.json, so commit it and every developer on the repository gets the same rules. The steps below were checked against the Claude Code settings and permissions docs as of 2026-10-24. The docs do not state a CLI version, so confirm against your installed release.
- Create or open
.claude/settings.jsonat the project root. - Add a
denylist underpermissions. Each entry names a tool and a pattern. - Save the file, then run
/statusin Claude Code to confirm the project settings loaded.
{
"permissions": {
"deny": [
"Bash(rm *)",
"Bash(sudo *)",
"Bash(git reset --hard *)",
"Read(./.env)"
]
}
}
A trailing * with a space before it also matches the bare command, so Bash(rm *) covers rm and rm -rf build/. Deny rules also match past a leading variable assignment, so FOO=bar rm -rf tmp/ is blocked too. The Read(./.env) entry blocks Claude’s file tools from reading that file.
Know what a deny rule covers
Deny rules are the fastest layer to set up, and they have a clear scope. The docs describe three behaviors that matter for destructive commands.
- Compound commands: a rule must match each subcommand, and deny applies when any subcommand matches.
cd build && rm -rf distis blocked byBash(rm *). - Nested commands: deny rules also apply inside a subshell, a command substitution or a
forloop body. - Program paths and wrappers: the rule matches the text Claude writes.
/bin/rm -rf distandsh -c "rm -rf dist"are not matched byBash(rm *).
The last point is the gap that matters most. The docs state that a Bash rule “isn’t a security boundary around the program.” Treat deny rules as a guard against the usual invocation, then use the layers below for the rest.
Add a PreToolUse hook as a second layer
A PreToolUse hook runs before each tool call and can block it. The hook receives the call as JSON on stdin, with tool_name and tool_input fields. Exit code 2 blocks the call, and the text the hook writes to stderr is shown to Claude.
- Save a script in
.claude/hooks/. The docs example below blocks any command that starts withrm. - Make the script executable with
chmod +x. - Register it under
hooksin.claude/settings.json, with amatcherofBash.
#!/bin/bash
# Reads JSON input from stdin, checks the command
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed" >&2
exit 2 # Blocking error: tool call is prevented
fi
exit 0 # No decision: the normal permission flow applies
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
The example checks only the start of the whole command string. It does not catch cd build && rm -rf dist, because that string starts with cd. A hook for production use should split the command on &&, ||, ; and |, then test each part. The deny rule already does that split, which is why the two layers work better together.
Compare the layers
Each control runs at a different point and can be changed by different people. Use the table to decide where a rule belongs.
| Control | Where it lives | When it runs | Can a developer change it? |
|---|---|---|---|
| Deny rule | .claude/settings.json or ~/.claude/settings.json |
Before each Bash call, on every subcommand | Yes, for their own user file. A committed project file changes only through a commit |
| PreToolUse hook | hooks in the same files, plus a script |
Before the tool call runs; can’t override a deny rule | Yes, for user and project hooks. disableAllHooks set in user settings turns them off |
| Managed deny rule | managed-settings.json on each machine |
Same as deny rules, at the highest precedence | No. Nothing in user, project, local or --settings overrides it |
| Managed hook | Managed settings | Same as hooks | No, unless a managed disableAllHooks is set |
The managed rows are the only ones a developer cannot override by editing a file. The docs note that a developer with local admin rights can edit the managed file itself, so pair managed settings with device management that redeploys the file.
Deploy the policy as managed settings
Managed settings apply to every project on the machine, and no lower level can override a managed deny. Put the file in the system directory for each operating system.
- Write the policy to the managed file path for the platform.
- Deploy it with your device management tool, or copy it to the path directly for a single machine.
- Restart Claude Code and confirm the source is active, as described in the next section.
| Platform | Managed settings file |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.json |
| Linux and WSL | /etc/claude-code/managed-settings.json |
| Windows | C:\Program Files\ClaudeCode\managed-settings.json |
Managed permission rules combine with user rules instead of replacing them. Keep the destructive-command deny list in the managed file, and keep project-specific allow lists in the project file.
Check that the rules loaded
Run /status inside Claude Code. The Setting sources line lists each settings file loaded for the session, such as User settings or Project local settings. It confirms which files were read, not which file supplied each key. To see entries Claude Code rejected, including a malformed deny pattern, run claude doctor in a shell.
A rule that does not match anything also gives no error during normal use, so test with a harmless command. Ask Claude to run rm -rf on a temporary directory. The call should be refused before it runs.
Next steps
Start with the project deny list, commit it, then add the hook for the forms the rule misses. Move the same deny list to managed settings once the team agrees on it. For a broader view of how these controls fit into agent governance, see governance for AI coding agents, which covers the controls that settings files can’t, and Understanding AI Governance: A Market Map.
Enforcing this with AIControls
Settings files control one machine at a time. A gateway sits in front of the model API, so it covers every session that routes through it. AIControls supports Claude Code through its gateway for both the CLI and VS Code, and it provides per-user and per-session cost and budget limits. Those controls complement the file-based rules above. Download AIControls to run it in your own cluster.
Frequently asked questions
Does a deny rule stop Claude Code from running rm -rf?
Yes, for the form Claude usually writes. A deny rule such as Bash(rm *) blocks rm commands before any allow rule is checked, and it also matches rm when a command is chained with && or ;. It does not match the same program called by another path, such as /bin/rm, or wrapped inside sh -c, so pair it with a PreToolUse hook for those cases.
Can an allow rule override a deny rule in Claude Code?
No. Claude Code checks deny rules first, then ask, then allow. The first matching rule decides the outcome, and an allow rule cannot carve an exception out of a deny rule. Rule specificity does not change that order, so a broad deny beats a narrow allow.
Where should I put deny rules so the whole team gets them?
Put them in .claude/settings.json at the project root and commit the file. Every developer who opens the repository gets the rules. For a rule that no developer can remove, deploy it as managed settings to each machine, because managed settings sit above user, project and local settings.
Do hooks and deny rules conflict?
They stack. A PreToolUse hook that exits with code 2 blocks the call, and Claude sees the hook's stderr as the reason. Claude Code evaluates deny rules whatever the hook returns, so a deny rule still blocks even when a hook returns allow. A hook can add restrictions but cannot remove a deny rule.