AI coding agent admin guide·verified October 21, 2026

block terraform destroy for ai coding agents

Deny terraform destroy and local applies from coding agents while keeping plan, validate and fmt available. Policy code and a test table included.

the short answer

Add a pre-tool policy that denies terraform destroy, apply -destroy, local applies and state changes, and lets plan, validate, fmt, init, show and workspace list/select through. It only reads terraform in command position, so grep terraform or a commit message is not blocked. The deny reason tells the agent to save a plan and open a pull request so CI applies it after review. Blocking the whole Terraform CLI is simpler but stops useful work; a subcommand allowlist keeps both safety and speed.

Deny
destroy, any apply, import, state changes, unknown subcommands
Allow
plan (including plan -destroy), validate, fmt, init, show, output
Redirect
The deny reason points the agent at plan plus pull request
Built-in alternative
block-terraform (opt-in), reviewable by production-infra-change
Event
PreToolUse on the shell tool

Why Destroy Needs a Hard Block

terraform destroy is an alias for terraform apply -destroy: both remove every resource in the configuration. In a developer shell with a production backend configured, one agent call can take down a whole stack. Most teams already require Terraform applies to go through CI with review; a coding agent running locally skips that path entirely unless something stops it.

Policy: Plan Locally, Apply in CI

It reads the subcommand of each Terraform, OpenTofu or Terragrunt invocation in the command, so cd infra && terraform destroy and terraform fmt && terraform validate are judged correctly. Applies are denied rather than “instructed”: an instruct decision lets the call run with guidance, which is not what you want for a production apply. Pair it with the script-write rule from how agents get around deny rules so a teardown.sh containing the same command is refused before it exists.

import { customPolicies, allow, deny } from "failproofai";

// Finds each terraform/tofu/terragrunt invocation in command position (start of
// a segment, after sudo/env/time or bash -c, quoted or not) and reads its
// subcommand, skipping global flags such as -chdir=infra/prod. A mention in
// grep, brew or a commit message is not an invocation.
const INVOCATION = /(?:^|[;&|(`\n]|\b(?:sudo|env|time|exec|nohup|xargs)\s|\s-c\s)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*["']?(?:\S*\/)?(?:terraform|tofu|terragrunt)["']?((?:\s+-[\w-]+(?:=\S+)?)*)\s+([a-z][\w-]*)(?:\s+(?:-\S+\s+)*([a-z][\w-]*))?/g;
const READ_ONLY = new Set(["plan", "validate", "fmt", "init", "show", "output", "providers", "version", "graph", "console"]);
const WORKSPACE_READ = new Set(["list", "show", "select"]);

customPolicies.add({
  name: "terraform-plan-only",
  description: "Agents may plan Terraform; applies and destroys go through CI",
  match: { events: ["PreToolUse"] },
  fn: async (ctx) => {
    if (ctx.toolName !== "Bash") return allow();
    const cmd = String(ctx.toolInput?.command ?? "");
    for (const [, , sub, next] of cmd.matchAll(INVOCATION)) {
      if (sub === "destroy" || (sub === "apply" && /\s-destroy\b/.test(cmd))) {
        return deny("terraform destroy is not allowed from agents. Describe the change in a PR.");
      }
      if (sub === "apply" || sub === "import" || (sub === "state" && next !== "list" && next !== "show")) {
        return deny("Agents do not change Terraform state locally. Save a plan with terraform plan -out=tfplan and open a PR; CI applies after review.");
      }
      if (sub === "workspace" && !WORKSPACE_READ.has(next)) {
        return deny(`terraform workspace ${next ?? ""} changes state. Ask a human to run it.`);
      }
      if (sub === "workspace" || sub === "state" || READ_ONLY.has(sub)) continue;
      return deny(`terraform ${sub} is not on the agent allowlist. Ask a human to run it.`);
    }
    return allow();
  },
});

Test Cases to Run Before Enforcing

Run them with fp policies test ./terraform-plan-only.policy.mjs --command "terraform apply" --expect deny, one row at a time, and backtest the draft in Admin → policy editor against the last 30 days of fleet traffic to count the working calls it would have interrupted.

CommandExpected
terraform plan -out=tfplanallow
terraform -chdir=infra/prod planallow
terraform plan -destroyallow
terraform fmt -recursive && terraform validateallow
terraform state listallow
terraform destroy -target=aws_route53_record.cdndeny
terraform apply -destroydeny
terraform apply -auto-approve tfplandeny
terraform applydeny
terraform state rm module.cdndeny
cd infra && terraform destroydeny
tofu applydeny
"terraform" destroydeny
bash -c "terraform destroy -auto-approve"deny
terraform workspace delete -force proddeny
terraform workspace select staging && terraform planallow
grep -rn terraform infra/allow
git commit -m "terraform apply fix"allow
brew upgrade terraform tflintallow

Or Use the Built-in Policy with a Decision Model

The coding agent pack includes block-terraform, which is off by default and denies the whole CLI, plan included. It is marked reviewable by the production-infra-change semantic check: with Jev on and the FailproofAI/jev-policies pack installed, read-only calls like plan and validate are cleared while mutations against live infrastructure stay denied. Jev comes with Failproof: connect with a machine key carrying jev:evaluate, or run failproofai jev setup.

failproofai policies add FailproofAI/policies --policy block-terraform
failproofai policies add FailproofAI/jev-policies
failproofai jev status          # confirm Jev is on

Scope It by Directory or Persona

  • Use ctx.session?.cwd to apply the strict rule only when the session runs under your production stack directories. It is the session’s working directory, so cd infra/prod && … inside one command does not change it.
  • Deploy the strict version to admin and on-call machines first, where the blast radius is largest. See different rules per team.
  • Keep the deny reason actionable. An agent told exactly what to do instead stops looking for another route.

FAQ

Does this stop Terraform Cloud runs?

No. It gates commands the agent runs locally. Remote runs triggered by a merged PR are governed by your Terraform Cloud or CI permissions.

What about `terraform plan -destroy`?

Allowed: it only produces a plan, which is useful evidence for a human reviewer.

Does the same rule work in Codex and Cursor?

Yes. Failproof normalizes each harness’s shell tool to Bash with a command field, so one policy covers every supported harness at the pre-tool gate.

Sources

Checked against the sources below on October 21, 2026. Harness settings, hook capabilities and cloud features change between versions.

  1. HashiCorp: terraform destroy command reference
  2. HashiCorp: terraform apply command reference
  3. Failproof AI docs: Custom policies (policy SDK)
  4. Failproof AI docs: Test a policy
  5. Failproof AI docs: Use a policy pack
  6. Failproof AI docs: Policy authority
  7. Failproof AI docs: Jev policies
  8. Failproof AI docs: Agent harnesses and enforcement capability