Free Terraform review — in your pull requests, and in your terminal.
One of the two lines on the right deletes the database. The other only looks
like it prevents that. TF Pre-Deploy Firewall reads your Terraform against the
provider's real schema and reports on the line that caused it — no
terraform init, no plan, no state file, no credentials, no account.
It is one scanner and one engine, in two shapes: a GitHub Action that comments on the pull request, and a command-line scanner that runs on your machine, in a pre-commit hook, or in any CI — GitLab included. Unlimited repositories, unlimited scans, forever.
An Action, or a command. Two minutes either way.
Pick whichever you already have — you can use both. Same engine, same 32 rules, same findings; only where they land differs. The free scan needs no account and sends no code to our service. AI auto-fix, on Starter and Growth, is a separate opt-in feature.
with: block —
the defaults are the free scan. fetch-depth: 0 matters: without
it the runner has no base branch to diff against.
What you get for nothing: 32 rules across 80 AWS and Azure resource types, PR comments with the fix inline, SARIF for GitHub Code Scanning, your own custom rules, and a baseline file so an existing repository doesn't drown you on day one. Unlimited repositories, unlimited scans, no seat count, no telemetry.
Paste your own Terraform. No account.
The same engine and the same rule pack that run in a pull request — not a mockup. Nothing is stored.
Up to 64 KB. A snippet has no previous revision, so rules that compare against one — including destroy-on-rename — only run on a real pull request.
Paste something and press Scan it.
Two commands, from the directory you want to look at. No account, no
CI, no terraform init.
pip3 install "tf-predeploy-firewall @ git+https://github.com/foadtalsi/tf-predeploy-firewall@v1"
tf-predeploy-firewall --full-repo-scan
If pip answers
error: externally-managed-environment, your Python is
one of the managed ones (Debian, or a recent Homebrew). Install it
with pipx install and the same quoted spec instead —
brew install pipx or sudo apt install pipx
first. It needs Python 3.11 or newer, which the system Python on
macOS is not: brew install python, or use pipx.
pip install "tf-predeploy-firewall @ git+https://github.com/foadtalsi/tf-predeploy-firewall@v1"
tf-predeploy-firewall --full-repo-scan
If pip is not on your PATH, use
py -m pip install instead.
MIT-licensed, and it makes no network call without a key. It exits
non-zero when something reaches your blocking threshold, so the same
command works as a pre-commit hook or a CI step.
Reading your cloud account to grade findings on what already exists is
opt-in and needs the [aws] extra — the tool prints that
command when you ask for it.
Four findings, on four lines, before anyone ran anything.
This is the scanner's own output for the diff above — not a mockup of one. Two of the four are about lines the diff never touched.
| Severity | Location | Finding |
|---|---|---|
| critical | rds.tf:5 |
ForceNew change on stateful resource
db_name changed from "appdb" to "app_production" — this attribute is ForceNew on aws_db_instance and will destroy + recreate the resource on apply.
|
| critical | rds.tf:8 |
Hardcoded credential
password resolves to a hardcoded string literal (via var.db_password), not a secret reference — credentials must not be committed in plain text.
|
| high | rds.tf:10 |
Unknown / hallucinated attribute
attribute "enable_deletion_protection" is not a known argument of aws_db_instance — likely hallucinated or deprecated; verify against the provider docs.
|
| medium | rds.tf:1 |
Missing prevent_destroy
aws_db_instance is a stateful resource with no lifecycle { prevent_destroy = true } guard.
|
The guard that isn't one
The argument is deletion_protection. enable_deletion_protection
does not exist on aws_db_instance — the provider declares 95 arguments and
that is not one of them.
It is a good hallucination: the right concept, the right value, a name that reads like a dozen other AWS arguments. Approving this PR feels like approving a safety improvement.
The rename that drops the instance
db_name is ForceNew on aws_db_instance. Changing it does not
rename anything: Terraform destroys the instance and creates a new, empty one.
Thirteen of that resource's arguments behave this way and engine_version
— the one people assume — is not among them. This is knowledge about a specific
provider version, which is why the scanner reads it from the provider rather than
from a list someone maintained by hand.
The check that would catch this needs the credentials you don't want to give a generated branch.
Terraform can answer some of these questions itself. It is worth being exact about which ones, and what each answer costs.
| Question | terraform validate |
terraform plan |
This scanner |
|---|---|---|---|
Is enable_deletion_protection a real argument? |
yes — after init |
yes | yes |
Does changing db_name destroy the instance? |
no | yes | yes |
Is there a credential in terraform.tfvars? |
no | no | yes |
| Needs provider downloads | yes | yes | no |
| Requires cloud credentials and state | no | yes | no |
| Where the answer appears | build log | build log | on the line, in the PR |
Only plan knows about the destroy — and running plan means handing
real cloud credentials to a branch nobody has read yet. That is the gap: the pull request
most worth planning is the one least safe to plan. This reads the same provider schema
statically, from one package, with nothing to authenticate to.
If your pipeline already runs plan somewhere safe, pass the JSON with
plan-json and three more checks turn on: a confirmed replace, a sensitive
attribute drifting that this PR never touched, and a plan that destroys more resources
than your threshold allows.
And if you want it, one more option. force_destroy = true on a bucket this
pull request invents and force_destroy = true on the bucket holding your
backups are the same eleven characters — nothing in the repository tells them apart, so
the rule has to score both the same. Give the scan a read-only role and
it asks your account which one this is, and says so in the finding. It is the only thing
it uses the access for.
A secret moved out of main.tf has to land somewhere.
Scanning resource blocks and stopping there would miss most of a mature
repository. These are read too, and findings say where a value actually lives.
terraform.tfvars, *.auto.tfvars and their JSON forms, walking nested objects and lists. Only files git actually tracks — a properly gitignored tfvars is never read, which is the correct answer, because the finding is this secret is in the repository.password = var.db_password whose variable declares default = "changeme" is a hardcoded credential one indirection away, and the more common of the two. Resolution is directory-scoped the way Terraform scopes it.inputs and remote_state.config maps, recursively. Values that reference a dependency output can't be evaluated statically and are skipped rather than guessed at.version, or a ?ref=main anyone can move. Generated Terraform almost never pins one, and an unpinned dependency means the plan that was reviewed isn't necessarily the plan that runs.The scanner is free. Plans add coverage, history and help fixing findings.
Every rule, the HCL parsing, the PR comment, the SARIF output, your own custom rules — MIT, in the repository, no account. What those rules run against is a generated knowledge base of a provider's resource types: every valid argument at every nesting depth, which of them force a destroy, and which types are stateful enough to need a guard.
| Resource types | Embedded | With a plan |
|---|---|---|
| AWS 6.65.0 | 39 | 1,724 |
| Azure 5.6.0 | 41 | 1,105 |
| Carrying ForceNew data | 76 | 2,550 |
| Delivery | embedded in the scanner | cached 24h on the runner |
The embedded packs are not a crippled demo. They cover the types most repositories use most, they detect real problems on real code, and they will never be taken away. They are also not enough for a real estate of Terraform — which is the honest reason to charge for the rest.
Why it is generated, not curated
This data used to be hand-written, and was wrong in both directions. It listed 29
arguments for aws_instance; the provider declares 71 — so
launch_template, cpu_options and hibernation were
all reported as hallucinated at severity high. That is a blocked pull
request on valid Terraform.
It also claimed identifier was ForceNew on aws_db_instance.
It isn't — the provider renames in place. A confident, wrong warning about destroying
a production database.
Packs are now generated from terraform providers schema -json and, for the
ForceNew flags the schema doesn't expose, from the provider's own source. Both packs
come from the same generator and the same provider release, so they never disagree
about a type they share — the paid one simply covers more. A type in neither is skipped
rather than guessed at, so wider coverage only ever adds findings.
Resources from other providers still get every rule that reads values rather than a schema — credentials, entropy, open CIDRs, your own rules. The scan says so on stderr rather than leaving you to infer it.
Pointed at a repo that already exists, it will find a lot. That is usually where these tools die.
Every finding may be true and collectively they're useless: the only available response is to lower the threshold until it stops complaining, which is the same as uninstalling it.
A baseline records what was already there. Run it once, commit the file, and pass it on every scan. Baselined findings still appear in the PR comment, in their own collapsed section — the debt stays visible — but they don't block a merge. Anything not in the file does.
The bleeding stops immediately and the backlog gets paid down on its own schedule, which is the only sequence that works on a codebase nobody has time to fix at once.
Matching is on rule + category + resource + file, deliberately not on line number: a baseline that broke every time someone added a line above it would be abandoned in a week.
Every check is data, not code.
All 32 built-in rules across 12 categories — what each looks for, its severity, its wording, the documentation it links to — are declarations in a YAML pack embedded in the scanner. Print it, change it, run against your version. No fork, no release.
A pattern that misfires on your repository is something you can correct today rather than file an issue about.
Add extends: builtin and your pack layers over the built-ins by rule id —
override one to change its severity, disabled: true to switch one off, a
new id to add your own. Without it, --rules replaces the built-ins outright,
and the scan says so loudly, because running on fewer rules than you think looks exactly
like a clean repository.
A proposed correction. You decide what gets applied.
Turn findings into Terraform corrections with AI. Review the diff in your pull request or terminal, then accept the changes you want. Available with an active Starter subscription (25 requests a month) or Growth subscription (1,000 requests a month), one request per file.
In your GitHub pull request
Enable auto-fix in the scanner step and add your Starter or Growth API key as a repository secret. Corrections appear as review suggestions. Read each one, then click Commit suggestion to accept it. Proposals outside the PR diff appear in the summary for manual application.
In your terminal
Set TFPDF_LICENSE_KEY to your Starter or Growth API key and run the scan with
--autofix. The CLI shows a diff and asks before writing each file.
Press Enter to decline. Without an interactive terminal, it only previews changes.
Auto-fix is off by default. Enabling it sends affected .tf files and
their findings to our service, which uses a hosted AI model to generate proposals.
After accepting a correction, validate your Terraform and rerun the scan.
A proposal never clears a finding or changes the current scan verdict on its own.
Free is the product. The plans are for when it stops being enough.
Most repositories never need a plan, and we would rather you find that out than be told it. What you pay for is volume and reach — more provider types, more repositories, more people, and the governance features a team eventually asks for. The scanner itself is never limited, never phones home unless you give it a key, and never fails your build because of us.
- The whole scanner, MIT-licensed — GitHub Action and CLI
- 32 rules across 80 AWS + Azure resource types
- PR comments, rule-based fix suggestions, SARIF output
- Your own custom rules, and a baseline for existing repos
- No credentials, no telemetry, no seat count
Nothing below takes this away.
- All 2,829 AWS + Azure types, with ForceNew data for 2,550 of them
- Inline PR comments and rule-based fix suggestions
- Per-finding waivers with a written justification and optional expiry
- 30 days of scan history, with its findings sortable by date, scan author and severity
- AI auto-fix, 25 requests a month, with manual acceptance
- Email support
- Everything in Starter
- AI auto-fix, 1,000 requests a month instead of Starter's 25
- 1 year of history, CSV and PDF export
- Findings trend chart
- Priority support
- Growth collaboration features; AI auto-fix is not included
- Unlimited history retention
- SSO / SAML, connected to your identity provider during onboarding
- Organisation audit log
- Dedicated onboarding and an SLA
Want to see the paid side first? Two weeks on the full 2,829-type pack, unlimited scans, 1 seat, 7 days of history, no card. When the two weeks end nothing breaks — you drop back to the free scan, no check turns red, and you pick a tier when you're ready.
Before you ask
No, and they're complementary. Those check a resource's configuration against known misconfiguration rules — unencrypted storage, a public bucket, an open security group. Those rules are about the state a resource ends up in.
This checks the change: whether an argument exists in the provider you're actually pinned to, whether editing it destroys the resource, whether the value resolves to a literal secret, whether a guard that used to be there still is. A repository can pass every tfsec rule and still contain the diff at the top of this page. Most teams run both.
No, and it's structural rather than a promise. The paid pack is fetched once, cached on the runner for 24 hours, then revalidated with an ETag. If the control plane is slow, down or unreachable, the scan runs on the cached pack; if there's no cache yet, it falls back to the pack embedded in the scanner and says so on stderr.
The same applies to usage reporting, which is what enforces your plan's limits: if it can't be reached, the scan still completes. Point TFPDF_CACHE_DIR at a directory your CI already restores to reuse cached packs. If the AI model is unavailable, auto-fix reports the error and the scan keeps its original verdict.
Without a licence key, nothing at all — no account, no network call outside GitHub, no telemetry.
With a licence key: the repository name, scan author, verdict and finding details — category, severity, resource, file path, line and message — are sent to our service for scan history and the dashboard. Finding messages may contain values from your code.
AI auto-fix, on Starter and Growth, is a separate opt-in: enabling --autofix in the CLI or autofix: "true" in GitHub also sends affected .tf files and their findings to our service, which passes them to a hosted AI model in the United States. The model returns proposed code for you to review.
Then most of this still applies. A ForceNew attribute destroys your database whoever typed it, a credential in terraform.tfvars is committed either way, and an unpinned module drifts on its own schedule.
What changed with generated code is volume and confidence — more diffs, arriving faster, written in a register that reads reviewed. The failure modes were always there; there are just more of them now, and they look better.
On a mature estate, loud on the first run — which is why the baseline exists. Commit one and only new findings block; the pre-existing ones stay visible in a collapsed section instead of being deleted or ignored.
Beyond that: values that can't be resolved statically are skipped rather than guessed at, resource types with no pack are skipped rather than assumed, and every rule's severity and wording is a YAML declaration you can override without waiting for a release.
The provider knowledge base the rules run against, regenerated weekly from the newest provider release: 1,724 AWS and 1,105 Azure resource types with their full argument surface and ForceNew flags, against the 39 and 41 embedded in the free scanner.
The free packs are not a crippled demo — they cover the types most repositories use most and detect real problems on real code. They are also not enough for a large estate of Terraform, which is the honest reason to charge for the rest. Plus the operational half: the dashboard, waivers, scan and findings history, SSO. Growth also includes opt-in AI corrections, reviewed and accepted in GitHub or the CLI.
Which means the answer for most people is nothing, for a long time. A plan is what you buy when the scan starts skipping types you actually use, or when enough people need to see the results in one place that a pull-request comment stops being where the conversation happens. Neither is a day-one problem, and pretending otherwise is how a free tier turns into a trap.
No. It never runs terraform, never reads state, and authenticates to nothing — a scanner reading the files in your repository against a schema it already has. That is the default and it is the whole product.
There are two things you can hand it if you want more, and both are off until you switch them on.
A plan. If you already run terraform plan somewhere safe, pass the JSON for three extra checks. You run the plan; this never does.
A read-only role. One question the repository cannot answer is whether the resource a finding is about already exists. force_destroy = true is written identically on a bucket this PR invents and on the one holding your backups; skip_final_snapshot = true reads the same on a database this PR creates and on the one in production. Without that answer the rule has to score both the same — too loud for one, far too quiet for the other. Given a read-only role it asks, and writes what it saw into the finding.
What "read-only" means here is narrow and checkable, not a promise: the scan may call sts:GetCallerIdentity, s3:ListObjectsV2, rds:DescribeDBInstances and rds:DescribeDBClusters, and a guard in the code refuses every other operation before the request is built. The policy we ask for does not include s3:GetObject — it can learn that a bucket is not empty and cannot read a single object in it — and the scan never connects to a database, it only asks whether one exists. Credentials stay in your CI and never reach us. Refuse the permission, let the role expire, take it away later: severities go back to what they were and nothing fails.
AWS and Azure have packs. That limit is deliberate: a pack is only worth shipping once its two hand-curated halves — which resource types lose data when destroyed, and which of their arguments force a replacement — have been thought through, and a half-considered pack produces confident wrong answers.
Resources from any other provider are still checked by every rule that reads values rather than a schema: credentials, entropy, open CIDRs, placeholder names, your own rules. What they don't get is the schema half, and the scan tells you which providers those were instead of letting you assume they passed.
No. In GitHub, you accept a review suggestion yourself. In the CLI, you review the diff and confirm before the file is written. The CLI never stages or commits changes, and refuses to overwrite a file that changed since the proposal was generated.
AI auto-fix requires an active Starter or Growth subscription: 25 requests a month on Starter, 1,000 on Growth, one request per file. It is not included in Free, the trial or Scale. The rule-based suggestions already included in the free scanner remain available.
Use a CLI or Action release that includes auto-fix. After accepting generated code, validate it and run the scan again: a model proposal is not a passing scan.
Run it on one repository and read what comes back.
Fourteen lines of YAML, no account, no card, nothing to cancel. Decide about a plan the day the free scan stops being enough — not before.
Curious about the paid side instead? , no card.