Claude Code will run rm -rf, a force push, or a curl | bash install script the moment it decides that's the right next step — nothing stops it by default except a permission prompt you can approve, silence, or bypass entirely. This guide covers the three real levers for stopping that: settings.json permission rules, PreToolUse hooks, and sandboxing — plus where a local observability tool like Beam fits (and doesn't).
TL;DR
| Question | Answer |
|---|---|
| Does Claude Code block dangerous commands automatically? | Partially. Manual mode prompts before most Bash commands, but a saved "don't ask again" rule, acceptEdits, bypassPermissions, or --dangerously-skip-permissions all remove that prompt. |
| What's the most reliable way to block a specific command? | A PreToolUse hook script that exits with code 2 — this always blocks the call regardless of any permission rule. |
| Do I need to write code? | For a settings.json deny rule, no. For pattern matching beyond simple text (regex, path checks, logging), yes — a short shell or Node script. |
| Is a deny rule alone enough? | No. Anthropic's own docs note a rule like Bash(rm -rf *) matches command text, not the program — bash -c "rm -rf build/" slips past it. Pair rules with a hook for anything you actually depend on. |
Does --dangerously-skip-permissions block anything? | No — it removes the permission system entirely. Use it only inside a container or VM, never on your host. |
| Does my data leave my machine if I add Beam? | No by default. Beam's collector binds to 127.0.0.1, redacts known credential formats before persistence, and Sentinel v1 does not implement blocking — it observes and flags. |
The three layers that actually matter
Claude Code doesn't have a single "dangerous command" toggle. It has three independent mechanisms that compose:
- Permission rules (
settings.json) — allow/deny/ask patterns matched against command text. - Hooks (
PreToolUse/PostToolUse) — your own script, run before or after a tool call, with the power to block via exit code. - Sandboxing — OS/container-level isolation that doesn't depend on command text at all.
Each catches something the others don't. None of them is optional if you're letting Claude Code run with real filesystem and shell access.
Layer 1: permission rules in settings.json
By default (Manual mode, aliased default), Claude Code prompts before Bash commands, file edits, and most tool calls — except a built-in set of read-only commands (ls, cat, grep, find, read-only git, and similar). Rules live under a permissions block in .claude/settings.json (project, shareable), .claude/settings.local.json (project, gitignored), or ~/.claude/settings.json (user-level), and are evaluated in a fixed order: deny, then ask, then allow — the first match wins regardless of how specific a later rule is.
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(git commit *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(curl * | bash)",
"Bash(curl * | sh)"
]
}
}
A few mechanics worth knowing before you rely on this:
- A bare tool name removes the tool entirely.
"deny": ["Bash"]means Claude never sees Bash as an option, versus a scoped rule likeBash(rm -rf *), which leaves Bash available and blocks only matching calls. - Wildcards are text matches, not semantic ones.
Bash(git push *)blocksgit push origin mainbut notgit -C . push origin mainorgit 'push' origin main— Anthropic's own permissions documentation states this explicitly and recommends aPreToolUsehook for anything that needs to hold up against a differently-formatted invocation. - Compound commands are split and checked independently. Claude Code recognizes
&&,||,;,|, and newlines as separators, so a deny rule onrm -rfstill catchescd /tmp && rm -rf *. - Command substitution and subshells are covered too — an ask/deny rule matches a subcommand nested inside
$(...)or aforloop, not just the top-level command.
Manage rules interactively with /permissions inside a session — it lists every active rule and which settings file it came from, and lets you add or remove rules live.
The permission modes
| Mode | What it does |
|---|---|
default / manual | Prompts on first use of each tool per session (the safe default) |
acceptEdits | Auto-accepts file edits and common filesystem commands (mkdir, touch, mv, cp) in the working directory |
plan | Read-only exploration; no edits, no non-read-only shell commands |
auto | Auto-approves tool calls behind a background safety classifier |
dontAsk | Auto-denies anything that would otherwise prompt (opposite failure mode of bypass) |
bypassPermissions | Skips permission prompts almost entirely, including writes to protected paths like .git and .claude |
bypassPermissions is the one to be deliberate about. It's a legitimate mode for CI or a disposable container, but Anthropic's own docs carry an explicit warning: use it only in an isolated environment where Claude Code can't cause damage. The CLI flag --dangerously-skip-permissions does the same thing from the command line and carries the same warning — the name is not decoration.
Layer 2: PreToolUse hooks — the lever that actually blocks
Permission rules are pattern matching on text. A PreToolUse hook is a program you run — it receives the full proposed tool call as JSON on stdin, can inspect it with real logic, and can block it outright. This is the most concrete, actionable control a reader can wire up today, and it's the one layer that reliably survives command text you didn't anticipate.
The hook fires before the tool call executes. Configuration goes in the same settings.json hooks block:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/block-dangerous.sh
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -Eq 'rm -rf|git push (-f|--force)|curl[^|]*\| *(bash|sh)'; then
echo "Blocked by PreToolUse hook: destructive command pattern" >&2
exit 2 # exit 2 always blocks the tool call, even if JSON says "allow"
fi
exit 0 # no decision; normal permission flow applies
Make it executable (chmod +x .claude/hooks/block-dangerous.sh). Two things worth knowing about how this resolves:
- Exit code 2 is the actual block. It overrides any JSON output, including a stray
"permissionDecision": "allow". A non-zero, non-2 exit is a non-blocking error — the action proceeds unless the hook's JSON explicitly supplies a decision. - The alternative to a bare exit code is structured JSON on stdout, when you want a reason string surfaced back to Claude:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Force pushes are blocked in this repo. Open a PR instead."
}
}
PostToolUse uses the same structure but fires after the tool call has already run — useful for logging or flagging, not for preventing the action. If you want to observe every tool call without blocking anything, use PostToolUse (or a PreToolUse hook that always exits 0); if you want to actually stop a command, it has to be PreToolUse with exit code 2.
Layer 3: sandboxing — when text matching isn't enough
Permission rules and hooks both operate on command text. Neither one is a security boundary against a command invoked in an unexpected form (/bin/rm instead of rm, a script wrapping the call, a differently-quoted git push). For enforcement that doesn't depend on parsing the command string at all, Claude Code's documentation points to OS/container-level sandboxing — filesystem and network isolation that holds regardless of how the command is phrased.
The practical version most teams reach for is a devcontainer: a disposable, network-restricted container where --dangerously-skip-permissions becomes reasonable precisely because the blast radius is the container, not your host. Even then, a devcontainer doesn't stop a malicious project from exfiltrating anything reachable inside that container — including Claude Code's own stored credentials — so treat sandboxing as containment, not a replacement for permission rules and hooks.
Where Beam fits
Beam is a local, privacy-first observer, not a blocker. It pairs with Claude Code the same way it pairs with any agent: through the hook mechanism above, but pointed at Beam's own collector instead of (or alongside) your blocking logic.
Per apps/sentinel-collector/README.md, the collector's Claude Code integration works like this: its CLI accepts a PreToolUse or PostToolUse JSON payload on stdin, sends a bounded request to a local collector process, and emits no allow/deny response — capture failure is reported on stderr and never stops the agent. You wire it in as its own hook entry:
{
"hooks": {
"PreToolUse": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "bun /absolute/path/to/apps/sentinel-collector/src/cli.ts hook claude-code"
}]
}]
}
}
What that buys you: every proposed tool call gets logged locally and run against Beam's heuristic scanner — 11 patterns plus an MCP version-pin check, covering credential references, deletion, downloaded-and-executed code, network sweeps, privilege changes, and reverse-shell patterns, among others. Flagged events show up in Activity, where you inspect the evidence and mark them reviewed.
What it does not do: block, deny, or prevent anything. "Sentinel v1 does not implement blocking," per the same README. If a command is destructive, Beam will show it to you after the fact (or as it happens, if you're watching); it will not stop it from running. That's the honest scope — pair it with a real PreToolUse deny hook (Layer 2 above) if you need actual enforcement, and use Beam for the visibility layer on top: what actually ran, across sessions, with the evidence to review later.
Everything Beam captures stays local by design. The collector binds to 127.0.0.1:4319, persists to apps/sentinel-collector/.data (directory mode 0700, files 0600), and redacts known credential formats, key/value assignments, auth headers, and URL query parameters before anything is written to disk — detection runs on the raw text first, so redaction happens before persistence, not instead of capture. Retention is capped at the latest 10,000 events and 500 reports. Nothing leaves the machine unless you explicitly export or forward it yourself.
What none of this catches
Stated plainly, because a security guide that hides its own gaps isn't a useful one:
- Permission rules match text, not intent. A sufficiently unusual invocation of a blocked command can slip past a deny rule. This is documented Claude Code behavior, not a bug you can file.
- Hooks run your logic, so they're only as good as your logic. A regex that misses
sudo rm -rfbecause you only checked for barerm -rfis a gap you introduced. - Beam's scan is heuristic, not semantic malware analysis. It catches known shapes — a curl-pipe-to-bash pattern, a credential-delivery instruction — not novel obfuscation. It's a second set of eyes, not a guarantee.
- None of this replaces reading the diff. Permission rules, hooks, and Beam's flags all reduce how often you need to look closely. They don't replace looking closely at anything genuinely irreversible — a force push to
main, a production database command, a credential rotation.
Practical setup, in order
- Start with deny rules in
.claude/settings.jsonfor the obvious destructive patterns —rm -rf, force pushes,curl | bash. This is a five-minute change and stops the common accidental case. - Add a
PreToolUsehook for anything you actually depend on holding — broader regex coverage, protected-path checks, or forwarding events somewhere you can review them (Beam's collector, your own logging). - If you're going to run with
bypassPermissionsor--dangerously-skip-permissionsfor velocity, do it inside a devcontainer or disposable VM, never on your host, and assume anything reachable inside that container — including stored credentials — is in scope if the project turns out to be malicious. - Pair the above with local observability so you have evidence to review after the fact, not just prevention before it.
If you're running Codex instead of Claude Code, the same three-layer approach applies with different config surfaces — see stopping Codex from running dangerous commands. Cursor users should read stopping Cursor from running dangerous commands, which covers Cursor's own permission and rules setup. For the underlying concept behind all of this — what a hook actually is, and why every agent harness ends up needing one — see what are agent harnesses. If you're deciding between agents in the first place, our safety comparison of Cursor, Claude Code, and Copilot covers how their permission models differ.
Summary
Claude Code gives you three real levers against dangerous commands: settings.json deny/ask rules for the common cases, PreToolUse hooks with exit-code blocking for anything that needs to hold up against text you didn't anticipate, and sandboxing for enforcement that doesn't depend on command text at all. None of them is automatic — you configure all three yourself. Beam adds a fourth layer on top: a local, heuristic observer that logs and flags what actually happened, with everything redacted and stored on-device, but it does not block anything in v1 — pair it with a real deny hook if enforcement is the goal.
Claude Code's permission rules, hook payloads, and CLI flags described here reflect Anthropic's documentation as of September 2026 and are subject to change in future releases — verify against code.claude.com/docs before relying on exact syntax in production. Beam's own capabilities are accurate as of Sentinel collector v0.1, a local prototype.