Why You Need hcl-linter?
If you've worked with HCL (HashiCorp Configuration Language) in Terraform or Terragrunt, you know the pain: formatting debates in code reviews, subtle typos in block names, drift between environments, and the endless "just run terraform fmt" dance. hcl-linter exists to move those conversations from human review into deterministic, fixable checks.
More Than Just Formatting
Unlike terraform fmt or terragrunt hclfmt - which only touch whitespace - hcl-linter enforces structure. It checks block order, naming conventions, required fields, path references, duplicate labels, and more. Most importantly, many of these rules are auto-fixable, so your CI can not only fail fast but also auto-clean legacy configs.
-
hcl-linter check ./- CI-friendly lint that exits 1 on any issue -
hcl-linter fix ./- Apply fixable rules in-place (block order, array formatting, blank lines, naming) -
hcl-linter fix ./ --dry-run- See exactly what would change before committing -
hcl-linter fix ./ --format- Bootstrap formatting without any config
Config That Matches Your Files
hcl-linter uses per-filename config matching via .hcl-linter/ directory. One file per target filename (e.g., terragrunt.hcl, root.hcl, default.hcl as fallback). Config is HCL, not YAML or JSON - no context switching required. Configs support inheritance via extends and per-directory overrides (closest wins), making it ideal for monorepos.
# .hcl-linter/default.hcl
rules {
block_order {
enabled = true
order = ["include", "locals", "terraform", "dependency", "inputs"]
}
required_blocks {
required {
type = "terraform"
count = "once"
error = "missing terraform block"
}
}
}
Rules That Matter in PRs
- block_order - Enforces logical block sequencing; unlisted blocks placed after listed ones
- array_format - 1 item inline, 2+ items multiline with trailing commas
- name_validation - Enforces snake_case (or custom regex) on labels
-
duplicates - Catches duplicate
dependency "x"orinclude "x"labels - required_fields - Ensures critical attributes exist when a block is present
-
dependency_paths - Validates
config_pathactually exists on disk -
remote_state - Enforces
backendwhenrequire_backend = true -
hcl_functions - Validates
find_in_parent_folderspaths and warns onget_envwithout defaults -
terraform_block - Checks
source, version format, and deprecations -
count_for_each - Warns on
count = 0,for_each = {}, and conflicts -
dependency_outputs - Validates
dependency.x.outputs.yagainst actual outputs
Fix-Only Rules
Some issues can't be auto-fixed safely (e.g., missing source could hide bugs). Others - like formatting and ordering - are fix-only with no Check phase: blank_lines cleans up whitespace; array_format normalizes lists; block_order reorders blocks.
Zero-Config Fast Path
No config yet? Run hcl-linter fix ./ --format for opinionated defaults:
- Block order:
include → locals → terraform → dependency → inputs - Arrays with 2+ items → multiline with trailing commas
- Blank-line cleanup within and between blocks
This is perfect for one-off formatting or bootstrapping a new project before introducing config-based rules.
CI Integration
In CI, use hcl-linter check ./ for strict non-zero exit on violations, or hcl-linter fix ./ --dry-run to detect byte-level drift - including drift from fix-only rules that check would silently pass.
# Validate configs for typos
hcl-linter validate-config
# Lint all matching files
hcl-linter lint ./ --verbose
# Enforce formatting without config
hcl-linter fix ./ --format --dry-run
Why This Thread Matters
As teams grow and HCL files multiply, the drift compounds: noisy diffs, review time spent on layout, and ../vpc paths that silently point to moved resources. hcl-linter moves those concerns into deterministic, automated checks - not human reviewers.
The config is HCL. The rules are per-file/type. Some fixes are automatic, some not, because we still need people for some chores.
Follow the thread and stay tuned!
Originally published at https://bard.sh/posts/hcl_linter/
Top comments (0)