terraform fmt Command with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
Applies to Any host with Terraform installed
Lab environment Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu
Privilege Normal user
Scope terraform fmt workflow — canonical formatting, -check, -diff, -recursive, path targets, supported file types, CI enforcement, and how fmt differs from terraform validate. Does not cover HCL language reference, TFLint, Checkov, or full CI/CD pipeline setup.
Related guides Terraform lab environment on Ubuntu
Terraform init command
Terraform HCL syntax
Terraform providers
Terraform Associate certification course

terraform fmt is the fastest quality gate in the Terraform CLI. It rewrites configuration files to canonical spacing and alignment so teams stop debating indentation in code review. This guide starts with deliberately ugly but valid HCL, walks through check and diff modes, formats nested modules recursively, and shows why formatting is not the same as validation.

NOTE
Use the Terraform lab environment on Ubuntu and confirm terraform is installed before you start. Run every command in an isolated directory under ~/terraform-labs/terraform-fmt/ so you do not collide with other lessons on the same VM.

What does terraform fmt do?

terraform fmt scans Terraform configuration files and rewrites them to HashiCorp canonical style:

  • Indentation — two spaces per nesting level inside blocks
  • Alignment — argument = signs line up within a block when fmt can do so safely
  • Spacing — consistent gaps around operators and between arguments

fmt changes how configuration looks, not what it means. Resource names, provider settings, and argument values stay the same. If the configuration was already valid, terraform plan should not show new changes only because you ran fmt.

fmt is also safe to rerun. A second pass on already formatted files exits quietly and changes nothing.


Format Terraform configuration

This section covers default write behavior, read-only checks, and diff preview — the three modes you use on a single directory before you worry about recursion or path targets.

Format the current directory

Create a lab directory and add a misformatted root module file.

bash
mkdir -p ~/terraform-labs/terraform-fmt

Move into that directory — every command below assumes you are here:

bash
cd ~/terraform-labs/terraform-fmt

Write main.tf with cramped spacing and missing indentation:

hcl
resource "local_file" "demo" {
content="fmt lab"
filename="${path.module}/demo.txt"
}

Before you format, inspect the file so you can compare afterward:

bash
cat main.tf

Sample output:

output
resource "local_file" "demo" {
content="fmt lab"
filename="${path.module}/demo.txt"
}

Run fmt in the current directory. By default it writes changes back to disk and prints the name of each file it updated:

bash
terraform fmt

Sample output:

output
main.tf

The single filename on stdout is fmt's change list. Read the file again to see canonical spacing:

bash
cat main.tf

Sample output:

output
resource "local_file" "demo" {
  content  = "fmt lab"
  filename = "${path.module}/demo.txt"
}

Arguments are indented and the = signs align. That is the before/after you want in version control.

Check formatting without changing files

CI jobs and pre-commit hooks usually need a read-only check. Pass -check so fmt reports drift without rewriting files:

bash
cat > main.tf << 'EOF'
resource "local_file" "demo" {
content="fmt lab"
filename="${path.module}/demo.txt"
}
EOF

Recreate the ugly file above, then ask fmt whether anything is out of spec:

bash
terraform fmt -check

Sample output:

output
main.tf

fmt lists files that are not formatted. Capture the shell exit status — automation relies on it:

bash
echo $?

Sample output:

output
3

Exit status 0 means every scanned file already matches canonical style. A non-zero status means at least one file would change; on Terraform 1.15.x unformatted native-syntax files typically return 3. Scripts in CI treat any non-zero status as failure.

Format the file, then rerun the check:

bash
terraform fmt

Confirm the directory is clean:

bash
terraform fmt -check

No output means no unformatted files were found. Capture the successful exit status separately:

bash
echo $?

Sample output:

output
0

HashiCorp defines exit status 0 as success. fmt prints nothing on stdout when every scanned file is already formatted.

Preview changes with -diff

When you want to see what fmt would change, use -diff. Without -check, fmt still writes the rewrite to disk:

bash
cat > main.tf << 'EOF'
resource "local_file" "demo" {
content="fmt lab"
filename="${path.module}/demo.txt"
}
EOF

Run fmt with a unified diff:

bash
terraform fmt -diff

Sample output:

output
main.tf
--- old/main.tf
+++ new/main.tf
@@ -1,4 +1,4 @@
 resource "local_file" "demo" {
-content="fmt lab"
-filename="${path.module}/demo.txt"
+  content  = "fmt lab"
+  filename = "${path.module}/demo.txt"
 }

The - lines are removed; the + lines are what fmt writes. Combine -check and -diff when a pipeline should fail on drift but still print the patch for logs:

bash
cat > main.tf << 'EOF'
resource "local_file" "demo" {
content="fmt lab"
filename="${path.module}/demo.txt"
}
EOF

Run the combined flags:

bash
terraform fmt -check -diff

Sample output:

output
main.tf
--- old/main.tf
+++ new/main.tf
@@ -1,4 +1,4 @@
 resource "local_file" "demo" {
-content="fmt lab"
-filename="${path.module}/demo.txt"
+  content  = "fmt lab"
+  filename = "${path.module}/demo.txt"
 }

Because -check is set, fmt does not rewrite the file. Confirm the non-zero exit status:

bash
echo $?

Sample output:

output
3

Choose which files terraform fmt processes

By default, fmt scans only the current directory. You can widen the scan with -recursive, narrow it with an explicit path, or rely on the built-in file-type rules documented below.

Format recursively

Create a nested module with misformatted configuration while the root main.tf from the previous section is already formatted:

bash
mkdir -p modules/example

Write an unformatted nested module file:

bash
printf '%s\n' 'resource "local_file" "nested" {' 'content="nested module"' 'filename="${path.module}/nested.txt"' '}' > modules/example/main.tf

Run fmt without -recursive. Only files in the current directory are scanned, so the nested copy is skipped:

bash
terraform fmt

With a formatted root and an ugly nested file, fmt prints nothing and leaves the module unchanged. Verify the module file is still cramped:

bash
cat modules/example/main.tf

Sample output:

output
resource "local_file" "nested" {
content="nested module"
filename="${path.module}/nested.txt"
}

Add subdirectories to the scan with -recursive:

bash
terraform fmt -recursive

Sample output:

output
modules/example/main.tf

Only the nested file appears because the previous non-recursive terraform fmt already formatted the root file. Terraform lists files it actually changes; -recursive merely adds subdirectories to the scan. Confirm the nested file was rewritten:

bash
cat modules/example/main.tf

Sample output:

output
resource "local_file" "nested" {
  content  = "nested module"
  filename = "${path.module}/nested.txt"
}

Format a specific file or directory

You can pass a file or directory path instead of relying on the current working directory. Create a standalone file to target:

bash
printf '%s\n' 'resource "local_file" "one" {' 'content="x"' 'filename="${path.module}/one.txt"' '}' > one.tf

Format just that file:

bash
terraform fmt one.tf

Sample output:

output
one.tf

Inspect the result:

bash
cat one.tf

Sample output:

output
resource "local_file" "one" {
  content  = "x"
  filename = "${path.module}/one.txt"
}

Point fmt at a directory to process every supported file in that folder (non-recursive unless you also pass -recursive):

bash
printf '%s\n' 'resource "local_file" "nested" {' 'content="bad"' 'filename="${path.module}/nested.txt"' '}' > modules/example/main.tf

Run fmt against the module path:

bash
terraform fmt modules/example

Sample output:

output
modules/example/main.tf

fmt accepts - as a target to read from standard input, but stdin mode always disables -write and -check. For everyday module work, path arguments on disk are simpler.

Supported file types

Terraform 1.15.x documents these rules in terraform fmt -help:

File pattern Modified by fmt?
.tf Yes — native HCL syntax
.tfvars Yes — variable definition files
.tftest.hcl Yes — Terraform test files
.tf.json, .tfvars.json, .tftest.json No — JSON syntax is not rewritten
Other extensions No — fmt errors if you pass an unsupported path

Add sample variable and test files to see non-.tf formatting:

bash
printf 'demo_label="unformatted"\n' > variables.tfvars

Add a minimal Terraform test file with the same cramped style:

bash
printf 'run "demo" {\ncommand = plan\n}\n' > demo.tftest.hcl

Run fmt on the directory:

bash
terraform fmt

Sample output:

output
demo.tftest.hcl
variables.tfvars

Read the updated files:

bash
cat variables.tfvars

Sample output:

output
demo_label = "unformatted"

The .tfvars file now has spaces around =. Inspect the test file next:

bash
cat demo.tftest.hcl

Sample output:

output
run "demo" {
  command = plan
}

fmt adds spaces around = in .tfvars and indents blocks inside .tftest.hcl the same way it does for .tf files.

Passing a .tf.json file fails with an explicit error — JSON targets are not supported:

bash
printf '{"resource":{"local_file":{"demo":{"content":"json","filename":"demo.json"}}}}\n' > main.tf.json

Point fmt at the JSON file to see the rejection:

bash
terraform fmt main.tf.json

Sample output:

output
╷
│ Error: Only .tf, .tfvars, and .tftest.hcl files can be processed with terraform fmt
│
│
╵

The JSON file on disk is unchanged.


Use terraform fmt in CI

Most teams run one non-destructive check before plan or apply stages:

bash
terraform fmt -check -recursive

The command walks the repository tree, prints any unformatted relative paths, and exits non-zero when drift exists. A typical failure looks like this when a nested module was edited without fmt:

bash
terraform fmt -check -recursive

Sample output:

output
modules/example/main.tf

Capture the status your pipeline will see:

bash
echo $?

Sample output:

output
3

Fix locally with terraform fmt -recursive, commit the formatting diff, and rerun the check until the exit status is 0. Full GitHub Actions or GitLab pipeline YAML belongs in a dedicated CI/CD lesson — here the important part is the command and its exit code contract.


terraform fmt vs terraform validate

fmt is a formatter. terraform validate checks syntax and internal configuration consistency against installed provider schemas. It does not call remote provider APIs or other remote services.

Demonstrate formatted but invalid configuration

fmt can exit successfully even when configuration is semantically invalid for terraform validate. Create a separate error lab directory so you do not break the main fmt examples:

bash
mkdir -p ~/terraform-labs/terraform-fmt-errors

Switch into the error lab before you write invalid configuration:

bash
cd ~/terraform-labs/terraform-fmt-errors

Write configuration with a semantic error — missing quotes around a string value:

hcl
resource "local_file" "demo" {
  content  = "bad syntax"
  filename = missing_quote
}

Save that as main.tf. The file is already canonically formatted, so ask fmt whether it would change anything:

bash
terraform fmt main.tf

terraform fmt exits successfully and prints nothing because the file already matches canonical formatting. The bare missing_quote reference is still invalid for this configuration, which terraform validate catches next.

Validation requires an initialized working directory. Add provider requirements:

hcl
terraform {
  required_version = ">= 1.12.0"

  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

Save that as versions.tf, then initialize without configuring a remote backend:

bash
terraform init -backend=false

Sample output:

output
Terraform has been successfully initialized!

Run validate on the formatted but invalid configuration:

bash
terraform validate

Sample output:

output
╷
│ Error: Invalid reference
│
│   on main.tf line 3, in resource "local_file" "demo":
│    3:   filename = missing_quote
│
│ A reference to a resource type must be followed by at least one attribute
│ access, specifying the resource name.
╵

fmt reported no changes; validate caught the reference error. Run both — fmt before you commit, validate before you plan.

fmt vs validate comparison

terraform fmt terraform validate
Purpose Canonical formatting and style Syntax and internal configuration consistency
Requires init No Yes — providers must be installed
Modifies files Yes by default; -check disables writes No — read-only analysis
Catches invalid references and schema issues No Yes — based on installed provider schemas, not live API calls
Typical CI use terraform fmt -check -recursive terraform init -backend=false then terraform validate
Exit code on failure Non-zero when unformatted (3 on 1.15.x for drift) Non-zero when validation fails (1 for configuration errors)

Use fmt first so reviews focus on behavior. Use validate to prove the configuration is internally consistent before you plan.


Common terraform fmt problems

Symptom Likely cause Fix
fmt exits 0 but files look unchanged Files already match canonical style Expected — rerun after edits that break alignment
Nested module still misformatted Omitted -recursive Run terraform fmt -recursive or fmt inside the module directory
CI fails with exit 3 and a file list Committed code was not formatted Run terraform fmt -recursive locally, commit the diff
fmt errors on a file HCL parse error fmt cannot recover from Fix syntax first; fmt formats valid native syntax
Large diff after upgrading Terraform New fmt rules in a newer CLI release Run fmt once on a branch, review the style-only diff, commit
Passing .tf.json to fmt JSON targets are unsupported Format .tf / .tfvars / .tftest.hcl only, or maintain JSON manually

fmt fails with a parse error

Single-line blocks with multiple arguments can be valid after expansion but confuse the parser when everything is crammed on one line. If fmt prints Invalid single-argument block definition, split arguments across lines yourself or fix brace placement, then rerun fmt.

CI reports formatting drift on unchanged code

Another developer may have committed unformatted files. Run terraform fmt -check -diff -recursive locally to print the same diff CI sees, apply terraform fmt -recursive, and push the formatting-only commit.


References


Summary

terraform fmt is the quick formatting pass every Terraform repository should adopt.

You walked through:

  • Rewriting misaligned .tf files
  • Checking style with -check
  • Previewing patches with -diff
  • Reaching into child modules with -recursive

The exit status from -check is what CI scripts key on — non-zero means someone needs to run fmt before merge.

fmt does not replace terraform validate:

  • Formatting keeps diffs readable
  • Validation checks syntax and internal consistency against installed provider schemas after terraform init

Run terraform fmt -check -recursive in automation, fix drift locally with terraform fmt -recursive, and treat validate as the next gate before terraform plan.

The mistake to avoid is assuming a green fmt run means configuration is deployable. When nested directories stay ugly, -recursive was probably missing. When CI fails with a short file list and exit code 3, format those paths and commit — no functional change, just canonical style.

Next in the Core Workflow track: terraform validate in depth, then terraform plan.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)