# How do I block dangerous commands in Claude Code?

> Use a PreToolUse hook, which Claude Code runs before every tool call whatever the model decides. Immiscible's hook sends each Bash command, file edit, web fetch and MCP call to a rule a person wrote and refuses, asks or allows; it fails closed.

Source: https://immiscible.fly.dev/docs/answers/claude-code-block-commands

Use a `PreToolUse` hook: Claude Code runs it before every tool call, and a hook that exits with code 2 blocks the call whatever the model wanted. Immiscible's hook sends each Bash command, file write, web fetch and MCP call to a rule a person wrote, then refuses it, asks you with the reasons and an approval link, or lets it through; if Immiscible cannot be reached, it refuses.

## How do I install it?

```bash
npx immiscible init
```

In a Claude Code project (one with `.claude/` or `CLAUDE.md`), init shows the change to `.claude/settings.json`, asks, and adds one entry:

```json
{
  "matcher": "Bash|Write|Edit|MultiEdit|NotebookEdit|WebFetch|mcp__(?!immiscible__(check_action_status|explain_decision|spend_summary|find_waste|unwatched_keys)$).*",
  "hooks": [
    {
      "type": "command",
      "command": "node --env-file-if-exists=\"$CLAUDE_PROJECT_DIR/.env\" \"$CLAUDE_PROJECT_DIR/.claude/hooks/immiscible-claude-code-hook.mjs\" || exit 2",
      "timeout": 60
    }
  ]
}
```

Beside it, init adds Claude Code deny rules to the same file, in the same diff, so Claude Code itself refuses the worst commands and reading `.env`, before any hook runs:

```json
"permissions": {
  "deny": ["Bash(rm -rf:*)", "Bash(rm -fr:*)", "Bash(sudo rm:*)", "Bash(git push --force:*)", "Bash(git push -f:*)", "Bash(git reset --hard:*)", "Bash(git clean -f:*)", "Read(./.env)", "Read(./.env.*)"]
}
```

The matcher covers every Bash command, file change, web fetch and MCP call, except Immiscible's own read-only tools (asking Immiscible whether the agent may ask Immiscible would only loop). `|| exit 2` makes a missing file or a crash block the call instead of letting it through. Without the CLI: `npm install -g @immiscible/claude-code-hook` and `immiscible-claude-code-hook --print-config`. Check it with `npx immiscible doctor`, which also runs the hook against an address nothing answers on to prove it refuses.

## How do I decide which commands are allowed?

Write a rule for the agent's tool calls that names the domains it may reach, such as `github.com`, `registry.npmjs.org` and your own. A shell command that posts to anywhere else is then refused (`recipient_not_allowed`), and an MCP server not named (`mcp:github`, or `mcp:*`) is refused too. In the console: **Agents**, **Agent limits**, **Add a rule**, an action rule for `tool.call` with those domains. See [mandates](https://immiscible.fly.dev/docs/concepts/mandates.md#action-mandates).

## How is this different from Claude Code's permissions deny list?

Claude Code's own `permissions.deny` rules match tool names and patterns in your settings, and they are the right first layer: use them. The hook adds what a pattern cannot: a decision that knows where a command sends data, asks a named person and waits, counts bursts of calls, applies one rule across Claude Code and your other agents, can be frozen from the console or Slack, and leaves a signed record. The two work together; an `allow` from the hook still goes through Claude Code's own permissions.

## Can it stop rm -rf?

Yes, in three layers. Claude Code's own deny rules, which init adds, refuse `rm -rf`, force pushes and reading `.env` before the hook runs. Then, under every Immiscible rule and at every standing, a destructive command (`rm`, `git push --force`, `git reset --hard`, `git clean`, a history rewrite, `curl ... | sh`, writing over `~/.bashrc` or `.git/hooks`) asks a person, and so do any `git push`, a deploy, a publish, a package install from the network, anything run with `sudo` and anything that names a path outside the project. A command too long to send whole, spelt in escape codes or built from a variable asks too. A secrets file or the environment leaving the machine is refused outright. Under the default rule, General tasks, an intern asks before anything that is not provably read-only; once the agent is past its intern stage, edits, test runs and builds inside its project go ahead (`localWrites: "allow-after-intern"`, shown on the agent's page, and an owner can set it to `ask`).

A pattern can still miss a determined disguise, and a program the agent runs can delete files by itself. Run the agent where a mistake is recoverable (a container, a branch, a backup), and keep the hook for what crosses a boundary as well: network calls, MCP tools, payments and data.

## How do I apply it to a whole team?

Commit `.claude/settings.json` and `.claude/hooks/immiscible-claude-code-hook.mjs` to the repository, and give each person their own agent key in `.env` (`npx immiscible init` per person). For a policy people cannot turn off, Claude Code's managed settings can carry the same hook entry. Every person's agent then shares one set of rules, one kill switch and one record in the console.

## What does it not do?

- A new agent starts at the workspace's starting tier: intern, unless an owner has chosen junior in the console. Under the default rule, an intern asks a person before every tool call that changes something, except read-only calls (`ls`, `git status`, reading a file), which go ahead and are recorded. A junior agent edits files and runs tests and builds inside its project without asking; pushes, deploys, publishes, installs, destructive commands and anything outside the project still ask. The project is the one the hook names (`CLAUDE_PROJECT_DIR`, or the working directory); an older hook that does not send the project gets a person for its edits. See [autonomy tiers](https://immiscible.fly.dev/docs/concepts/autonomy-tiers.md) for how an agent earns the next rung.
- It sees what Claude Code passes to the hook: the tool, its input and the session. It does not read the model's reasoning.
- Claude Code's hosted inference is not enforced by the hook; to meter Claude Code's model spend, point it at the gateway with `ANTHROPIC_BASE_URL=https://immiscible.fly.dev/anthropic`. See [one budget across providers](https://immiscible.fly.dev/docs/ai-agents.md#one-budget-across-openai-and-anthropic).
- The hook's tool calls are limited to 300 a minute per agent (`IMMISCIBLE_HOOK_RPM` on your own server), apart from the agent's other requests; past it the hook refuses, because it fails closed. Tool calls also have their own burst line, 200 in 10 minutes by default, after which a person is asked; see [how many tool calls before a person is asked](https://immiscible.fly.dev/docs/guides/mcp-proxy.md#how-many-tool-calls-before-a-person-is-asked).

The full reference is [the Claude Code hook](https://immiscible.fly.dev/docs/guides/mcp-proxy.md#the-claude-code-hook).
