How to Fix Terraform 'unexpected EOF' Malformed HCL Config Parser Error
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: Terraform's HCL parser hit an unexpected end-of-file — meaning a block, string, heredoc, or expression was opened and never closed, causing the entire config to be unparseable.
- How to fix it: Locate the unclosed brace
{}, bracket[], parenthesis(), quote", or heredoc delimiter and close it. Runterraform validateafter each edit. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your broken
.tffile and get the corrected HCL output without sending your config to any external server.
The Incident (What Does the Error Mean?)
You ran terraform plan or terraform init and got slapped with:
Error: Failed to parse config
on main.tf line 47:
47: }
Unexpected end of file; expected a block definition, attribute, or end of file.
or the more terse variant:
Error: Failed to parse config 'unexpected EOF' malformed HCL
Immediate consequence: Terraform cannot load any resource in the affected file. This is not a partial failure — the entire configuration module is dead. terraform plan, terraform apply, terraform validate, and terraform destroy all abort immediately. If this hits a CI/CD pipeline mid-deployment, your infrastructure state is frozen. No rollback, no apply, nothing proceeds.
The Attack Vector / Blast Radius
This is not a runtime error — it is a parse-time hard stop. The blast radius depends on which file is malformed:
main.tforvariables.tfin a root module: Every downstreammodulecall that references this root is broken. A monorepo with 12 environment workspaces all fail simultaneously.- A shared module in a registry or Git source: Every consumer of that module across all environments inherits the failure. A single bad commit to a shared VPC module can kill
planacross prod, staging, and dev in parallel. backend.tfmalformed: Terraform cannot initialize state backend. Remote state is inaccessible. If you're mid-migration between backends, you risk state lock corruption.- Heredoc truncation (most dangerous): A truncated
<<-EOTblock in auser_dataorpolicyattribute silently cuts off IAM policies or cloud-init scripts. The parser fails, but if somehow bypassed (older Terraform versions), you get a malformed policy that may grant unintended access or a broken bootstrap script on EC2.
The common causes, ranked by frequency in production incidents:
- Unclosed block brace — a
resource,module,locals, ordynamicblock missing its closing} - Unterminated string literal — a
"opened inside an attribute value, never closed - Broken heredoc —
<<EOTor<<-EOTwith the closing delimiterEOTmissing, indented incorrectly, or followed by trailing whitespace - Truncated file — a git merge conflict, partial
scp, or editor crash that wrote an incomplete file to disk - Mismatched
forexpression brackets —[ for x in var.list : x.idmissing the closing] - Invalid escape sequence inside a string —
\ninside a non-templatefile string context confusing the lexer
How to Fix It (The Solution)
Basic Fix: Bracket/Brace Audit
The fastest triage is a brace-count diff. In your terminal:
# Count opening vs closing braces in the offending file
grep -o '{' main.tf | wc -l
grep -o '}' main.tf | wc -l
# They must match. Same for [ ] and ( )
Then run the native validator:
terraform fmt -check main.tf # flags formatting issues
terraform validate # reports exact line of parse failure
Example 1 — Unclosed resource block:
resource "aws_security_group" "web" {
name = "web-sg"
description = "Web tier security group"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
- # Missing closing brace for ingress block
+ }
+}
Example 2 — Truncated heredoc in an IAM policy:
resource "aws_iam_role_policy" "lambda_exec" {
name = "lambda-exec-policy"
role = aws_iam_role.lambda.id
policy = <<-EOT
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup"],
"Resource": "*"
- }]
- # EOF hit here — closing EOT delimiter missing entirely
+ }]
+ }
+ EOT
}
Example 3 — Broken for expression:
locals {
- subnet_ids = [ for s in var.subnets : s.id
+ subnet_ids = [ for s in var.subnets : s.id ]
}
Enterprise Best Practice: Enforce HCL Validity at Commit Time
Never let malformed HCL reach a shared branch. The fix is pre-commit enforcement:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
- rev: v1.77.0 # pinned — update this regularly
+ rev: v1.96.1 # latest stable as of mid-2025
hooks:
- - id: terraform_fmt
+ - id: terraform_fmt
+ - id: terraform_validate
+ - id: terraform_tflint
+ args:
+ - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl
Additionally, add a CI gate in your GitHub Actions / GitLab CI pipeline:
# .github/workflows/terraform-validate.yml
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
- terraform_version: "1.5.0"
+ terraform_version: "1.9.x" # Use constraint, not pinned patch
- name: Terraform Init
run: terraform init -backend=false
- # Missing validate step — this is why malformed HCL reached main
+ - name: Terraform Validate
+ run: terraform validate
+ - name: Terraform Format Check
+ run: terraform fmt -check -recursive
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Three-layer defense. Implement all three — each catches what the previous misses.
Layer 1 — Local (pre-commit hook):
# Install once per developer machine
pip install pre-commit
pre-commit install
# Now terraform fmt + validate runs on every git commit
Layer 2 — CI Pipeline Gate (non-negotiable blocking step):
- Use
terraform validatewith-no-colorfor clean CI log parsing. - Use
tflintwith the AWS/Azure/GCP ruleset plugin for provider-specific syntax validation beyond what the core parser checks. - Fail the pipeline on exit code
!= 0. Do not warn-only.
Layer 3 — Static Analysis with Checkov:
pip install checkov
checkov -d . --framework terraform
# Checkov parses HCL independently of Terraform binary
# Catches syntax errors AND security misconfigs in one pass
Layer 4 (for teams using Terraform Cloud / Atlantis):
- Enable Sentinel policies or OPA policy-as-code that reject any plan where
terraform validatedid not pass as a prerequisite run task. - Set branch protection rules: no direct push to
main/master. All changes via PR. The CI validate gate runs on every PR.
Editor-level (zero-cost, immediate):
- Install the HashiCorp Terraform extension for VS Code. It runs the HCL parser in real-time and underlines malformed syntax before you ever commit.
- Enable
editor.formatOnSavewith the Terraform formatter. Unclosed blocks become immediately visible.