The short answer
Don’t give the agent credentials that can apply. Let it run terraform plan and hand you the plan; you run the apply. Add prevent_destroy to the resources you can’t lose, put a pre-command hook in the agent that asks before terraform destroy, and attach an explicit deny for delete actions to whatever cloud role the agent does get. Each layer catches something the others miss.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
Why -auto-approve is the risk
terraform destroy deprovisions every object a configuration manages. It is only an alias for terraform apply -destroy, so blocking one spelling and not the other does nothing.1 Normally terraform apply prompts you to approve the plan. With -auto-approve it applies without asking, and HashiCorp’s own page notes that this includes destructive operations such as deleting resources.2
Coding agents don’t sit at an interactive prompt. When an agent hits Terraform’s confirmation, the obvious fix from its point of view is to add -auto-approve and run again. So the prompt that protects a person at a terminal is the first thing an agent learns to skip. The same goes for Pulumi: pulumi destroy --yes approves and performs the destroy after the preview, and --skip-preview drops the preview too.6
Layer 1: let the agent plan, not apply
The cleanest split is by credential. The agent gets credentials that can read state and refresh, and runs:
Code Commands typed in a terminal. The text around it says what happens.
terraform plan -out=tfplan
terraform show -no-color tfplan > plan.txt
You read plan.txt and run terraform apply tfplan yourself, with credentials the agent never sees. If you want to see what a destroy would do, terraform plan -destroy shows the proposed destroy changes without executing them.1
This is the layer that does the most work, because it doesn’t depend on reading the command. An agent with read-only cloud credentials can type terraform destroy -auto-approve and the cloud API refuses each delete.
Layer 2: prevent_destroy and protect
Mark the resources you can’t lose:
Code What the screen or file shows, copied as is.
resource "aws_db_instance" "main" {
# ...
lifecycle {
prevent_destroy = true
}
}
With this set, Terraform rejects any plan that would destroy the object and returns an error, and terraform destroy stops working for the configuration.3 Pulumi’s equivalent is the protect resource option: a protected resource can’t be deleted directly, and a deployment that tries is an error.5
Know the gaps before you lean on them:
- Deleting the block defeats it.
prevent_destroydoes not stop Terraform destroying a resource if you remove its configuration.3 An agent that “cleans up unused resources” by deleting the block, then applies, gets past it. - It’s enforced by the tool, not the cloud. Pulumi’s
protectnever reaches the cloud provider.5 Turn on the provider’s own deletion protection too (RDS, Cloud SQL and others have one). terraform state rmisn’t a destroy. It removes the binding to the real object without destroying it.4 The database keeps running, but Terraform no longer manages it, which is its own kind of mess to clean up.
Layer 3: a pre-command hook
A hook in the agent sees the command before the shell does. We ran Deedglass’s shell guard from 0.13.0 against the Terraform and Pulumi commands an agent is likely to type. “asks” means exit 1 and a decision for you; “nothing” means exit 0 and the agent’s own rules decide.8
| Command | Answer |
|---|---|
terraform destroy |
asks |
terraform apply -destroy -auto-approve |
asks |
terraform destroy -target=aws_instance.web |
asks |
pulumi destroy --yes |
asks |
terraform plan -destroy |
nothing (it only plans) |
terraform state rm aws_db_instance.main |
nothing |
Code What the screen or file shows, copied as is.
$ npx deedglass@0.13.0 guard shell --no-files --command 'terraform apply -destroy -auto-approve'
Deedglass asks first: terraform apply -destroy destroys every resource the configuration manages.
terraform-destroy terraform apply -destroy destroys every resource the configuration manages
The Terraform and Pulumi rows are two of the catalog’s 55, each with its source and the date it was checked.8 npx deedglass protect installs the guard in Claude Code, Cursor, Codex CLI, Gemini CLI and Copilot CLI in one step.8 Our walkthrough shows the files it writes.
What a hook can’t see matters just as much. It reads text, so a value known only at run time, such as a Makefile target, a script or an alias, passes untouched.8 make teardown that runs terraform destroy inside it gets no question. And if the hook can’t run, because of a timeout or a missing binary, the command goes ahead unchecked.8 One more limit from the table above: a targeted destroy of one resource gets the same ask as a full one, so the reason the guard gives (“every resource the configuration manages”) overstates a -target run. Read the command, not just the reason.
Layer 4: credentials that can’t delete
If the agent must hold cloud credentials, attach an explicit deny for the delete actions you care about. In AWS, an explicit deny in an identity policy, a permissions boundary or an SCP overrides any allow.7 A sketch for an agent’s role:
Code Data in JSON, the format software uses to pass records and settings.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AgentNeverDeletes",
"Effect": "Deny",
"Action": [
"rds:DeleteDBInstance",
"rds:DeleteDBCluster",
"s3:DeleteBucket",
"ec2:TerminateInstances",
"dynamodb:DeleteTable"
],
"Resource": "*"
}
]
}
This is the only layer that holds when the command never passes through the agent’s shell tool at all, for example when Terraform runs from a script the agent wrote, or when the agent calls the cloud SDK from Python. Put it in a permissions boundary or SCP, not just the role, if the agent can edit its own IAM policies.
Which layer stops what
| Situation | Plan-only access | prevent_destroy / protect | Pre-command hook | Explicit deny |
|---|---|---|---|---|
terraform destroy -auto-approve typed by the agent |
stops it | stops it | asks first | stops it |
| Agent deletes the resource block, then applies | stops it | misses it | misses it (it’s an apply) | stops it |
make teardown wrapping a destroy |
stops it | stops it | misses it | stops it |
| Agent calls the cloud SDK directly | stops it if the keys are read-only | misses it | misses it | stops it |
terraform state rm |
allowed | misses it | misses it | not a delete |
No single column is all “stops it”. The hook is the cheapest to add and gives the clearest prompt, but the credential layers do the heavy lifting. If you only do one thing today, take apply rights away from the agent. Then add the hook so that the commands it does run get a second look.
Related reading: the same approach for AWS CLI deletes and for kubectl delete, what blast radius means for an agent’s credentials, and the checklist before an agent touches production.
To check a command yourself, run npx deedglass guard shell --no-files --command 'terraform destroy'. It reads the command, never runs it, and needs no account. To grade what an agent can reach on your own machine, run npx deedglass scan (what the grade means). To watch the checks without installing anything, open the playground.
Written with AI assistance and checked against our claims register.