terraform fmt rules, without the binary
PasteKit uses a built-in HCL parser and printer (terraform fmt style). It applies the same conventions HashiCorp’s formatter applies:
- every attribute and nested block goes on its own line, indented by one level per block
- the
=signs of consecutive single-line attributes are aligned into a column - spacing inside expressions is made canonical:
a = b,{ Name = "site" },[1, 2] - block headers are written as
resource "type" "name" {
Like terraform fmt, it is conservative about everything else. Line breaks you chose inside lists, function calls and parentheses are kept, blank lines between blocks are left alone, and strings, ${...} templates and heredocs are printed exactly as written. Comments in all three styles (#, //, /* */) stay attached to the lines they describe.
Any HCL2 file works, not just Terraform: .tfvars, terragrunt.hcl, Packer templates, Nomad job specs and Vault or Consul configuration all share the syntax.
Indentation and keyboard shortcuts
There are no HCL-specific options. Terraform’s own style is two spaces, which is also the toolbar default; you can choose another indent or tabs if a non-Terraform tool expects them, but terraform fmt -check in CI will only accept two spaces. Alignment widths are computed per group, so a blank line, a comment line or a nested block starts a new alignment group, exactly as in the CLI.
Ctrl/Cmd+Enter formats, Ctrl/Cmd+Shift+C copies, and Ctrl/Cmd+K opens the command palette. Configuration files have no minified form, so Ctrl/Cmd+Shift+M is not offered.
Errors and limits
The parser stops at the first structural problem and points at it: an unclosed { names the block type and the line it was opened on, an unterminated heredoc names the marker it was waiting for, and two attributes written on one line separated by a comma are flagged with a hint to split them. A missing = shows up as “Expected ‘{’ to open block”, because name value looks like the start of a nested block to an HCL parser.
This is a formatter, not terraform validate. It does not download providers, so unknown resource types, misspelled arguments or wrong variable types are not reported. Run terraform validate or tflint for that.
JSON-syntax Terraform (.tf.json) is plain JSON; format it with the JSON formatter instead.
A practical workflow for code review: paste the block from a pull request, format it, and compare. If the result differs from what was committed, the author’s editor is not running terraform fmt on save, and CI with terraform fmt -check -recursive will fail. Formatting here first saves a round trip through the pipeline. The same applies to modules copied from documentation or the Terraform Registry, whose examples are frequently pasted with inconsistent indentation.
Examples
Module call and for_each
All seven module arguments share one alignment column; the second block aligns separately.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
name = "main"
cidr = "10.0.0.0/16"
azs = ["ap-southeast-1a","ap-southeast-1b"]
enable_nat_gateway = true
}
resource "aws_iam_user" "dev" {
for_each = toset(["aisha","ben"])
name = each.key
}module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1"
name = "main"
cidr = "10.0.0.0/16"
azs = ["ap-southeast-1a", "ap-southeast-1b"]
enable_nat_gateway = true
}
resource "aws_iam_user" "dev" {
for_each = toset(["aisha", "ben"])
name = each.key
}
A .tfvars file
Top-level variable assignments are aligned and spacing inside the object and list is normalised.
region="eu-west-1"
instance_type = "t3.micro"
tags={Team="platform",CostCenter="1234"}
allowed_cidrs=["10.0.0.0/8","192.168.0.0/16"]region = "eu-west-1"
instance_type = "t3.micro"
tags = { Team = "platform", CostCenter = "1234" }
allowed_cidrs = ["10.0.0.0/8", "192.168.0.0/16"]
for expression and heredoc
The heredoc body is left byte-for-byte; the attribute before it is not aligned with it because a heredoc is multi-line.
locals {
names = [for u in var.users : upper(u.name) if u.active]
policy = <<EOT
{"Version": "2012-10-17"}
EOT
}locals {
names = [for u in var.users : upper(u.name) if u.active]
policy = <<EOT
{"Version": "2012-10-17"}
EOT
}
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Unclosed '{' of block 'resource' opened on line 1Explained | A block body is never closed, so the parser reaches the end of the file still inside it. | Add the missing ‘}’ at the end of that block. Nested blocks inside it are a common place to lose one. |
Expected '{' to open block 'x', found '1' | An attribute was written without =, so x 1 looks like the header of a block named x. | Write it as x = 1. |
Unterminated string starting on line 1 | A quoted string has no closing " before the end of the line. HCL strings cannot span lines. | Close the quote, or switch to a heredoc (<<EOT ... EOT) for multi-line text. |
Unexpected ',' in the value of 'a' | Two attributes were put on one line separated by a comma, as you might in JSON. | Put each attribute on its own line. Commas belong only inside { } object values and [ ] lists. |
Unterminated heredoc: no closing 'EOF' line for the heredoc starting on line 1 | The terminator line is missing, misspelled or has other text on it. | Add a line containing only the marker (leading spaces are allowed with <<-EOF). |
Frequently asked questions
Is the output identical to terraform fmt?
It follows the same rules: two-space indentation, aligned equals signs and canonical spacing, with your own line breaks and heredocs untouched. It is an independent implementation, so report any difference you find.
Does it validate my Terraform?
It checks syntax only. Provider schemas, references and types are checked by terraform validate, which needs the providers installed.
Can I format Terragrunt or Packer files?
Yes. terragrunt.hcl, Packer .pkr.hcl and Nomad job files use the same HCL2 syntax and format the same way.
Will it change the values or interpolations in my strings?
No. Quoted strings, template interpolations and heredoc bodies are printed exactly as written; only whitespace between tokens is adjusted.