How to Validate Terraform HCL Configurations Locally (2026 Guide)

#1Validating Terraform HCL locally before you hit CI
What we tested: We validated environment files, Docker configurations, and CI pipeline syntax using the browser-based tools on this site. All parsing and validation runs locally.
Terraform validation is easiest to understand as a stack of checks, not a single command.
The local workflow is straightforward: parse the file locally, check Terraform’s own schema rules, then add lint and policy checks before the change ever reaches CI.
#2What "Validate" Actually Means in Terraform
A common source of frustration is that terraform validate does less than its name suggests. It checks that the configuration is internally consistent, variables referenced, modules wired correctly, resource types known to the loaded providers, but it does not check whether your AWS credentials are valid, whether the resource arguments correspond to anything real, or whether your tags follow the company convention. That is what the other three layers are for.
The four-layer model:
| Layer | Tool | Catches |
|---|---|---|
| 1. Syntax / formatting | terraform fmt, browser-based HCL formatter | Bad braces, wrong quoting, inconsistent indentation |
| 2. Schema / consistency | terraform validate | Unknown resource type, wrong argument name, type mismatch, undeclared variable |
| 3. Lint / best practice | tflint, tfsec | Deprecated arguments, missing tags, hard-coded credentials, IAM wildcards |
| 4. Policy / compliance | checkov, OPA / conftest, Terraform Cloud Sentinel | "All S3 buckets must have encryption," "no public load balancers," org-specific rules |
Treat them as ladders, not alternatives. A clean terraform validate does not mean your config is good, it means it parses and is self-consistent. The lint and policy layers catch the real-world bugs.
#2Layer 1, Syntax in 30 Seconds
You pasted a snippet from a blog, an LLM, or a colleague's Slack. Before downloading anything, you want to know if it parses.
The fastest path: paste it into the Terraform HCL Formatter. It runs a real HCL parser in your browser, formats valid input, and points at the line and column of bad input. No install, no provider download, no network round-trip. For a five-line snippet, this is faster than opening your terminal.
When you have a checkout open, terraform fmt does the same job at the filesystem level:
terraform fmt -recursive # rewrite in place
terraform fmt -check -recursive # exit non-zero if any file needs changes
terraform fmt -diff -recursive # show what would changeThe -check form is the right one for CI: it reports drift without modifying files. The -diff form is the right one for code review: it shows what fmt would do without doing it.
The most common Layer-1 errors:
- Unclosed braces or brackets, usually because a heredoc string is missing its terminator.
terraform fmtwill tell you, but it will not always tell you clearly; the browser formatter often gives a better column pointer. - Mixed tabs and spaces, HCL accepts both, but consistency matters.
terraform fmtnormalizes to two-space indentation. - Wrong quoting, single quotes are never valid in HCL; only
"..."and heredocs (<<-EOF ... EOF) are.
#2Layer 2, Schema Validation with terraform validate
terraform validate checks the configuration against the schemas of the providers declared in the configuration. To run it, you need an initialised working directory (terraform init first), but you do not need real cloud credentials, validation is offline once providers are downloaded.
terraform init -backend=false
terraform validateThe -backend=false flag is the small unlock: it lets init complete without configuring remote state, which means you can validate inside CI or a sandbox without leaking state-backend credentials. It is the right default for any environment where validation is the only operation.
What terraform validate catches that terraform fmt does not:
- Unknown resource type,
resource "aws_s4_bucket"instead ofaws_s3_bucket. - Wrong argument name,
bucket_name = "foo"instead ofbucket = "foo". - Type mismatch, passing a list where a string is expected, or vice versa.
- Undeclared variable,
var.environmentreferenced but novariable "environment"block exists. - Undeclared output, module referenced an output that the child module does not expose.
- Broken
for_each/count,for_eachover a non-iterable or with anullargument.
What it does not catch:
- Whether the resource will actually create.
aws_s3_bucket.bucketwith a name that already exists globally will fail atapply, not atvalidate. - Whether IAM permissions are sufficient.
- Whether tags follow your convention.
- Whether the version of your provider exists.
For those, you need Layer 3.
#2Layer 3, Lint with tflint and tfsec
tflint and tfsec (now folded into Aqua Trivy, but still distributed separately) are the two essential static analysers for Terraform.
#3tflint, provider-aware lint
tflint reads the same configuration terraform validate does and applies a rule set that includes:
- Deprecated arguments, e.g.
aws_db_instance.password(deprecated in favour ofaws_db_instance.manage_master_user_passwordsince AWS provider 5.x). - Unused declarations, variables, outputs, locals defined but never referenced.
- Module pinning, modules sourced without a version constraint.
- Naming conventions, enforce
snake_case, prefix conventions, etc.
brew install tflint # or apt, yum, scoop
tflint --init
tflint --recursiveThe killer feature is the AWS plugin (tflint-ruleset-aws, also for Azure and GCP), which validates instance types, AMI IDs, ARNs, and region codes against real provider catalogues. That catches "I typed t2.miro instead of t2.micro" before you ever run apply.
#3tfsec / trivy config, security lint
The security-focused counterpart:
brew install tfsec
tfsec .tfsec checks for things like:
- S3 buckets without encryption at rest.
- Security groups open to
0.0.0.0/0on sensitive ports. - IAM policies with
Action: "*"andResource: "*". - RDS instances with public IPs.
- Plain-text secrets in
defaultortags.
Output is JSON-friendly for CI, with a stable rule ID per check (e.g. aws-s3-enable-bucket-encryption) that you can ignore explicitly with an inline #tfsec:ignore:aws-s3-enable-bucket-encryption comment when you have a documented exception.
#2Layer 4, Policy with checkov and OPA
Layer 4 is where your organisation's rules live. The two tools that dominate this layer in 2026:
checkov(Bridgecrew, now Prisma Cloud), ships with 1,000+ built-in policies, supports custom Python and YAML rules. Best for "tell me the obvious mistakes."- OPA +
conftest, Rego-based policy engine, infinitely flexible, used when you need policies as code that the security team owns. Best for "enforce our specific compliance rules."
A representative checkov run:
pip install checkov
checkov -d . --framework terraform --skip-check CKV_AWS_18--skip-check lets you suppress checks that do not apply to your org, with a stable check ID. Production teams typically suppress 10–30 checks they have explicitly decided do not apply, and gate CI on everything else.
For custom rules, conftest plus Rego is the standard pattern:
package terraform
deny[msg] {
resource := input.resource.aws_s3_bucket[name]
not resource.server_side_encryption_configuration
msg := sprintf("S3 bucket %s is missing encryption", [name])
}The pattern: terraform show -json plan.json | conftest test -p ./policy/ plan.json. Rego enforces what checkov cannot, anything that depends on your specific organisation, naming conventions, account boundaries, or shared service catalogue.
#2The Pre-Commit Setup That Catches Everything in Five Seconds
A focused .pre-commit-config.yaml runs all four layers locally before code ever reaches CI:
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.95.0
hooks:
- id: terraform_fmt
- id: terraform_validate
args: ["--args=-backend=false"]
- id: terraform_tflint
- id: terraform_tfsec
- id: terraform_checkov
args: ["--args=--quiet"]Install:
pip install pre-commit
pre-commit installFrom that point on, every git commit runs the four layers across the files you changed. The first run is slow (downloads tflint, tfsec, checkov binaries); every subsequent commit is sub-second on a touched-files-only basis. CI runs the same hooks against the full tree, so the local hook and CI cannot disagree.
The single biggest win from this setup: by the time you push, you have already run the same checks CI will run, on the same files. The "I forgot to format" / "I forgot to add a tag" / "I left a wildcard IAM" round-trips are eliminated.
#2The Five HCL Errors That Catch People Most Often
#31. Block label vs argument confusion
# WRONG
resource "aws_s3_bucket" {
name = "my-bucket"
}
# RIGHT
resource "aws_s3_bucket" "main" {
bucket = "my-bucket"
}Block labels (the strings after the keyword) are positional and required. resource needs two: the type and the local name. The argument inside is bucket, not name.
#32. Heredoc indentation
# WRONG, heredoc terminator must be at column 0
policy = <<EOF
{
"Version": "2012-10-17"
}
EOF
# RIGHT, use <<- (dash) to strip leading whitespace
policy = <<-EOF
{
"Version": "2012-10-17"
}
EOFThe <<- form (the dash) strips the least-common leading whitespace from every line, including the terminator. That is what you want 95 % of the time inside a nested block.
#33. Interpolation in plain strings
# OLD STYLE, pre-0.12, still works but linted
name = "user-${var.environment}"
# MODERN STYLE, same thing, but bare references are cleaner outside strings
name = "user-${var.environment}" # fine inside strings
provider = var.environment # bare reference, no interpolationThe deprecated form is "${var.environment}" as the entire string, use the bare reference instead. Inside a string template, ${...} is correct.
#34. for_each on a list
# WRONG, for_each requires a set or map
for_each = ["a", "b", "c"]
# RIGHT, convert with toset()
for_each = toset(["a", "b", "c"])for_each needs a map or set so each entry has a stable key. A list does not provide stable keys (re-ordering would re-create resources), so Terraform refuses. toset() is the standard conversion.
#35. Provider block in a module
A child module should declare required providers, not configure them:
# In modules/network/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}The actual provider "aws" { region = ... } block lives in the root module, not the child. Configuring a provider inside a module is technically allowed but is deprecated and breaks for_each on modules, every modern tutorial assumes you do not.
#2When the Browser Tester Is Faster Than the CLI
Three concrete cases where you should reach for the Terraform HCL Formatter over terraform fmt:
- You have a snippet, not a project. A six-line snippet from a colleague does not justify
mkdir tf-test && terraform init. Paste it into the browser, see whether it parses, copy it back. - You are reviewing a PR diff in GitHub. Open the formatter in a new tab, paste the added lines, get instant column-level feedback on parse errors. Faster than checking out the branch.
- You are working on a machine without Terraform installed. An iPad, a borrowed laptop, a Cloud Shell with no IAM permissions to
init. The browser tool needs nothing but a browser tab.
For everything else, running the full validate / tflint / tfsec / checkov ladder, install the CLIs and use pre-commit. The browser tool is the cheap-first-pass; the CLI ladder is the comprehensive check.
#2Frequently Asked Questions
#3What is the difference between terraform fmt and terraform validate?
terraform fmt is a syntax check, does the file parse, and is it formatted according to canonical HCL conventions? It runs in milliseconds against the raw text. terraform validate is a schema check, given a provider, do all the resources, arguments, variables, and modules line up consistently? It needs terraform init to have run so providers are downloaded. The two catch different bugs: fmt catches unclosed braces and bad indentation; validate catches unknown resource types and undeclared variables. Run both in your pre-commit hook, they are not alternatives.
#3How do I check Terraform syntax without installing Terraform?
Use a browser-based HCL parser. The AllDevToolsHub Terraform HCL Formatter runs a real HCL parser in your browser tab, paste a snippet, get formatted output and column-level error reporting, with nothing installed. This is the fastest way to validate an LLM-generated snippet or a Stack Overflow paste before you commit to setting up a full Terraform project. For schema-level validation (does this argument exist on this resource type?) you still need terraform validate against an init-ed working directory, because that requires the provider schema.
#3What does terraform validate actually catch?
Schema and consistency errors only: unknown resource types, unknown argument names, type mismatches, undeclared variables, broken module wiring, malformed for_each and count. It does not catch: invalid IAM permissions, AWS resource names that conflict globally, missing tags, deprecated arguments, security misconfigurations, or any organisation-specific rules. For those, layer tflint (deprecation and provider-specific checks), tfsec or trivy config (security), and checkov or OPA (policy / compliance) on top.
#3How do I validate Terraform in CI without real cloud credentials?
Run terraform init -backend=false followed by terraform validate. The -backend=false flag tells init to skip remote-state configuration, so it completes against any environment with internet access (to download providers) and no cloud credentials. This is the standard CI pattern for validation-only jobs. Pair it with tflint, tfsec, and checkov, all of which run offline once their rule databases are present. The full ladder runs in 30–60 seconds even on a fresh CI runner.
#3Should I use checkov, tfsec, or both?
Both, because they overlap by maybe 40 % and the non-overlapping 60 % is where the real coverage gain is. tfsec (now Trivy) is focused, fast, and ships clean defaults for ~150 security rules. checkov ships ~1,000 rules covering security plus compliance frameworks (SOC 2, PCI, HIPAA, NIST) and supports custom Python rules. Run tfsec in pre-commit for the fast feedback, and checkov in CI for the compliance breadth. If you can only pick one, pick checkov for the wider coverage and accept the slower run.
Terraform validation works best as a ladder, not a single check. The browser formatter for snippets, terraform fmt for whitespace, terraform validate for schema, tflint for provider-aware lint, tfsec for security, and checkov for policy, each catches a class of bug the others miss. The five-second pre-commit setup runs all of them on every commit and is the single highest-leverage thing you can add to a Terraform repo.
Format and parse-check any HCL snippet now at the AllDevToolsHub Terraform HCL Formatter, runs entirely in your browser, nothing leaves your tab. Pair it with the Regex Cheatsheet for the input-validation patterns you will reach for inside validation blocks, and the JWT Tokens Explained guide for the surrounding identity surface that Terraform-managed IAM lives in.
Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.
#2Sources / Further reading
- HashiCorp - Terraform HCL syntax documentation
- HashiCorp - terraform validate command
- HashiCorp - terraform plan documentation
- Checkov - Terraform static analysis
#2Try These Tools
Quick Summary
>- A practical guide to validating Terraform HCL locally — from a 30-second syntax check in the browser to terraform fmt, terraform validate, tflint, checkov, and the pre-commit setup that catches drift before it hits CI. Covers the four layers of validation, the most common HCL parse errors, and the gotchas that catch teams between Terraform versions.
Key Takeaways
- Terraform provides three levels of local validation: terraform fmt (formatting), terraform validate (syntax), and terraform plan (execution preview).
- Static analysis tools (Checkov, tflint, tfsec) catch security misconfigurations and policy violations before terraform apply.
- HCL syntax errors are caught by terraform validate, but logical errors (wrong resource attributes, missing dependencies) require terraform plan.
When to use it
- Validating a Terraform configuration before submitting a pull request in CI/CD.
- Checking for security misconfigurations (public S3 buckets, overly permissive security groups) before applying.
- Formatting HCL files to match team style conventions with terraform fmt.
Common Mistakes
- Running terraform apply without terraform plan — always preview changes before applying to production infrastructure.
- Not using a state locking mechanism — concurrent terraform apply operations can corrupt state.
- Ignoring terraform validate errors — syntax errors that pass fmt may still fail validate with unclear messages.
How to Validate Terraform HCL Configurations Locally (2026 Guide), Frequently Asked
What is the difference between terraform validate and terraform plan?
terraform validate checks HCL syntax and internal consistency without connecting to providers. terraform plan connects to providers, reads current state, and shows what changes would be applied — it catches logical errors that validate misses.
How do I check Terraform configurations for security issues?
Use static analysis tools: Checkov (open source, 1000+ policies), tfsec (Aqua Security), or tflint with custom rules. Run them in CI/CD before terraform apply to catch misconfigurations early.
Tools Mentioned in This Article
Terraform HCL Formatter
Format and align your Terraform HCL (.tf) configurations locally.
.env File Parser
Parse, edit, and export .env files as JSON, Docker flags, or shell exports.
Crontab Expression Generator
Build and validate cron expressions with a visual editor.
.gitignore Generator
Generate .gitignore files for any language, framework, OS, or editor.
Tools, tactics, and toughened-up tips, once a week
New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.
Found an error or have feedback?
We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.