# How to stop Claude Code from running terraform destroy

Deny rules and a 12-line PreToolUse hook that stop Claude Code from running terraform destroy or terraform apply -destroy, the wrappers that get past both, and the credential setup that makes the question smaller.

Add these to `.claude/settings.json`:

```json
{
  "permissions": {
    "deny": [
      "Bash(terraform destroy *)",
      "Bash(terraform * destroy*)",
      "Bash(terraform apply *-destroy*)"
    ],
    "ask": ["Bash(terraform apply *)"]
  }
}
```

By the matching rules in Claude Code's docs, that refuses `terraform destroy`, `terraform -chdir=infra destroy` and `terraform apply -destroy`, and prompts before any other apply. We derived these from the docs and have not run them in a live session. It does not see `make teardown`, and it does not cover `tofu` or `terragrunt` unless you repeat the rules for them. It also misses `terraform -chdir=infra apply -destroy`, which the hook below catches.

## Why there are three deny rules

Rules go in `permissions.deny`, `permissions.ask` and `permissions.allow`, in `.claude/settings.json` for the repo or `~/.claude/settings.json` for every project. Deny is checked first, then ask, then allow ([permissions docs](https://code.claude.com/docs/en/permissions)).

Two details that decide whether these rules match:

- A trailing ` *` also matches the bare command, so `Bash(terraform destroy *)` covers `terraform destroy` with no arguments.
- A rule is matched against the command text. `terraform -chdir=infra destroy` does not start with `terraform destroy`, which is why the second rule is there.

Committing the rules to the repo means every teammate who opens it in Claude Code gets them.

## One hook for Terraform, OpenTofu and Terragrunt

One hook covers Terraform, OpenTofu and Terragrunt, and both spellings of destroy. Save it as `~/.claude/hooks/no-terraform-destroy.mjs`:

```js
#!/usr/bin/env node
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
  let cmd = "";
  try { cmd = String(JSON.parse(raw).tool_input?.command ?? ""); } catch {}
  const destroy = /\b(terraform|tofu|terragrunt)\b[^|;&\n]*\s(destroy\b|apply\b[^|;&\n]*\s--?destroy\b)/;
  if (destroy.test(cmd)) {
    console.error("Blocked: this destroys infrastructure. Stop and ask the human to run it.");
    process.exit(2);
  }
});
```

Register it in `~/.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \"$HOME/.claude/hooks/no-terraform-destroy.mjs\"", "onFailure": "block" }
        ]
      }
    ]
  }
}
```

The registration is the same as on the [force-push page](/blog/stop-claude-code-from-force-pushing), which explains exit code 2 and `onFailure`.

## Wrappers and other ways to delete infrastructure

Ten commands, with the script's answer to each:

```
BLOCKED  terraform destroy -auto-approve
BLOCKED  terraform -chdir=infra destroy
BLOCKED  terraform apply -destroy -auto-approve
BLOCKED  tofu destroy
ran      terraform plan -destroy
ran      terraform apply
ran      make teardown
ran      npm run infra:down
ran      terraform state rm aws_db_instance.main
ran      aws rds delete-db-instance --db-instance-identifier prod
```

`terraform plan -destroy` only prints a plan, so letting it run is intended. The rest is the gap:

- `make teardown` and `npm run infra:down` run the destroy from a file. The command text does not say so.
- `terraform apply` with a changed config can delete as much as a destroy. Only the plan tells you. That is what the `ask` rule on apply is for.
- `terraform state rm` and a direct cloud CLI call remove things without Terraform's destroy path at all.

Start with credentials. Give the shell the agent runs a role that can plan and cannot apply, and run apply from CI or your own terminal. The rule and the hook sit on top of that.

## ACP beyond Terraform

ACP's hook covers more tools than the one above. It holds `pulumi destroy`, `cdk destroy` and `kubectl delete namespace`, along with `destroy` and `apply -destroy` on `terraform`, `tofu` and `terragrunt`. With you at the keyboard in Claude Code, the command shows in a prompt and you can say yes. With nobody there, it is blocked until you approve it on the console or from the email. Plans and previews are not held.

It reads command text like the hook above, so `make teardown` gets past it too. Today it also misses two spellings the hook above catches: a credential wrapper such as `aws-vault exec prod -- terraform destroy`, and Terragrunt's older `destroy-all`. Keep the hook above if you use either. What ACP adds for the cases neither catches is the log: every command the agent ran, in order, with the decision next to it.



## Frequently asked questions



## Related

- [How to stop Claude Code from dropping your database](/blog/stop-claude-code-dropping-database)
- [How to stop Claude Code from force-pushing](/blog/stop-claude-code-from-force-pushing)
- [How to block dangerous commands in Claude Code, Codex and Cursor](/blog/block-dangerous-commands-coding-agent)
- [Claude Code hooks reference](/blog/claude-code-hooks-reference)
