Skip to content

All posts

AI

How to block destructive commands in Claude Code

Ritesh Patel

AI: 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.

  1. Create or open .claude/settings.json at the project root.
  2. Add a deny list under permissions. Each entry names a tool and a pattern.
  3. Save the file, then run /status in 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 dist is blocked by Bash(rm *).
  • Nested commands: deny rules also apply inside a subshell, a command substitution or a for loop body.
  • Program paths and wrappers: the rule matches the text Claude writes. /bin/rm -rf dist and sh -c "rm -rf dist" are not matched by Bash(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.

  1. Save a script in .claude/hooks/. The docs example below blocks any command that starts with rm.
  2. Make the script executable with chmod +x.
  3. Register it under hooks in .claude/settings.json, with a matcher of Bash.
#!/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.

  1. Write the policy to the managed file path for the platform.
  2. Deploy it with your device management tool, or copy it to the path directly for a single machine.
  3. 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.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Govern AI agents with AIControlsControl what AI agents do, and prove it.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.