Why this matters
A bug costs least when caught early. The chain:
- In the editor. An IDE plugin (HashiCorp Terraform extension for VS Code, IntelliJ HCL) shows errors immediately.
- At pre-commit. The hook blocks unformatted or invalid code from entering the repository.
- In CI. The last checkpoint, catching whatever slipped past the local hook.
- In review. A human.
pre-commit sits at the second level. It also covers the third: CI runs the same commands that developers run locally.
Installation
pip install pre-commit
In the repository, .pre-commit-config.yaml:
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.95.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
args:
- --args=--minimum-failure-severity=warning
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-merge-conflict
Register the hooks with git:
pre-commit install
From this point on, git commit runs the hooks. If any hook fails, the commit
is rejected.
What the main hooks do
| Hook | What it runs | Effect on files |
|---|---|---|
terraform_fmt | terraform fmt (with auto-fix) | May modify files. Re-stage and commit again. |
terraform_validate | terraform validate | No file changes. Fails on syntax errors and init problems. |
terraform_tflint | tflint | No file changes. Fails on tflint rule violations. |
terraform_docs | Regenerates README with inputs/outputs | Modifies files. |
terraform_trivy | trivy config | No file changes. Security findings. |
terraform_checkov | checkov | No file changes. Security findings. |
You do not need all of them. The baseline is terraform_fmt plus
terraform_validate. tflint is useful. Trivy and Checkov are slow per commit;
most teams add them only in CI.
fmt: auto-fix vs check-only
For local pre-commit, let terraform_fmt apply auto-fix: changes are applied
automatically, the developer runs git add and commits again. No manual editing
needed.
In CI, use terraform fmt -check without auto-fix. If the code is not
formatted, the step fails. This catches cases where the developer skipped
pre-commit (committed through the UI or edited in a merge request directly).
Configuration in .pre-commit-config.yaml:
- id: terraform_fmt
args:
- --args=-recursive
# no -check; auto-fix is desirable locally
In CI, run:
terraform fmt -check -recursive .
validate: what it checks and what it does not
terraform init -backend=false # no real backend required
terraform validate
validate checks:
- HCL syntax.
- That the referenced resources exist in the providers.
- That variable and attribute types match.
- That there are no dependency cycles (though
plancatches those too).
It does not check:
- Whether the cloud will accept the config (that is what
planshows). - Whether variable values are valid (only
validationblocks do that). - Whether dependencies actually work end-to-end (that requires
apply).
validate is a quick sanity check. Without it, a typo in an attribute name can
slip through and surface 30 minutes later when plan runs in CI.
See tf-validate.
tflint hook
- id: terraform_tflint
args:
- --args=--minimum-failure-severity=warning
- --args=--enable-rule=terraform_naming_convention
tflint focuses on style and provider-specific best practices, not security. Examples: "aws_instance.instance_type is not a valid type", "module without a version constraint", "output without a description".
Teams that add tflint to pre-commit typically see code-review noise drop significantly.
Running all hooks at once
pre-commit run --all-files
Use this when:
- You just enabled pre-commit on a legacy repository.
- You want to confirm the entire repository is clean before a merge.
- CI runs this instead of separate steps.
CI pipeline
# .github/workflows/lint.yml
on: [pull_request]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- run: |
curl -L https://github.com/terraform-linters/tflint/releases/download/v0.55.1/tflint_linux_amd64.zip -o tflint.zip
unzip tflint.zip
sudo mv tflint /usr/local/bin/
- uses: pre-commit/action@v3.0.1
pre-commit/action installs pre-commit itself and runs pre-commit run --all-files. One step covers all hooks.
Skip mechanism
Sometimes you need to commit while bypassing a hook (for example, fixing a blocking bug when the formatting issue is not the priority right now):
SKIP=terraform_fmt git commit -m "fix: ..."
This is a local bypass only; CI will still check. Use sparingly.
To skip pre-commit entirely:
git commit --no-verify
This is poor practice. Find out why the hook is failing instead.
Updating hook versions
pre-commit autoupdate
This updates the rev: fields in .pre-commit-config.yaml. Review the
git diff, open a PR, run pre-commit run --all-files, and confirm nothing
broke.
Pitfalls
-
pre-commit only checks files changed in the commit. If you do not modify a
.tffile,terraform_fmtwill not check it. To scan the whole repository, runpre-commit run --all-files. CI typically runs exactly this command, catching files that were left unformatted in earlier commits. -
The terraform_validate hook needs
init. At the pre-commit stage, init may not have run.pre-commit-terraformperforms init with-backend=falseautomatically, but this takes time and makes pre-commit slow on large projects. An alternative: run validate only in CI. -
Hooks only see tracked files. Files listed in
.gitignoreare not checked. This is usually acceptable, but.tfvarsfiles are often ignored, so validate will not cover them. -
autoupdatecan break things. A new major version of a hook often changes its arguments. Update deliberately, in a separate PR, and run--all-filesto verify. -
pre-commit-terraform requires the terraform CLI. If a developer does not have it installed locally, the hook fails immediately. Document prerequisites in the README, or use a devcontainer or asdf to provide a reproducible environment.
-
CI must duplicate pre-commit. Never rely solely on local pre-commit. A developer can bypass it (
--no-verify, commit through the UI, or forget to runpre-commit install). CI is the only real gate.