Terraform and AI coding agents: destroy is only as safe as your state file
On February 26, 2026, an AI coding agent ran terraform destroy
against the production infrastructure behind DataTalks.Club, a data
engineering course platform. The agent had switched to a new computer
mid-migration and started without a local copy of the Terraform state
file. It ran terraform plan, saw what looked like a clean slate, and
applied a second copy of the entire stack next to the one already
serving real traffic. Cleaning up the resulting duplicates, in the
agent's own words, would be "cleaner and simpler" through Terraform
than through the AWS CLI.
It was right about the tool and wrong about what the tool now
believed. Once the original state file resurfaced and gave Terraform,
in the founder's own account,
"a consistent view of infrastructure," that view included both stacks
as equally real. terraform destroy doesn't know which one is the
duplicate. It deletes whatever state says exists: the VPC, the ECS
cluster, both load balancers, the bastion host, the RDS database, and
every automated snapshot, in one run. A single table, courses_answer,
held 1,943,200 rows of 2.5 years of course submissions.
The command did exactly what it was told. The state file is what was wrong, and nothing about running the command said so.
Why terraform plan can look clean and still be wrong
A terraform apply against real infrastructure is the operational
cousin of a migration against a real database:
a clean plan and correct syntax guarantee the tool will do exactly
what it's told, never that what it's told is the right thing.
terraform plan refreshes the attributes of every resource already
listed in its state file, then shows the difference between that and
your configuration. It has no way to notice real infrastructure the
state file never mentions. An empty or stale state doesn't read as
"something's wrong." It reads as "nothing exists yet," and the plan
that follows looks exactly like any other clean, boring plan: a list
of resources to create.
That's the gap the agent walked into above. Nothing in the plan output said this already exists somewhere else. Nothing in the apply said you now have two of everything. The only step that looked dangerous was the last one, and by then the decision had already been made two commands earlier, by a state file nobody had checked was the right one.
Three guardrails that look like protection, and their gap
Terraform ships real safety mechanisms. Each one covers a specific failure and misses another one sitting right next to it:
| Mechanism | Protects against | Misses |
|---|---|---|
| State locking | Two concurrent applies corrupting one shared state | Two separate, stale state files that never had to fight over the lock |
prevent_destroy |
A plan that changes a resource in a destructive way | Doesn't prevent destroying a resource if you remove its configuration entirely |
| Manual approval | A human reading the plan before typing yes | The fiftieth prompt of the day, approved on reflex like the first |
None of these are broken. They each protect a narrower slice of "safe" than the name suggests, and an agent optimizing for "get this done" walks straight through the gap between them without noticing there was one.
What happens when two agents share one remote state
HashiCorp's own documentation
is clear about what locking actually buys you: it "happens
automatically on all operations that could write state" and "prevents
others from acquiring the lock." That's real protection against two
agents running apply against the same backend at the same moment.
It's also a narrower promise than it sounds. Locking serializes writes to one shared, remote state file. It does nothing for two agents each holding their own stale, unsynced copy, which is closer to what happened above than a locking failure was. Git worktrees solve the equivalent problem for a checkout: every agent gets its own folder and its own branch, nothing to collide over. Terraform state has no worktree. Two sessions pointed at the same infrastructure are pointed at the same ground truth, whether their local copies agree about it or not. It's the same shape of problem as a feature flag nobody remembered to retire: the risk was never the tool, it's state that outlives whoever last checked it.
Where this goes wrong, and what it really costs
A missing state file fails silently, not loudly. No warning, no error, just a plan that reads like a fresh install.
Recovery is luck unless you've tested it. DataTalks.Club came back because AWS support found a snapshot that wasn't even visible in the founder's own console, and restoration still took 24 hours. Not every resource type keeps automated snapshots. Not every provider's support team goes looking for one.
Auto-approve exists because typing "yes" got old. Flags that skip the confirmation prompt remove the one step that might have caught this, for the same reflexive-approval reasons this blog has covered before: the dialog for a safe command and a destructive one looks identical, so attention decays until neither gets read.
The postmortem always names a process, not a bug. The founder's own fix afterward wasn't a smarter agent. It was moving a human back in front of every destructive step: "Every plan is reviewed manually. Every destructive action is run by me."
Give the plan a review lane before anything applies
SanuDesk is a desktop app built around exactly that gap between a
plan that reads clean and infrastructure that's real. Your Claude
Code, Codex and Gemini sessions tile into one grid, so a terraform plan output is something you read beside the diff that produced it,
not something you reconstruct from a state file after the fact. Work
arrives through a Kanban board: "migrate the pricing service" is a
card, it deploys to a fresh session, and the plan output parks in a
review lane before anyone runs apply, one checkpoint between a stale
state file and production. Because SanuDesk is bring-your-own-model,
that review can run on a cheaper model than the one that wrote the
plan. A recurring drift check, running terraform plan against main
and flagging anything unexpected, becomes a Loop, journaled per run
instead of a habit everyone means to keep. The grid and board are in
the free plan (see pricing).
Read your own plan before the next apply
Open the last terraform plan an agent handed you and actually read
the resource count before the summary line. If it says more resources
to create than you expect, or the state file came from a checkout
that's more than a day old, stop there. That mismatch is cheaper to
question now than to explain in a postmortem later.
Download SanuDesk free to give every plan a review lane before it applies, or see how the grid, the board and Loops fit together on the features page.