In a CI job, what does `terraform fmt -check` do differently from plain `terraform fmt`, and what does it miss by default?
answer
- the read-only variant of a formatter
- the exit code is the whole point
- scope defaults to a single directory
- -recursive walks the module subtree
- whitespace only, never semantics
basics
~20 sterraform fmt -check reports formatting problems without rewriting anything and exits non-zero if any file would change, which is what makes it usable as a build gate. By default it looks only at the current directory, so nested modules need -recursive.
solid answer
~50 sPlain `terraform fmt` rewrites files in place and exits zero, so running it in CI silently fixes the throwaway checkout and reports success — the repository stays unformatted. `-check` turns the command read-only and turns the answer into an exit status: zero when everything is already canonical, non-zero when at least one file would change, which is exactly what a pipeline step needs. Add `-diff` if you want the job log to show what it would have rewritten. The trap is scope: `terraform fmt` processes only the directory you point it at, not the tree beneath it, so a root-level check passes while `modules/` drifts. Use `-recursive`. Formatting is whitespace, indentation and `=` alignment only — it never reorders arguments or changes what the configuration means, so it is safe to run on the whole repo.
go deeper
Know that plain terraform fmt rewrites files while -check only reports and fails the build, and remember to add -recursive so nested modules are covered.
Be able to explain why running the fixer in CI produces a permanently green, permanently unformatted repo, and where fmt sits below validate and lint in the chain of checks.
Show the judgment that formatting belongs on save or in a pre-commit hook with CI as a backstop, and that a style gate must never be confused with a correctness or security gate.
Own the tradeoff between enforcement and friction: a formatting failure that blocks an urgent apply costs more than it saves, so decide deliberately whether style checks gate the deploy or only the review.
## What the command actually does `terraform fmt` rewrites Terraform configuration files into HashiCorp's canonical style: indentation, spacing, and vertical alignment of the `=` signs inside a block. It operates on `.tf` and `.tfvars` files. What it does **not** do is as important — it never reorders arguments, never renames anything, never adds or removes blocks, and never touches meaning. Formatting a repository cannot change a single byte of the resulting plan. That is why it is safe to run over everything and why it is a pure hygiene tool rather than a correctness tool. It does need the input to be parseable HCL. A file with a genuine syntax error is a parse failure, not a formatting suggestion, so `fmt` reports an error rather than reformatting it. ## Why `-check` exists A pipeline does not want a fixer, it wants a verdict. Two flags change the mode: - `-check` — do not write anything; exit zero if every file is already formatted, non-zero otherwise. - `-diff` — print the changes that would be made, so the failing job explains itself. The classic broken pipeline runs plain `terraform fmt`. It happily rewrites the files inside the ephemeral CI workspace, exits zero, and the workspace is then thrown away. Every build is green and the repository never gets formatted. `-check` is the difference between a gate and a no-op. ```bash terraform fmt -check -recursive -diff ``` Developers run `terraform fmt -recursive` locally to fix; CI runs the same command with `-check` to enforce. Same tool, two modes. ## The directory-scope trap By default `terraform fmt` processes the files in one directory — the current one, or a path you pass as an argument — and stops there. It does not walk subdirectories. A repository laid out as a root module plus `modules/network`, `modules/rds` and so on will pass a root-level `terraform fmt -check` while every module underneath it is unformatted, which is exactly the code reviewers spend their time complaining about. `-recursive` fixes it, and it is the single most common omission in a Terraform CI job. ## What it deliberately cannot tell you `fmt` is the shallowest check you can run. It says nothing about whether a variable is declared, whether an attribute exists on that resource type, whether an argument is required, whether the value is legal for the target cloud, or whether the resource is a security problem. Those belong to progressively deeper tools: - `terraform fmt -check` — style, no init required, milliseconds. - `terraform validate` — internal consistency against provider schemas, requires an initialized directory. - `tflint` — provider-specific value rules and best-practice lint. - `checkov` / `trivy` — security and compliance rules over HCL or plan JSON. - `terraform plan` — what would actually change, requires credentials. Each one can only see failures the previous layer let through, and only the last one talks to a cloud API. ## Practical notes `fmt` reads from stdin when you pass `-` as the target, which is how editor integrations format a buffer without touching disk. `-list=false` suppresses the filename listing when you only want the exit status. And because formatting is semantically inert, the sane team policy is to make it automatic — an editor-on-save action or a repository hook — and keep the CI check purely as a backstop for people who bypassed it, rather than as the primary mechanism for getting formatted code. One last subtlety worth saying out loud in an interview: a green `fmt -check` proves nothing about the configuration's correctness. It is worth having because inconsistent whitespace makes diffs noisy and code review harder, not because it protects production.
- Your CI job runs `terraform fmt -check` at the repository root and passes, yet reviewers keep seeing misaligned HCL under modules/. What is happening?The check is only covering the root directory. `terraform fmt` does not descend into subdirectories unless you pass `-recursive`, so every nested module is invisible to the gate. Adding `-recursive` to both the local fix command and the CI check closes the gap, and the first run after that will produce a large but semantically empty diff.
- A colleague argues the pipeline should run `terraform fmt` and commit the result instead of failing the build. What do you say?It can work, but it means the pipeline pushes to the branch, which needs write credentials and can fight concurrent pushes and re-trigger itself. The cheaper version of the same idea is to format on save or in a pre-commit hook so the code arrives formatted, and keep `-check` in CI purely as a backstop.
- What does `terraform fmt` do if a file contains a genuine HCL syntax error?It reports a parse error rather than reformatting, because it has to build a syntax tree before it can print one back out. That is incidental, though — it is not a validator, and you should not rely on it to catch broken configuration. `terraform validate` is the command that actually checks references, argument names and types.
saying these in an interview costs you the question
- Thinks fmt -check rewrites the files it inspects
- Claims terraform fmt validates the configuration or catches errors
- Assumes fmt walks the whole repository by default
- Believes reformatting can change what a resource does
- Runs plain terraform fmt in CI and calls it a gate