Safeguard
Developer Tools•

What does terraform plan detailed-exitcode return in CI?

With the detailed exit code option, terraform plan returns 0 for no changes, 1 for an error and 2 for changes. Here is how to use that in a CI gate.

Safeguard Security Team
3 min read

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 codeMeaning
0Succeeded, empty diff, no changes
1Error
2Succeeded, 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:

  1. Format and validate the code.
  2. Run a plan with the detailed exit code.
  3. If the code is 2, convert the plan to JSON and run policy checks against it.
  4. 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.