Fast, opinionated Terraform naming linter & fixer – written in Go
tfsuit helps you enforce consistent, organisation‑wide naming rules for every Terraform variable, output, module and resource – in your editor, in CI and in your pull‑requests.
- Enforce naming policies once and share them across repos, teams and CI
- Catch inconsistent Terraform labels before they reach review or production
- Auto‑fix issues while keeping cross‑references in sync (no manual renames)
- Integrates with GitHub Actions, SARIF code scanning and editor tooling
| Feature | Notes | |
|---|---|---|
| Ultra‑fast core | Go implementation ▶ multi‑CPU parsing, intelligent cache | 10‑50× faster than the original Bash version |
| Configurable rules | HCL or JSON (tfsuit.hcl) |
Per‑type patterns, allow‑lists / ignore‑regex |
| Linter modes | scan (read‑only) |
Pretty, JSON or SARIF output |
| Auto‑fixer | fix – rewrites labels, updates all cross‑references |
--dry-run to preview, --write to apply |
| Code Scanning | SARIF + GitHub annotations | PR checklist + summary comment |
| GitHub Action | uses: josdagaro/tfsuit/action@v3 |
Runs in Docker, no build step |
| Homebrew formula | brew install josdagaro/tfsuit/tfsuit |
macOSÂ /Â Linux |
| Docker image | ghcr.io/josdagaro/tfsuit:<tag> |
Static binary, 6 MiB |
| VS Code extension | Preview: LSP‑based inline diagnostics & quick‑fix | Coming soon |
# 1. Install
brew install josdagaro/tfsuit/tfsuit
# 2. Drop a config file in your repo root
cat <<'EOF' > tfsuit.hcl
variables { pattern = "^[a-z0-9_]+$" }
resources { pattern = "^[a-z0-9_]+$" }
EOF
# 3. Scan your Terraform project
tfsuit scan ./infraFor CI enforcement, use the GitHub Action with fail: true to fail the job when violations are found.
brew tap josdagaro/tfsuit
brew install tfsuitUpdate to the latest tagged release:
brew update
brew upgrade tfsuitValidate your installation:
tfsuit --versionGrab the archive for your OS from the GitHub Releases page, extract and move tfsuit to a directory on your $PATH.
# latest stable
docker run --rm -v "$PWD:/src" ghcr.io/josdagaro/tfsuit:latest scan /srcAdd to your workflow:
- uses: josdagaro/tfsuit/action@v3
with:
path: ./infra # directory to scan (default '.')
config: .github/tfsuit.hcl # your rule file (default 'tfsuit.hcl')
format: sarif # pretty | json | sarif
fail: true # fail the job if violations foundThe action automatically uploads the SARIF file to GitHub Code Scanning.
variables {
pattern = "^[a-z0-9_]+$"
ignore_exact = ["aws_region"]
}
outputs {
pattern = "^[a-z0-9_]+$"
}
modules {
pattern = "^[a-z0-9_]+(_[a-z]+)?$"
ignore_regex = [".*experimental.*"]
require_provider = true
}
resources {
pattern = "^[a-z0-9_]+$"
require_provider = false
}
data {
pattern = "^[a-z0-9_]+$"
require_provider = false
}
files {
pattern = "^[a-z0-9_]+\\.tf$"
ignore_regex = ["locals.*\\.tf"]
}
block_spacing {
min_blank_lines = 1
allow_compact = ["variable", "output"]
}Compile‑time validation – invalid regex is caught at startup.
Set require_provider = true in any block to ensure Terraform declarations explicitly pin a provider. Modules default to require_provider = true, while variables, outputs, resources and data sources default to false. Override those defaults in tfsuit.hcl when you want the fixer to enforce providers for additional block types, use the files block to constrain every .tf filename (for example, enforcing snake_case only), and configure block_spacing to require a minimum number of blank lines between blocks (with optional exemptions for compact single-line variables/outputs). When enabled, tfsuit verifies:
resource "aws_s3_bucket" "logs" {
provider = aws.primary
}
data "aws_s3_bucket" "selected" {
provider = aws.primary
}
module "network" {
source = "../network"
providers = {
aws = aws.primary
}
}
`tfsuit fix` also injects the most-used provider when one is missing (for example `provider = aws.primary` or a `providers = { aws = aws.primary }` block). If no provider is defined anywhere, the command fails and creates a `providers.tf` with a comment reminding you to declare at least one aliased provider before retrying. The fixer understands the `providers = { ... }` mappings inside `module` blocks, so it can propagate aliases down to nested submodules even when the actual configurations live only at the root, renames `.tf` files that break your `files` pattern (e.g. `Bad-Name.TF` → `bad_name.tf`), and inserts blank lines between blocks to satisfy `block_spacing`.Generate a starter config from your repository’s current labels and a few interactive choices:
tfsuit init .Prompts include:
- Allow uppercase / hyphens in labels
- Optional module suffix pattern like
_[a-z]+ - Suggested
ignore_exactandignore_regexentries
Writes tfsuit.hcl in the selected path (asks before overwriting).
tfsuit scan [path] # lint only
-c, --config <file> # config file (default tfsuit.hcl)
-f, --format pretty|json|sarif
tfsuit fix [path] # auto‑fix labels
-c, --config <file> # config file (default tfsuit.hcl)
--fix-types # limit fixes to comma-separated kinds (file,variable,output,module,data,resource,spacing)
--dry-run # show diff
--write # apply changes
tfsuit init [path] # interactive config bootstrap (creates tfsuit.hcl)Example:
# CI – generate SARIF and upload to Code Scanning
mkdir results
tfsuit scan ./infra --format sarif > results/tfsuit.sarif
# Gradual fixes per kind
tfsuit fix ./infra --dry-run --fix-types file # only rename files
tfsuit fix ./infra --dry-run --fix-types spacing # enforce blank-line spacing
tfsuit fix ./infra --dry-run --fix-types module,resource,dataGiven a Terraform resource with a non‑conforming label:
resource "aws_s3_bucket" "BadBucket" {
bucket = "example"
}Scan output (pretty format):
$ tfsuit scan ./infra
infra/main.tf:2:15 resource.aws_s3_bucket.BadBucket label "BadBucket" does not match "^[a-z0-9_]+$"
Auto‑fix and review the change:
tfsuit fix ./infra --dry-run # see proposed rename
tfsuit fix ./infra --write # apply updates to all referencesThe fixer rewrites references (modules, locals, outputs) to keep your code compiling.
The upcoming extension provides live diagnostics and Quick Fix… to rename variables safely. Watch the project board for progress.
go vet ./... # static checks
go test ./... # unit tests- Go 1.24+ (matches
go.mod) - (optional) GoReleaser for snapshot packaging
go build ./cmd/tfsuit # compile binary into current directory
./tfsuit --help # inspect available commands
go run ./cmd/tfsuit scan ./samplesgo test ./...
go vet ./...Run the fixer against fixtures to verify behaviour:
go run ./cmd/tfsuit fix ./samples/simple --dry-run -c ./samples/simple/tfsuit.hcl
go run ./cmd/tfsuit fix ./samples/provider-chain --dry-run -c ./samples/provider-chain/tfsuit.hclUse snapshot releases to emulate the CI pipeline without publishing artifacts:
goreleaser release --snapshot --cleanThe command builds platform packages, Docker image and the Homebrew formula locally so you can spot issues before opening a release PR.
- SemVer determined from PR label (
major /minor /patch). - GoReleaser builds binaries, Docker image, Homebrew formula.
- Tags
vX.Y.Z, moving tagsvX,vX.Y.
Details in .github/workflows/release.yml.
MIT License – see LICENSE.
