# Terraform and OpenTofu plans

Produce plan and state exports, pair a saved plan, and read evidence limits.

Produce a completed plan with your usual Terraform or OpenTofu workflow.
Rootform reads its JSON export locally. It does not plan, refresh, apply,
contact providers, or fetch missing Dialects. Run `rootform run` from the
project whose `rootform.lock` selects the Dialects you want;
`--project` chooses another project directory explicitly. Evaluate selected
Policy Packs separately with `rootform check`.

## Protect the plan files

> [!WARNING]
> Saved plans and their JSON exports can contain sensitive values in clear
> text, even when terminal output hides them. State JSON has the same risk.
> Keep these files out of Git and public artifacts. Rootform does not modify
> or delete the inputs.

## Produce the accepted JSON

Rootform accepts completed plan and state exports in JSON format `1.x`.
This is the export format, not the Terraform/OpenTofu or provider version.
Other major formats are refused. Each JSON or saved Form input is limited
to [128 MiB](https://docs.rootform.dev/reference/provider-coverage/#input-limits).

Save the plan and export that same saved plan. OpenTofu users use `tofu`
where Terraform users use `terraform`:

<!-- rootform:tabs Planning tool -->
<!-- rootform:tab Terraform -->

```sh
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
```

<!-- rootform:tab OpenTofu -->

```sh
tofu plan -out=plan.tfplan
tofu show -json plan.tfplan > plan.json
```

<!-- rootform:endtabs -->

Use `show -json` on the saved plan. `plan -json` is a planning event
stream, not the completed export Rootform accepts. Preserve the binary plan
for verification if you need expression evidence. Where state already
exists, export it with `terraform show -json > state.json`, or
`tofu show -json > state.json`. A new working directory has no state to
export.

## Pair the saved plan

Pass the plan JSON export and its saved plan:

<!-- docs-check:journey-plans-verify -->
```sh
rootform run plan.json --plan-file plan.tfplan --require-enrichment --no-serve -o analysis.json
```

```ansi title="Paired saved plan excerpt"
[1mPlan analyzed[0m
[2mInput[0m              plan.json
[2mPlan completeness[0m  Complete, as reported in the plan
[2mEnrichment[0m         Saved plan paired with this plan JSON (1 module)
[2mWrote     [0m analysis.json
```

**Enrichment** confirms that the saved plan and the JSON export agree on the
recorded tool version, timestamp, and configuration shape. These checks decide
whether the saved plan may enrich this export; they do not validate the plan in
general, and they do not prove one planning operation. The count is the number
of configuration modules read from the saved plan. **Input** names the export.
**Producer** appears only when the tool is established, as `Terraform` or
`OpenTofu`, without a version. The JSON's shared `terraform_version` field
and provider registry hosts do not identify the tool; `--producer` records
your explicit attestation. The Form retains the reported version, hints and
attestations. **Plan completeness** repeats what the plan reports about itself;
it does not say that the analysis settled every fact.

A paired saved plan lets Rootform inspect direct identity traversals in the
saved configuration, even when an endpoint's evaluated ID is unknown until
apply. It does not execute the configuration. The Form records
`enrichment.snapshot.status` as `verified` when the pair passes these checks,
`refused`, or `absent`.

If the pair is mismatched or unreadable, Rootform reports the refusal on
standard error. Without `--require-enrichment`, analysis continues from JSON
alone and records `refused`; the missing traversal evidence can leave
closures indeterminate. With `--require-enrichment`, refusal exits `3`.
Export the JSON from the saved plan you pass, rather than trying to pair a
new plan with an earlier export.

## Compare both sides of one plan

A plan's `planned` stage shows proposed instances. `refreshed` describes the
state the plan starts from; the plan does not record whether or how far refresh
ran. `recorded` is reconstructed from drift records and can be partial.
The same plan may report three comparisons: Reported drift
(`recorded` to `refreshed`), Planned changes (`refreshed` to `planned`),
and Net change (`recorded` to `planned`). A state export has only
`recorded`.

In accepted plan format 1.x, Terraform and OpenTofu omit `prior_state` exactly
when the state before the plan has no resource and no root output. Rootform
records an empty Refreshed stage and reconstructs Recorded from drift records.
Recorded can contain a resource deleted during refresh even when Refreshed is
empty. Terraform 1.12.2 and OpenTofu 1.10.7 exports are qualified; other 1.x
exports are accepted by shape.

The Recorded to Refreshed comparison is Reported drift. The separate drift
report lists the producer's drift records and their architectural consequences.
[What does this plan change?](https://docs.rootform.dev/guides/explore-architecture/#what-does-this-plan-change)
shows where the Explorer lists these views.

## Read plan comparisons correctly

`-refresh=false` prevents Terraform or OpenTofu from checking live objects.
“No drift reported in this plan” means the export contains no drift records;
it does not prove that infrastructure is unchanged.
`-target` or `-exclude` can leave instances outside the plan's scope. Terraform
may report `complete: false` for such plans, while OpenTofu may omit a completeness
field. Rootform preserves that uncertainty. A missing planned instance is
not automatically a deletion or proof of zero instances. An instance present
before the plan that it neither changes nor deletes remains in Planned with
status `carried`; its population is unverified and the report says the plan did
not evaluate it. When a fact cannot be settled on both sides, the comparison
records it as indeterminate rather than inventing an addition, removal, or no
change. See
[stages and facts](https://docs.rootform.dev/concepts/forms/#stages-and-facts) and
[comparisons and drift](https://docs.rootform.dev/concepts/forms/#comparisons-and-drift).

## Record scope and tool claims

`--plan-complete=attested` records your explicit claim that the plan covers
its intended scope when the export does not establish completeness. It is
not inferred from absent entries. `--producer terraform` or
`--producer opentofu` records the tool you used; the shared
`terraform_version` JSON field alone does not establish that identity.
`--provider-map observed=binding` makes an explicit provider registry
mapping when a Dialect binding needs it. These options record your claims,
not facts independently verified by Rootform. Do not use them to hide a targeted
or partial plan.

## Interpret unknown and sensitive evidence

A planned value can be known, unknown until apply, sensitive, or unavailable.
A verified direct traversal can identify an endpoint despite an unknown ID;
a transformed expression or dependency list alone cannot. Sensitive values
are discarded before Form output, reports, SARIF, and the Explorer.
Dialect-declared external identities may still be disclosed at their declared
tier. Read [limitations](https://docs.rootform.dev/limitations/) before relying on a missing fact
as a negative conclusion.

## Stream the export

To avoid an intermediate JSON file, pipe the saved-plan export
into Rootform. The binary saved plan still needs protection:

```sh
terraform show -json plan.tfplan | rootform run - --plan-file plan.tfplan --no-serve
```

For OpenTofu, replace `terraform` with `tofu`. The saved plan remains
local. Rootform's outputs still describe infrastructure names, structure,
and relationships, so apply your internal sharing rules.

To compare two plans, or a state Form with a later plan, follow
[Compare two Forms](https://docs.rootform.dev/guides/compare-architectures/). For plans from two
Git revisions, [Review a pull request](https://docs.rootform.dev/workflows/#choose-the-review-input)
adds isolated checkouts and cleanup.
To review a completed plan in automation, see
[Other CI/CD](https://docs.rootform.dev/integrations/ci/#review-a-completed-plan) or
[GitHub](https://docs.rootform.dev/integrations/github-actions/#quick-start).
