Initializing Enclave...

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. Run terraform validate after each edit.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your broken .tf file 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.tf or variables.tf in a root module: Every downstream module call 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 plan across prod, staging, and dev in parallel.
  • backend.tf malformed: 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 <<-EOT block in a user_data or policy attribute 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:

  1. Unclosed block brace — a resource, module, locals, or dynamic block missing its closing }
  2. Unterminated string literal — a " opened inside an attribute value, never closed
  3. Broken heredoc<<EOT or <<-EOT with the closing delimiter EOT missing, indented incorrectly, or followed by trailing whitespace
  4. Truncated file — a git merge conflict, partial scp, or editor crash that wrote an incomplete file to disk
  5. Mismatched for expression brackets[ for x in var.list : x.id missing the closing ]
  6. Invalid escape sequence inside a string\n inside 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 validate with -no-color for clean CI log parsing.
  • Use tflint with 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 validate did 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.formatOnSave with the Terraform formatter. Unclosed blocks become immediately visible.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →