Save the plan with terraform plan and an output file, convert it to JSON with terraform show and the json option, then evaluate Rego rules against that JSON using the Open Policy Agent command line. A rule that returns messages for forbidden changes can fail the pipeline before anything is applied.
Why check the plan, not just the code
A plan has resolved variables, module outputs and the actual actions Terraform intends: create, update or delete. Rules on raw code cannot see those. Rules on the plan can, for example "never delete a database" or "no changes to IAM without review". The Open Policy Agent documentation walks through this same flow.
The steps
- Create the plan. Save it to a file so the exact plan you checked is the plan you apply.
terraform init
terraform plan -out=tfplan.binary
- Convert it to JSON. OPA reads JSON, not the binary plan.
terraform show -json tfplan.binary > tfplan.json
- Write a rule. The JSON has a list called resource_changes. Each entry has a type and a change object with an actions list. This is a small example rule to adapt, not a complete policy:
package terraform.plan
import rego.v1
deny contains msg if {
some rc in input.resource_changes
rc.type == "aws_db_instance"
"delete" in rc.change.actions
msg := sprintf("Deleting %s needs a manual review", [rc.address])
}
- Evaluate it.
opa eval --format pretty --data policy.rego --input tfplan.json "data.terraform.plan.deny"
- Fail the build if the result is not empty. Wrap the command in a script that exits non-zero when it returns any message.
What the OPA tutorial's own policy does
The official walkthrough uses a different idea: it computes a blast radius score from weighted counts of creates, deletes and modifications by resource type, and authorises the plan only if the score is under a threshold and no IAM resources change. It notes that in practice you would vary the threshold by user. That pattern suits teams that want automatic approval for small changes and human review for large ones.
Things to watch
- Treat the JSON as sensitive. It can contain secret values. See saved plan files contain secrets.
- Test your rules. A rule that never fires is worse than none. Write unit tests with opa test.
- Apply the saved plan. If you re-plan before applying, you are applying something nobody checked.
Try it yourself
Click through it at your own pace, or open it full screen.
Look at how a failing check blocks the merge. A non-empty deny result from your plan check should do the same job.
Quick check
Why convert the plan to JSON?
OPA evaluates structured data such as JSON. The binary plan file is not something it can read directly.
Is checking code enough without checking the plan?
Code checks are fast and early, but they cannot see resolved values or the actual actions. Plan checks catch what only appears once values are known.
Safeguard runs policies and gates on pull requests, so a failed rule can hold a merge, and its dependency and SBOM scanning covers the packages your infrastructure code pulls in. You can try a free scan at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.