skip to content

What does `terraform plan -detailed-exitcode` return, and how would you use it in a script?

level: middleimportance: nice to knowfreq 32%

answer

  1. default plan cannot say empty or not
  2. three codes, not two
  3. two means success with changes
  4. one is still a real failure
  5. set -e will kill the job

basics

~20 s

With -detailed-exitcode, terraform plan exits 0 when there are no changes, 1 on error, and 2 when the plan succeeded and changes are pending. Scripts use it to tell an empty diff apart from a real one without parsing output.

solid answer

~40 s

By default `terraform plan` exits `0` whether the diff is empty or a hundred resources long, and `1` only on error — so a script cannot tell "nothing to do" from "about to rebuild production" without parsing text. `-detailed-exitcode` splits that: `0` means success with no changes, `1` means the run failed, `2` means success with a non-empty diff. That gives you two useful behaviours: a scheduled job that runs a plan and alerts only on `2`, meaning the real estate no longer matches the code, and a pipeline that skips the apply stage entirely when there is nothing to do. The trap is `set -e` in a shell script, which treats `2` as failure and aborts the job — you have to capture the code deliberately.

code

bash · 10 lines
bash
set +e
terraform plan -input=false -detailed-exitcode -out=tfplan
code=$?
set -e

case "$code" in
  0) echo "in sync - skipping apply" ;;
  2) echo "changes pending - sending for review" ;;
  *) echo "plan failed with $code" >&2; exit "$code" ;;
esac

go deeper

for a junior

Remember the three values — 0 for no changes, 1 for an error, 2 for changes pending — and that plain terraform plan does not distinguish the first from the last.

for a middle

Explain why the split exists, that 2 is a success rather than a failure, and how a shell script must capture the code so set -e does not abort on a normal result.

for a senior

Describe wiring it into a scheduled comparison job: what alerts on 2, why 1 must page differently, and why the exit code is only a trigger for looking at the actual diff.

for a principal

Own what the signal means organisationally — how often an unexpected non-empty plan is tolerable, who is accountable for closing it, and whether that metric belongs in your change-management reporting.

## The default is deliberately blunt Run `terraform plan` and the process exits `0` on success and `1` on error. "Success" covers both an empty diff and a plan that destroys your database. For a human reading the terminal that is fine — the diff is right there. For anything automated it is useless, because the only way to distinguish the two outcomes is to scrape the output text, and output text is not an interface. ## Three codes instead of two ```bash terraform plan -detailed-exitcode ``` - `0` — succeeded, and the plan is empty; configuration and reality already agree. - `1` — the run errored: bad configuration, missing credentials, a provider failure, an uninitialised directory. - `2` — succeeded, and there are changes to apply. Code `2` is the interesting one, and note that it is a *success*. Nothing went wrong; Terraform is reporting a fact about the world. ## Two things it is good for **A scheduled comparison check.** Run a plan on a schedule against the committed configuration. Exit `0` means the estate still matches the code and nobody needs to know. Exit `2` means something diverged — a change merged but never applied, or someone altered infrastructure outside Terraform — and the job raises an alert with the diff attached. Exit `1` is a broken pipeline and pages differently, which is the second reason the three-way split matters: without it, a credentials failure looks identical to a clean run. **A conditional pipeline.** A plan stage that exits `0` can skip the apply stage — no approval request left sitting unanswered, no noise in the change log for a no-op. Exit `2` proceeds to review and apply. This works well combined with `-out`, since one run can both report the code and save the plan for the apply stage. ## The shell trap Most CI scripts run under `set -e`, which aborts on any non-zero exit. Under that setting a plan with changes kills the job, and the failure message will be unhelpfully generic. Capture the code explicitly instead: ```bash set +e terraform plan -input=false -detailed-exitcode -out=tfplan code=$? set -e case "$code" in 0) echo "no changes" ;; 2) echo "changes pending" ;; *) echo "plan failed"; exit "$code" ;; esac ``` The `||` idiom (`terraform plan -detailed-exitcode || code=$?`) works too, but the explicit `case` makes the intent readable to whoever debugs it at 3am. Either way, `1` must still fail the job — collapsing `1` and `2` into "non-zero, carry on" turns a broken pipeline into a silent one. ## What it does not tell you The exit code is a single bit of information: changes or no changes. It does not say whether those changes are additions or destructions, whether they are safe, or where they came from. For any judgement beyond "something differs", you need the plan itself — the human diff for a reviewer, or the JSON form for a policy tool. Treat `-detailed-exitcode` as the trigger, not the analysis. It also says nothing about *why* the diff is non-empty. An unapplied merged commit and an out-of-band console edit both produce `2`. Distinguishing them is a separate exercise; the exit code just tells the script that the exercise is now necessary. ## Interview framing This is a small, concrete detail, and it is usually asked as a proxy: does the candidate write infrastructure automation that reacts to results, or do they only run commands by hand? Being able to recite `0/1/2` and immediately name the `set -e` trap signals someone who has actually wired a plan into a scheduled job.

  • Why is exit code 2 considered a success rather than an error?
    Because the plan itself worked — Terraform read the configuration, compared it against state and reality, and produced a correct answer. "There are changes" is the answer, not a fault. Keeping it distinct from `1` is what lets a script tell a broken run apart from a run that simply found work to do, which need very different responses.
  • Can you use `-detailed-exitcode` and `-out` in the same plan run?
    Yes, and it is the common pattern. One run reports whether a diff exists and saves that plan for a later apply stage, so the pipeline neither re-plans nor guesses. The script branches on the exit code and, on `2`, hands the saved file to the approval and apply steps.

saying these in an interview costs you the question

  • Thinks exit code 2 means the plan failed
  • Assumes plain terraform plan already distinguishes empty diffs
  • Greps plan output text instead of checking the exit code
  • Treats any non-zero code as changes, hiding real errors
  • Forgets set -e aborts the job on exit 2

context