RSS

What is terraform fmt? Command, Flags, Recursive, and CI

Terraform CI/CD Infrastructure

What you'll learn: terraform fmt rewrites Terraform configuration files into HashiCorp's style, but doesn't validate provider schemas, check internal consistency, or change how infrastructure behaves. Important flags in real repositories are -check, -diff, -write=false, -list=false, and -recursive. They turn local formatting into an enforceable workflow. terraform fmt -check -recursive -diff is the right CI/CD gate because it fails with a non-zero exit code when files are not formatted, does not rewrite them, and can show the exact diff. -recursive matters the moment your repo has modules/ or environment directories, as terraform fmt only processes the current or explicitly targeted directory by default.

terraform fmt is not a personal preference. It's the team rule that keeps Terraform code formatted the same way before anyone reviews, plans, or applies it, which means cleaner diffs and fewer pointless review comments.

How to keep Terraform code reviewable with terraform fmt

Terraform configurations age badly when every engineer formats them differently. The code may still apply, but reviews get noisy, diffs stop being trustworthy, and tiny edits become larger than they should be because nobody agrees on where arguments, nested blocks, and spacing should land.

HashiCorp's own style guide exists for that reason, and terraform fmt is the built-in mechanism that applies a subset of those conventions automatically.

That is the main job of terraform fmt: It solves the formatting layer, not the correctness layer.

This guide covers the terraform fmt command, the flags that change how it behaves, a concrete terraform fmt example, why terraform fmt recursive is non-negotiable in real repos, and how to wire the command into CI so formatting stops being a social problem and becomes a mechanical one.

What is terraform fmt?

The terraform fmt command is the built-in Terraform formatter. You use it to automatically format Terraform code, so teams stop making ad hoc formatting decisions file by file.

The command rewrites Terraform configuration files so they match HashiCorp's canonical HCL style. In practice, that means consistent indentation, aligned equals signs for consecutive single-line arguments, and predictable block layout.

What does terraform fmt do?

terraform fmt rewrites Terraform configuration files into HashiCorp's canonical format and style, applying a subset of the Terraform language style conventions and minor readability adjustments.

What it doesn't do is validate the configuration against provider schemas, check whether the module is internally consistent, or change what terraform plan and terraform apply will do. It's formatting, not validation.

That opinionated behavior is intentional. The goal of terraform fmt is consistency across Terraform codebases, not endless debate over local formatting preferences, so it is intentionally non-configurable. Teams that want consistent formatting shouldn't treat fmt as a nice-to-have editor feature and start treating it as the canonical format contract for the repository.

The basic syntax is usually written as terraform fmt [options] [TARGET]. The current CLI docs express that as terraform fmt [options] [target...], so the command can scan a directory, process a specific file, or read from standard input when you pass -. If you omit the target, Terraform scans the current directory by default.

terraform fmt flags are what turn it into a workflow

The base command is tiny, while the flags decide whether you are modifying files, previewing changes, or enforcing policy in CI. HashiCorp's docs also list -no-color, but the five flags that matter day to day are the ones below.

  1. -list=false suppresses the list of files with formatting inconsistencies, making it useful when you want quieter output in automation or editor integrations.
  2. -write=false prevents modifying files, making it the right choice when you want to preview what fmt would touch without actually rewriting the file formatting on disk.
  3. -diff displays the formatting changes as a diff, making it especially useful in CI because the failure becomes actionable instead of mysterious. On Windows, that flag depends on an external diff binary, and installing GNU Diffutils, often through Chocolatey, solves the common diff not found error.
  4. -check verifies whether files are formatted correctly, exits with status 0 when they are, and exits non-zero when they are not. It doesn't rewrite files, meaning it's the flag you want in CI/CD.
  5. -recursive tells Terraform to process files in subdirectories as well as the current directory. Without it, fmt only processes the directory you explicitly point it at.

There is one more operational detail that most teams learn the hard way: The canonical output of terraform fmt can change in minor ways between Terraform versions, and those changes aren't treated as breaking. That means the same files can produce fresh formatting diffs after a version upgrade, and that's expected behavior, not a regression.

Once the flags are clear, the fastest way to see what they really do is to look at a concrete file before and after formatting.

terraform fmt examples make the command obvious

Here is a minimal terraform fmt example using a deliberately messy aws_instance resource.

resource "aws_instance" "web" {
ami="ami-123456"
instance_type="t3.micro"
tags={
Name="web"
Environment="dev"
}
root_block_device{
volume_size=20
encrypted=true
}
}

After you run terraform fmt, the same resource becomes this.

resource "aws_instance" "web" {
  ami           = "ami-123456"
  instance_type = "t3.micro"

  tags = {
    Environment = "dev"
    Name        = "web"
  }

  root_block_device {
    encrypted   = true
    volume_size = 20
  }
}

The changes are not cosmetic. Nested blocks now have consistent indentation, consecutive arguments have aligned = signs, and the block layout matches the style HashiCorp documents for readable Terraform code.

When you run terraform fmt without extra flags, Terraform prints the names of the files it modified. If you use -list=false, it stops listing them. If you use -write=false, it still tells you which files would change but skips modifying them.

If you add -diff, the output becomes even more useful when you are debugging a CI failure.

-ami="ami-123456"
-instance_type="t3.micro"
+ami           = "ami-123456"
+instance_type = "t3.micro"

That preview is why -diff pairs so well with -check. It shows the exact formatting changes instead of forcing the developer to guess what the formatter disliked.

terraform fmt -recursive is where most teams trip

By default, terraform fmt only touches the given directory, or the current directory if you do not pass one. That lines up with how Terraform treats modules in the filesystem, as nested directories are separate modules and are not automatically included just because they live under a parent directory.

In a real repository, that default is usually too narrow.

.
├── main.tf
├── variables.tf
├── modules/
│   ├── network/
│   │   └── main.tf
│   └── compute/
│       └── main.tf
└── environments/
    ├── dev/
    │   └── main.tf
    └── prod/
        └── main.tf

Run terraform fmt from the repo root, and you only format main.tf and variables.tf. Run terraform fmt -recursive, and Terraform will process the files under modules/ and environments/ too. The reality is that fmt only modifies Terraform code in the directory where you execute it unless you include -recursive.

A local pass can look clean while CI still fails. The developer formatted only the root, while CI checked nested directories too. Nothing magical happened. The repository simply was not fully scanned.

When that pattern shows up more than once, the only sensible move is to enforce the right command in CI.

CI is where terraform fmt becomes a contract

The command you want in CI is terraform fmt -check -recursive -diff.

-check makes the step fail with a non-zero exit status when files are not formatted, -recursive makes sure you actually scan the whole repository tree that matters, and -diff makes the failure readable. As -check implies read-only behavior, this step enforces formatting without modifying files in the pipeline.

A practical GitHub Actions workflow looks like this.

name: terraform-fmt

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  fmt:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.5
          terraform_wrapper: false

      - name: Check formatting
        id: fmt
        continue-on-error: true
        run: terraform fmt -check -recursive -diff

      - name: Comment with fix instructions
        if: github.event_name == 'pull_request' && steps.fmt.outcome == 'failure'
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: [
                '`terraform fmt -check -recursive -diff` failed.',
                '',
                'Run `terraform fmt -recursive` locally, commit the rewritten files, and push again.'
              ].join('\n')
            })

      - name: Fail the job
        if: steps.fmt.outcome == 'failure'
        run: exit 1

That pattern includes the use of continue-on-error and follow-up PR comments when a Terraform step fails.

Version alignment matters just as much as the command itself. HashiCorp explicitly warns that terraform fmt output can change between Terraform versions, so pin the same Terraform version locally and in CI.

A .terraform-version file works well for teams using tfenv, because tfenv will detect that file and select or install the specified version.

Pre-commit hooks are the right complement, not the replacement. The antonbabenko/pre-commit-terraform project ships a terraform_fmt hook, which lets developers auto-fix formatting before commit, while CI remains the final gate that catches anything that slips through.

Formatting belongs at the front of the pipeline

Formatting should be completed first because it is the cheapest check you can run. terraform fmt only scans and rewrites configuration files, while terraform validate requires an initialized working directory with providers and modules installed. Put differently, fmt stabilizes the input before heavier checks do more expensive work.

A sane pipeline order is:

fmt check → init → validate → lint → plan

Once that order is in place, the remaining problems are not philosophical. They are boring, specific, and usually easy to diagnose.

Most terraform fmt failures are mundane and diagnosable

There are four common failure modes:

  1. Syntax

When terraform fmt starts returning parse errors, such as invalid characters and invalid expressions, then the HCL is malformed, and the command cannot format code that it cannot parse. Fix the syntax problem first, then run terraform validate to catch the deeper semantic issues that formatting does not cover.

  1. Incomplete scope

If you run terraform fmt locally, but don't use -recursive while CI runs terraform fmt -check -recursive -diff, then the files are not newly broken, they were simply never checked from your current directory.

  1. Version mismatch

One engineer is on one Terraform version, CI is on another, and the repository suddenly shows formatting diffs after an upgrade. New formatting rules can land in new versions, so the fix is version pinning.

  1. Operator error

Terraform only scans the directory you point it at, and nested directories are separate modules, so running fmt from the wrong place or from a parent directory with no relevant target means the tf files you care about will never be processed.

There is also one filename detail worth updating in older team folklore. Current Terraform behavior includes .tfvars among the files terraform fmt can process, alongside .tf and .tftest.hcl, so automation that assumes .tf only is stale.

terraform validate vs. fmt: They solve different problems

So, what's the difference between terraform fmt and terraform validate?

terraform fmt handles layout. It rewrites indentation, spacing, alignment, and block structure into canonical format.

terraform validate checks whether a configuration is syntactically valid and internally consistent, and that validation requires an initialized working directory with referenced plugins and modules installed.

One fixes how the code looks. The other checks whether the configuration makes sense. You want both, and neither replaces the other.

Command Job What it doesn't do Typical place in pipeline
terraform fmt Formatting Does not check whether the configuration is correct First
terraform validate Validation Does not rewrite or standardize file formatting After terraform init

The next problem is not formatting, it's state

terraform fmt should be boring. That's the goal. It should run early, run automatically, and stop wasting review time on formatting inconsistencies. In real repositories, that means three things:

  1. Treat terraform fmt as the formatting contract
  2. Use -recursive for anything larger than a toy module
  3. Enforce -check in CI so the contract is real instead of aspirational.

Once formatting is solved, teams run into the next bottleneck, which is not whitespace but state management at scale, lock contention, slow plans, fragmented visibility, and partial failures across too many separate state files.

Stategraph is built for that layer, replacing the flat state file with a database-backed graph. Get started with Stategraph today.

terraform fmt FAQs

How do I run terraform fmt recursively?

Run terraform fmt -recursive from the repository root, or from the specific directory you want to scan deeply. Without -recursive, Terraform only processes the current or explicitly targeted directory.

How do I use terraform fmt in a CI pipeline?

Use terraform fmt -check -recursive -diff as an early pipeline step. It leaves files unchanged, returns a non-zero exit status when formatting issues are found, and shows the diffs that make pull request fixes obvious.

Does terraform fmt change how my infrastructure behaves?

No, terraform fmt changes file formatting, not resource behavior, provider interactions, or the semantics of plan and apply.