With the detailed exit code option, terraform plan returns 0 when there are no changes, 1 when it fails, and 2 when it succeeds and the plan contains changes. That lets a CI job tell a clean run from a pending change without parsing text output, and fail or flag the build on exit code 2.
What the three codes mean
Without the option, terraform plan exits 0 whenever it succeeds, whether or not it found changes. With -detailed-exitcode, the meaning becomes more granular. The HashiCorp reference documents it like this:
| Exit code | Meaning |
|---|---|
| 0 | Succeeded, empty diff, no changes |
| 1 | Error |
| 2 | Succeeded, non-empty diff, changes present |
The trap is that most CI systems treat any non-zero code as a failure. Code 2 is not an error, so a naive step will mark a perfectly healthy plan as broken.
A CI pattern that handles all three
Capture the code instead of letting the shell exit on it:
set +e
terraform plan -out=tfplan -detailed-exitcode
code=$?
set -e
case "$code" in
0) echo "No changes" ;;
2) echo "Changes present, run policy checks and wait for approval" ;;
*) echo "Plan failed"; exit 1 ;;
esac
Exit code 2 is the signal to run the next stage: a policy check on the plan, a human review, or both. Remember that saved plan files can contain sensitive values, so store the artifact with care.
Using code 2 to catch drift
Run the same plan on a schedule against an unchanged main branch. If it returns 2, something differs between your configuration and what exists. The cause might be a manual console change, a provider behaviour change, or a resource that expired. Either way it deserves a look, and the exit code gives you a cheap alarm without a separate drift product.
Open an issue automatically on exit code 2 from the scheduled job, and attach the plan summary rather than the full plan if it may hold secrets.
Where it fits in a gate
A reasonable pull request flow has four steps:
- Format and validate the code.
- Run a plan with the detailed exit code.
- If the code is 2, convert the plan to JSON and run policy checks against it.
- Require approval before apply, and apply only the saved plan.
The first step is quick and cheap. The expensive and risky steps only run when there is something to change.
Try it yourself
Click through it at your own pace, or open it full screen.
Open the demo and look at where a failing check stops the merge. An exit code 2 from a plan should land you at the same kind of checkpoint.
Quick check
Does exit code 2 mean the plan failed?
No. It means the plan succeeded and found changes. Exit code 1 is the error case.
Is the detailed exit code on by default?
No. You have to pass the option. Without it, a successful plan exits 0 whether or not it has changes.
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.