terraform-plan-review
Review a terraform plan before apply, from the plan text or terraform show -json output. Flags every destroy and replace with the attribute forcing it, changes to stateful resources such as databases, buckets and volumes, IAM and security-group widening, drift nobody made in code, and changes outside the intended scope, then gives a SAFE TO APPLY, APPLY WITH CARE or DO NOT APPLY verdict with reasons. Use when the user pastes a plan, says "review this terraform plan", "is this plan safe to apply", "why is terraform replacing my database", or "check the plan before I apply".
Other ways to install
With the sibling skills it hands off to:
The whole pack it belongs to:
By hand, from a clone of the repo:
In Claude Code, with the infra-as-code plugin installed, invoke it as:
Try it
A realistic prompt from the skill's acceptance evals:
Here's the plan for Acme's billing stack. The change is only supposed to add a tag to the API load balancer. Is it safe to apply?
Terraform will perform the following actions:
# aws_lb.api will be updated in-place
~ resource "aws_lb" "api" {
id = "arn:aws:elasticloadbalancing:eu-west-1:111111111111:loadbalancer/app/acme-billing-api/0123456789abcdef"
~ tags = {
+ "cost-centre" = "billing"
}
}
# aws_db_instance.billing must be replaced
-/+ resource "aws_db_instance" "billing" {
~ id = "db-EXAMPLE1234567890" -> (known after apply)
~ identifier = "acme-billing" -> "acme-billing-v2" # forces replacement
skip_final_snapshot = true
deletion_protection = false
}
# aws_security_group_rule.api_https will be created
+ resource "aws_security_group_rule" "api_https" {
+ type = "ingress"
+ from_port = 443
+ to_port = 443
+ cidr_blocks = ["0.0.0.0/0"]
}
Plan: 2 to add, 1 to change, 1 to destroy.What a good result looks like
Example output
Worked example: Northwind orders, "resize the app tier" · the skill's own worked example, source
Input
The user says: "This plan is meant to move the orders app from m6i.large to m6i.xlarge. Can I apply it?" and supplies plan.json from terraform show -json tfplan. The file is northwind-orders-plan.json, a hand-built plan in Terraform's documented JSON format (format_version 1.2). Placeholder IDs only.
The stated intent maps to two addresses, so the agent runs:
python3 scripts/plan_summary.py examples/northwind-orders-plan.json \
--scope aws_launch_template.app --scope aws_autoscaling_group.app
Script output (real, unedited)
# Terraform plan summary
Terraform 1.9.5, JSON format 1.2.
**Floor verdict: DO NOT APPLY** (mechanical; the reviewer reads every flagged change)
| create | update | replace | destroy | read | forget | no-op |
|---|---|---|---|---|---|---|
| 1 | 4 | 2 | 0 | 0 | 0 | 1 |
Scope: aws_launch_template.app, aws_autoscaling_group.app
## Changes to review
- [DO NOT APPLY] `aws_db_instance.orders` (replace (destroy then create))
- will be replaced; replacement forced by: storage_encrypted (action_reason: replace_because_cannot_update)
- stateful resource: data is lost unless a backup or snapshot exists
- outside the stated scope
- [DO NOT APPLY] `aws_iam_role_policy.app` (update)
- access widened: policy allows Resource "*"; policy allows wildcard action s3:*
- outside the stated scope
- changed attributes: policy
- [DO NOT APPLY] `aws_security_group_rule.app_ssh` (create)
- access widened: open CIDR 0.0.0.0/0
- outside the stated scope
- [CARE] `aws_instance.bastion` (replace (create then destroy))
- will be replaced; replacement forced by: ami (action_reason: replace_because_cannot_update)
- outside the stated scope
- [CARE] `module.monitoring.aws_cloudwatch_metric_alarm.cpu_high` (update)
- outside the stated scope
- changed attributes: threshold
## Drift (changed outside Terraform)
- `aws_security_group.db` (update): ingress
The two in-scope updates (aws_launch_template.app, aws_autoscaling_group.app) are counted but not flagged, because they are in scope and touch nothing risky.
Review (what the agent hands back)
Verdict: DO NOT APPLY. The plan resizes the app tier as intended, but it would also replace the orders database with an empty one, open SSH to the internet, and give the app role full S3 access on every bucket. None of that is part of "resize the app tier".
Counts: 1 create, 4 update, 2 replace, 0 destroy, 1 no-op.
Flagged changes
- 🟥
aws_db_instance.orders: destroy then create, forced bystorage_encrypted(falsetotrue,action_reason: replace_because_cannot_update). The plan also showsskip_final_snapshot = trueanddeletion_protection = false, so applying deletes the database without a final snapshot and creates a new, empty one. RDS cannot turn on encryption for an existing instance in place; the usual route is snapshot, copy the snapshot with encryption, restore from it, and cut over. That is a migration, not a resize. - 🟥
aws_security_group_rule.app_ssh: new ingress rule, port 22 from0.0.0.0/0on the app security group. Before: no such rule. After:cidr_blocks = ["0.0.0.0/0"],from_port = 22,to_port = 22. - 🟥
aws_iam_role_policy.app: policy widened. Before:Allow s3:GetObject, s3:PutObject on arn:aws:s3:::northwind-orders-exports/*. After:Allow s3:* on *. The app role can now read, write and delete every bucket in the account. - 🟧
aws_instance.bastion: create then destroy, forced byami. A new bastion gets a new instance ID and new IPs (private_ip,public_ipare known after apply). Anything allow-listing the old bastion IP breaks. Out of scope. - 🟧
module.monitoring.aws_cloudwatch_metric_alarm.cpu_high:threshold80 to 95. Out of scope, and it loosens an alert at the same moment the instance size changes.
Drift
aws_security_group.db: someone added an ingress rule outside Terraform, port 5432 from10.20.0.0/16, description "temp debug". This plan does not listaws_security_group.dbinresource_changes, so it neither reverts nor adopts the rule. The rule stays open and untracked until someone decides.
Out of scope
All five flagged changes fall outside aws_launch_template.app and aws_autoscaling_group.app. The pattern (an encryption change, an SSH rule, a wider policy, a new AMI, a looser alarm) looks like a branch that picked up unrelated commits, or a shared root module with several people's work in it.
Before you apply
- Split the change. Re-plan with only the launch template and autoscaling group changes; that plan should show 2 updates and nothing else.
- Take the
storage_encryptedchange out of this branch and plan it as a migration (snapshot, encrypted copy, restore, cutover). Setdeletion_protection = trueandskip_final_snapshot = falseonaws_db_instance.ordersfirst, and addlifecycle { prevent_destroy = true }. - Remove the
0.0.0.0/0SSH rule. If bastion access is needed, use a session manager or a specific source CIDR. - Revert the IAM policy to the named actions on
northwind-orders-exports/*. - Ask who changed the alarm threshold and the bastion AMI, and why, before either goes in.
- Decide about the "temp debug" rule on
aws_security_group.db: remove it in the console, or add it to code if it is needed.
Hand-offs: the database encryption change goes to migration-plan; the resize itself is small enough not to need a runbook.
How it's checked
Measured against plain Claude Code (2026-10-23): it met 74% of its checks, against 71% without it, and a blind judge preferred it. How skills are checked
26 assertions across 4 acceptance evals, and 8 trigger queries. Every skill's checks
What the output must do
When it should and shouldn't activate
Should activate:
- Review this terraform plan before I apply it
- Why does terraform want to replace my RDS instance? Plan output attached
- Here's terraform show -json for tomorrow's change, is it safe to apply?
- Check this plan for destroys, IAM widening and drift before we run apply in prod
Should leave to another skill:
- Review the Terraform changes in this pull request diff before I merge
- Plan the migration of our Postgres database to an encrypted instance with cutover and rollback
- Write the on-call runbook for applying infrastructure changes and rolling them back
- Estimate the monthly cost of moving the app tier to m6i.xlarge
Hands off to
- migration-plan — when a stateful resource must be replaced and its data moved. Plan the snapshot, copy, restore and cutover instead of letting apply destroy the data.
- runbook-generator — when the plan is approved and the apply needs a step-by-step procedure. Write the apply, verify and rollback steps the operator follows.
Related skills
capacity-cost-model, pre-merge-review, terraform-module
Source
SKILL.md · the whole folder on GitHub.