modules.tf
modulestf facts

Review Terraform module pull requests against written rules

Agent skills for Claude Code and Codex. They check a change to a reusable module rule by rule against the terraform-aws-modules review rules: breaking changes, provider-schema coverage, and whether each addition belongs. Every rule they could not check is listed, never counted as a pass.

Install the skillsSee a real review

From the maintainer of terraform-aws-modules. Apache-2.0.

Changes requested before merge.

  • HIGHstructure.sibling-count-mismatchmain.tf:1342
  • HIGHprofile.region-not-allowedexamples/regional-nat/main.tf:8
  • MEDIUMstructure.validation-missingvariables.tf:1260

Behaviour evidence: 56 changes classified: 0 verified, 3 covered by a plan, 0 inert, 0 refuted, 53 not verified

Posts nothing until you confirm.

terraform-aws-modules/terraform-aws-vpc#1270. Read the posted review

Who it is for

For maintainers of reusable Terraform modules who review pull requests against the terraform-aws-modules rules, including pull requests written by other people's agents.

It applies one profile today: the terraform-aws-modules rules. It does not apply your internal standards; other profiles need changes to the skills and are future work.

Not for root configurations or live stacks.

A real review, step by step

The first three steps come from the review posted on terraform-aws-modules/terraform-aws-vpc#1270, a pull request that adds a regional NAT gateway mode. That pull request had no second round; step four shows what one looks like.

Request

Reads: the pull request reference. Runs as you, with your own GitHub login.

A pull request is open and you ask your agent for a review.

Review terraform-aws-modules/terraform-aws-vpc#1270 with the terraform-module-pr-review skill.

On a local branch, terraform-module-reviewer returns findings only, no verdict.

Findings

Reads: the profile's rules, each with an ID. Every finding carries a severity, a location and a fix.

Findings come back grouped by severity, the way a code review does.

  • HIGHstructure.sibling-count-mismatch1

    main.tf:1342 - aws_route.private_dns64_nat_gateway still counts local.nat_gateway_count (1 in regional mode), so only the first private route table gets 64:ff9b::/96.

  • HIGHprofile.region-not-allowed1

    examples/regional-nat/main.tf:8 - The new example sets local.region to "ap-south-1", a region the profile does not allow for examples.

    Fix In the locals block of examples/regional-nat/main.tf, set region to "eu-west-1", per code-conventions.md Regions.

  • MEDIUMstructure.sibling-block-asymmetry1

    main.tf:1314 - The availability_zone_address dynamic block sets only availability_zone, never availability_zone_id, so azs given as zone IDs, which the subnets accept, reach the wrong argument.

    Fix In the dynamic block of resource "aws_nat_gateway" "regional", set availability_zone and availability_zone_id with the regexall("^[a-z]+-", ...) test aws_subnet.private uses, the other one null.

  • MEDIUMstructure.validation-missing1

    variables.tf:1260 - availability_mode and eip_allocation take closed value sets but their validation blocks are commented out, so a typo such as "Regional" silently yields zonal NAT gateways.

    Fix In variable "nat_gateway_connectivity_type", restore both commented validation blocks.

  • MEDIUMstructure.version-floor-understated1

    examples/regional-nat/version.tf:7 - The example declares hashicorp/aws >= 6.24.0 while the module it calls requires >= 6.28, so the caller's floor is below its callee's.

    Fix In the terraform block of examples/regional-nat/version.tf, set the aws version to ">= 6.28", and drop the >= 6.24.0 comment in the example main.tf, per code-conventions.md Version floors.

  • MEDIUMcoverage.optional-argument-unexposed1

    main.tf:1250 - New aws_eip.regional_nat leaves Optional public_ipv4_pool, ipam_pool_id, network_border_group and address unexposed, with no documented omission.

    Fix Needs decision: expose these arguments or record why they are left out.

  • MEDIUMcoverage.attribute-not-output1

    main.tf:1299 - New aws_nat_gateway.regional exports route_table_id, auto_provision_zones, auto_scaling_ips and regional_nat_gateway_auto_mode, and no output reads them.

    Fix In outputs.tf, add output natgw_regional_route_table_id = try(aws_nat_gateway.regional[0].route_table_id, null), the route table AWS creates.

  • MEDIUMexamples.feature-undemonstrated1

    main.tf:1308 - Manual EIP mode, aws_eip.regional_nat and the availability_zone_address dynamic block, appears in no example; examples/regional-nat sets only eip_allocation = "auto".

    Fix In examples/regional-nat/main.tf, add a module "vpc_manual" call like module "vpc" with eip_allocation = "manual" in nat_gateway_connectivity_type.

  • MEDIUMstructure.comment-unsourced2

    main.tf:1224 - The added comment asserts a regional NAT gateway has no network_interface_id or public_ip of its own, and cites no source or date.

    variables.tf:1252 - The added comment asserts optional() defaults need Terraform 1.3 and cites no source or date.

    Fix On each comment, add the documentation URL and the date it was read, per code-conventions.md Evidence: comments, copied values, and availability.

  • MEDIUMexamples.versions-missing1

    examples/regional-nat/version.tf - The new example keeps its terraform block in version.tf; every sibling example names that file versions.tf.

    Fix Rename examples/regional-nat/version.tf to examples/regional-nat/versions.tf, matching templates/example-versions.tf.template.

  • LOWexamples.docs-incomplete1

    examples/regional-nat/README.md - The new example README has no terraform-docs markers; its hand-written Requirements table states aws >= 6.24.0, below the module's floor.

    Fix In examples/regional-nat/README.md, replace the Requirements section with <!-- BEGIN_TF_DOCS -->/<!-- END_TF_DOCS --> markers.

From the posted review of terraform-aws-vpc#1270, against the terraform-aws-modules profile. Finding text is quoted from the review; some fixes are shortened.

Comment

Writes: one verdict, computed by fixed rules from the findings and the pull request's checks and threads. It posts nothing until you confirm.

You get one verdict, in plain words.

Changes requested before merge.

Two HIGH findings are open. Any HIGH finding blocks merge (ladder rule 2).

Behaviour evidence: 56 changes classified: 0 verified, 3 covered by a plan, 0 inert, 0 refuted, 53 not verified; 0 claims: 0 verified, 0 refuted, 0 not verified.

The verdict names the rule that produced it. When a check could not run, the verdict is Inconclusive, unless a CRITICAL or HIGH finding, a failing check, a merge conflict or a review that requests changes already decides it, as the two HIGH findings do here. Unknown is never a pass: the 53 changes not verified and the two examples that were not planned are listed under "Checks that could not run".

Round 2

Reads: the new head. Runs the same rules again and compares with the last list.

You fix what it found, push, and ask for the review again.

  • HIGHstructure.sibling-count-mismatchfixed
  • HIGHprofile.region-not-allowedfixed
  • MEDIUMstructure.sibling-block-asymmetryfixed
  • MEDIUMstructure.validation-missingfixed
  • MEDIUMstructure.version-floor-understatedfixed
  • MEDIUMcoverage.optional-argument-unexposedfixed
  • MEDIUMcoverage.attribute-not-outputfixed
  • MEDIUMexamples.feature-undemonstratedfixed
  • MEDIUMstructure.comment-unsourcedfixed
  • MEDIUMexamples.versions-missingfixed
  • LOWexamples.docs-incompleteopen

Good to merge, with nits.

Every HIGH and MEDIUM finding fixed, one LOW left open. Merging is still your decision.

Illustrative second round, built from the same rule IDs. To reach this verdict every HIGH and MEDIUM finding must be fixed and every check must have run; a LOW finding may stay open.

What it checks

A profile is the written-down set of rules a review checks against, each with an ID and a severity. One profile ships: terraform-aws-modules, the rules its maintainer uses to review pull requests for terraform-aws-modules. Every finding cites a rule. Some of them:

RuleChecksKind
compat.*Breaking changes held for the next major release. 11 rules, such as compat.variable-removed, compat.resource-address-changed and compat.default-changedCompatibility
coverage.attribute-not-outputAttributes a new resource exports surface as outputsProvider schema
coverage.optional-argument-unexposedOptional arguments of a new resource are inputs, or their omission is documentedProvider schema
examples.feature-undemonstratedEach feature the change adds appears in an exampleExamples
structure.comment-unsourcedComments that assert service behavior cite a source and a dateStructure
profile.region-not-allowedExamples use eu-west-1, or us-east-1 for a second regionHouse rule
scope.toolingNo new scanner, linter or cost tool configuration in the module repositoryHouse rule
scope.testsNo Go tests; native tests use terraform test with command = planHouse rule
scope.layoutNew files sit where the profile's layout allowsHouse rule

House rules are how terraform-aws-modules keeps its modules: what a module may contain, its scanners, tests, regions and layout. Scanner config belongs outside the module repository; keep running your scanners in your own CI.

A module's docs/SCOPE.md, read from the base branch, can only allow or deny resource types and providers. Scanner, test and layout rules have no per-module exception.

The profile is the maintainer's review practice, written down. It is not a policy document published by the terraform-aws-modules organization, and it changes as the review practice does. The model applies the rules; it does not invent them.

No account or hosted service. The profile is plain files in the modulestf repository; you can read every rule.

Read the terraform-aws-modules profile on GitHub

Not a generic code reviewer

A general-purpose review tool brings its own idea of good code. modulestf applies written rules, one by one.

QuestionGeneral-purpose review tool (typical)modulestf
Where does the standard come from?The model's own sense of qualityRules in the terraform-aws-modules profile, each with an ID
Does the addition belong at all?Reviews what is in the diffChecks each addition: it belongs, it is out of scope, or a maintainer decides
A rule it could not checkSilence reads as fineListed as not evaluated; it never counts as a pass
What do you get?Free-form commentsOne verdict, rule ID, location, a fix, evidence
Static toolsKeep running tflint, checkov, trivy and terraform-docs in your CI. modulestf does not run in CI; it runs in your agent and compares base and head.

The middle column describes the category, not a named product.

What modulestf will never do

Install

Local branch review

  • git
  • HashiCorp's Terraform MCP server, added to your agent under the name terraform. Without it, rules that need a provider fact are not run.

Pull request review (adds)

  • gh logged in as a person
  • jq, perl and terraform
  • mtf-facts, from Homebrew or go install. Without it a pull request review ends Inconclusive.
  • sandbox-exec (macOS) or bwrap (Linux)
# Claude Code
/plugin marketplace add antonbabenko/agent-plugins
/plugin install modulestf@antonbabenko

# Codex: add the marketplace, then install modulestf from /plugins
codex plugin marketplace add antonbabenko/agent-plugins

# Pull request review
brew install modulestf/tap/mtf-facts

The plugin also installs change-scope, which the three skills call. The README has the details and what each task needs.

Status

A wrong or noisy finding, a rule that should change, a missing rule, or a run that failed: open an issue with the rule ID and your reason, or with what you asked for and what you got. Watch the repository's releases to know when something new exists. Nothing else is collected.

modulestf is created and maintained by Anton Babenko, maintainer of terraform-aws-modules and terraform-skill, and creator of compliance.tf. terraform-skill helps agents write modules; modulestf reviews what they write.

FAQ

Does it block merges?
Only if you post its review as a request for changes and your branch protection requires reviews. It never merges.
Does it need my cloud credentials?
No. Validation runs without credentials. A plan pass runs only when you name an AWS profile and a read-only review role.
Who pays for the model?
You do: the skills run in the coding agent you already use.
Can it run in CI?
No. It runs in your agent as you; under a bot or CI identity it only renders and never posts.