# Troubleshooting

Match a Rootform symptom to its exact diagnostic, evidence, and next action.

Keep the command, its exit status, standard error, and saved Form together.
Exit meanings depend on the command. [Outputs and exit status](https://docs.rootform.dev/reference/outputs/)
gives each command's contract.

## Inputs and documents

### The command set differs from this documentation

Check which executable your shell runs and its version:

<!-- docs-check:troubleshooting-version -->
```sh
command -v rootform
rootform version
```

If help still lists an unexpected command or flag, fix `PATH` or use the intended exact release in automation. Do not reinterpret a diagnostic from a different binary.

### A directory or saved plan is refused as input

`run` needs plan JSON, state JSON, or a saved Form. A configuration directory returns status `2`, names the directory in the error headline, and gives export commands (`Code: DIRECTORY_INPUT`). A binary saved plan returns status `3` with the same kind of guidance:

<!-- docs-check:troubleshooting-binary-input -->
```sh
rootform run plan.tfplan --no-serve
```

<!-- docs-output:troubleshooting-binary-input -->
```text title="Standard error"
Error: this input looks like a saved plan

A saved plan is read with --plan-file, next to the plan JSON exported from it.

Try:
  terraform show -json plan.tfplan > plan.json
  rootform run plan.json --plan-file plan.tfplan

Code: INPUT_UNRECOGNIZED
```

Run the two suggested commands: the first exports the JSON that Rootform analyzes, the second pairs it with the saved plan it came from. OpenTofu users run the export with `tofu`. The saved plan and JSON can contain cleartext secrets; keep them out of Git and public artifacts. [Plan inputs](https://docs.rootform.dev/inputs/plans/) gives the complete procedure.

### Malformed or unsupported JSON is refused

Malformed JSON reports `Error: this input is not valid JSON`; a JSON object without plan, state, or saved Form fields reports `Error: this JSON is not a plan, state or saved Form`; a text file such as `main.tf` reports `Error: this input is not JSON`. Each carries `Code: INPUT_UNRECOGNIZED`. A state export from a working directory with no state reports the missing recorded state with the same code, followed by plan commands to run instead. The input kind is detected from content, not extension. A raw `terraform.tfstate` file and a `terraform plan -json` event stream are refused with the export commands to use instead. Re-export with `terraform show -json`, then confirm the file is complete before retrying. A plan with `errored: true` reports `Error: the plan JSON records a failed plan` with `Code: PLAN_ERRORED`; resolve the planning failure first rather than treating the result as an empty architecture.

### Saved plan pairing fails

The saved plan named by `--plan-file` must be the one used to make that exact JSON export. A mismatched pair records `PLAN_PAIR_MISMATCH`. Without `--require-enrichment`, analysis continues with plan JSON alone and the summary's `Enrichment` line says `Saved plan refused (PLAN_PAIR_MISMATCH); the plan JSON was analyzed alone`. With the requirement, it exits `3`:

<!-- docs-check:troubleshooting-pair -->
```sh
rootform run plan.json --plan-file other.tfplan \
  --require-enrichment --no-serve
```

<!-- docs-output:troubleshooting-pair -->
```text title="Standard error excerpt"
Error: saved plan does not match the plan JSON

--require-enrichment needs a verified pairing.

Re-export the JSON from the same saved plan:
  terraform show -json plan.tfplan > plan.json

Code: PLAN_PAIR_MISMATCH
```

Re-export JSON from the same saved plan, then retry. An unreadable or encrypted saved plan reports `PLAN_FILE_UNREADABLE`; use plan-only analysis if direct values suffice, or supply a readable matching pair. Never pair an arbitrary working directory with an old plan to establish references.

## Interpretation and evidence

### An instance has no Rule

The analysis can succeed while a resource remains uninterpreted. When the only instance of a plan matches no Rule, the run summary's `Limited interpretation` section says `The only instance matched no Rule in the selected Dialects.` and counts its resource type under `Resource type  Instances`. `rootform explain instance <address> --input analysis.json` then prints `Interpretation  none: no selected Dialect has a Rule for this type`. Rootform still records a Representation for that instance. Confirm the active Dialects with `rootform list dialects --format wide`; add or author a reviewed Dialect only if its architectural meaning is needed. If the type is outside reviewed Rule coverage, [report a semantic gap](https://docs.rootform.dev/contributing/#report-a-semantic-gap).

### A resource has no card in the current scene

The Explorer shows one scene at a time, so a represented instance may have no card where you are looking. **Search** covers the whole architecture: search by name or type, then select the result to open its containing context. A secondary resource can also appear in the Inspector of the object it contributes to. `rootform explain instance <address> --input analysis.json` confirms the instance in the saved Form. A missing card alone is not a missing resource; see [Reveal a secondary resource](https://docs.rootform.dev/guides/explore-architecture/#reveal-a-secondary-resource).

### An expected relation is missing

Find the instance in the Explorer or run `rootform explain instance <address> --input analysis.json`. Inspect each closure's Rule, `via` path, result, reason, and candidate counts. A Terraform dependency alone does not establish an architectural relation. The active Dialect must emit it from values or a verified direct traversal. `EMISSION_PATH_UNDEFINED` means the path is absent from that instance's provider schema; `VIA_VALUE_SHAPE` means its shape cannot be read as endpoint identity. Correct the Dialect path or gather better input; do not add a guessed edge.

### Unknown or sensitive evidence leaves a closure indeterminate

For `unknown_until_apply`, first check which stage the Form contains and which
stage the Policy evaluates. A paired saved plan can establish some direct
Planned-stage references even when their values are unknown. Confirm that the
saved plan matches the JSON export and that the expression uses a supported
reference form; see [Traversals and scope](https://docs.rootform.dev/language/reference/traversals/#value-and-identity-evidence).
The enrichment status must be `verified` for Rootform to use that evidence.
Transformed expressions and historical stages do not gain identity from the
pair. If no supported reference settles the endpoint, keep the closure
indeterminate until later plan or state evidence becomes available through
the normal workflow. Do not apply infrastructure solely to remove uncertainty.

Sensitive values remain masked and are never printed. A verified direct
reference may establish endpoint identity without exposing its sensitive
value. The [closure model](https://docs.rootform.dev/concepts/forms/#stages-and-facts) explains why
`absent` differs from `indeterminate`.

### Provider configuration or historical evidence is unavailable

A `provider.<path>` emission can show `indeterminate (unavailable)` when the plan has no traversal from a paired saved plan, the expression is literal or transformed, or the selected stage is historical. State JSON contains no provider configuration. For a planned-stage question, supply the paired saved plan and inspect whether the provider expression directly names a resource. Rootform never reads literal provider configuration values just to force a relation.

### Duplicate, ambiguous, or conflicting identities

`DUPLICATE_IDENTITY` means more than one eligible instance has the same matched identity. `ambiguous_unknown` means at least one candidate could match but its identity is unknown, sensitive, or unavailable. `EVIDENCE_CONFLICT` means a verified traversal and a known value point at different endpoints; the closure reason is `reference_ambiguous`. Inspect the candidate counts and provider configuration scope, then correct the input or Rule identity declaration. None of these is a resolved relation.

### An external endpoint is denied

A known value with no in-scope match yields `external_denied` unless the emission declares `external = "allow"`. Even with that declaration, an eligible unknown in-scope candidate prevents an external endpoint. Confirm whether the target is genuinely outside this plan's inventory before changing a Dialect. An external endpoint does not verify the remote object.

## Project selection and registries

### Locked project selection fails

`--locked` requires `rootform.lock` directly under the selected project root. Without it, `run` exits `3` and reports `Error: rootform.lock is required by --locked` with `Code: SEMANTIC_SELECTION`:

<!-- docs-check:troubleshooting-locked -->
```sh
rootform run plan.json --locked --no-serve
```

<!-- docs-output:troubleshooting-locked -->
```text title="Standard error excerpt"
Error: rootform.lock is required by --locked

Code: SEMANTIC_SELECTION
```

For embedded-only work, omit `--locked`. For an exact external selection, add content from the project root and commit the lock. If a selected local Dialect changed, the binary reports `selected Dialect network-review differs from rootform.lock`; use an override while editing, then `rootform update dialect network-review` to record a reviewed change. `init` cannot adopt source drift.

An invalid lock reports `Error: rootform.lock is invalid` and explains the expected structure. `list` and `show` carry `Code: SELECTION_LOCK_INVALID` and exit `3`; inspect the lock as strict JSON with known fields and no duplicate keys.

### Selected content is missing

A locked `run` exits `3` when a selected Dialect cannot be loaded; its headline names the unavailable Dialect and its code is `SEMANTIC_SELECTION`. `rootform check` exits `3` when a selected Policy Pack cannot be loaded; its headline names the unavailable Pack and its code is `SELECTION_POLICY_PACK_MISSING`. `rootform list` fails for the same reason, so read the entries in `rootform.lock` instead. For selected OCI content, run `rootform init --locked --no-input` from the project root to install the exact recorded digests; add `--offline` only when those bytes are already on this machine. `init` cannot choose another version or change the lock. A local source must be restored at its recorded path: `init` reports `the local source is unavailable` and cannot recreate it. When the project has a vendor tree, repair that tree instead, as described below.

### Installed content does not match rootform.lock

The Rootform home holds the version named by the lock, but its bytes no longer match the recorded digest. `init` does not overwrite an installed version. Delete the damaged copy with the family and `name@version` from the diagnostic, for example `rootform uninstall dialects payments@0.1.0` or `rootform uninstall policy-packs baseline@0.1.0`, then run `rootform init --locked --no-input` to install the pinned bytes again. The lock does not change. [External content storage](https://docs.rootform.dev/reference/storage/) explains where installed content lives.

### Vendored content is incomplete or altered

A locked run refuses a vendor family that no longer matches `rootform.lock` and exits `3`. While the family directory exists, Rootform does not fall back to a local source or registry. The error headline names the problem (`Code: SEMANTIC_SELECTION`):

```text title="Possible error headlines"
Error: selected Dialect network-review differs from rootform.lock
Error: vendored Dialect network-review is missing or invalid
Error: .rootform/dialects does not exactly match rootform.lock
```

The first line means that a vendored Dialect's source no longer has its locked content digest. The second means that its vendor metadata is missing or invalid. The third means that the family has a missing, extra, or unreadable entry; a `.rootform/dialects` path that is not a directory reports `is present but does not match rootform.lock` instead. Vendored Policy Packs are checked when `rootform check` loads them and report the same problems, for example `selected Policy Pack baseline differs from rootform.lock`. Repair only the affected family from verified local or installed bytes:

<!-- docs-check:troubleshooting-vendor-repair -->
```sh
rootform vendor dialects --offline
```

`vendor` preserves the lock. If verified bytes are unavailable on this machine, prepare them in a connected environment and transfer the complete vendor family. [External content storage](https://docs.rootform.dev/reference/storage/) explains precedence.

### An offline add or update refuses a tag

A tag needs a registry lookup. With `--offline` or `ROOTFORM_OFFLINE=1`, `add` and `update` stop with status `2` and `"<reference>" is a tag, which cannot be resolved offline`. The hint names the setting that enabled offline mode: rerun without `--offline`, or unset `ROOTFORM_OFFLINE` when the environment set it. Offline, use a reviewed local source directory or an exact digest reference already installed on this machine instead. [Add external content](https://docs.rootform.dev/guides/external-content/) shows both forms.

### A Dialect owner collides with an embedded owner

An external Dialect named like an embedded one, such as `aws`, never replaces it by accident. `add` stops with `aws is an embedded Dialect; adding another aws replaces it` and suggests `--replace`. Rerun with `rootform add dialects <source> --replace` only when replacement is intended, after reviewing which embedded Rules the project loses. The reserved `rf` vocabulary cannot be replaced. See [Replace or exclude an embedded Dialect](https://docs.rootform.dev/guides/external-content/#replace-or-exclude-an-embedded-dialect).

### rootform.lock.new blocks a change

`add`, `remove`, `update`, and `vendor` prepare the new lock in `rootform.lock.new`. If one was interrupted, the next of these commands stops with `rootform.lock.new exists: another rootform add, remove, update, or vendor is running, or one was interrupted`. Analysis still reads the committed lock. Confirm that no Rootform command is running, delete the leftover file, then retry. If vendored content no longer matches the lock, repair the affected family with `rootform vendor`. [Who writes this file](https://github.com/rootform-dev/rootform/blob/dev/contracts/rootform-lock.md#who-writes-this-file) lists every writer.

### Registry access or a private CA fails

Only explicit acquisition or publication crosses that boundary; normal `run` does not fetch packages. Check the exact OCI reference and digest in the lock, the registry host, `DOCKER_CONFIG`, credential-helper availability, and `SSL_CERT_FILE` for a private CA. Do not print credentials while diagnosing. An offline tag lookup cannot discover a new digest: use reviewed local source or an exact digest already installed, or perform selection while connected. [Registry compatibility](https://docs.rootform.dev/integrations/registry-compatibility/) and the [OCI mirror](https://docs.rootform.dev/offline-security/#oci-mirror) procedure give the details.

## Policy checks and comparisons

### A Policy is unavailable or has no decision

Selecting a Policy without a Policy Pack reports `Error: no Policy Pack is selected` (`Code: POLICY_UNAVAILABLE`) and suggests how to add one. Select a reviewed Policy Pack, then run `rootform check`. A Policy with no targets contributes no decision. When no selected Policy has a target, the summary reports `Evaluations    0` and `Verdict        NO DECISION`, then exits `3`. A passing Policy does not offset another Policy with no target; the overall status is NO DECISION unless a violation or indeterminate result takes priority. Inspect the target with `rootform show policy <identifier>` and compare it with the Form's interpreted Concepts and Rules. Zero evaluations are not compliance. [Target scope is exact](https://docs.rootform.dev/concepts/policies/#target-scope-is-exact) explains matching, and [Understand Policy outcomes](https://docs.rootform.dev/guides/check-architecture/) shows target coverage.

### A Policy is indeterminate or violated

A violation exits `1`; an indeterminate outcome exits `3`. SARIF names each violated or indeterminate target; `policy.md` shows at most ten per outcome unless written with `--details`. [Inspect the proof](https://docs.rootform.dev/guides/check-architecture/#inspect-the-proof) shows how `rootform explain policy <policy> --result <file>` reads the recorded outcome and `rootform explain instance <address> --input <input>` traces its facts and closures. Unknown, sensitive, and unverified absence cannot prove a negative assertion. Resolve input evidence or correct the Policy; do not remove a diagnostic to make the job pass. [Policy outcomes](https://docs.rootform.dev/concepts/policies/#evidence-produces-three-outcomes) explains the three results.

### A comparison appears empty or indeterminate

Check both selected stages and the comparison's `comparable`, `problems`, and `indeterminate` entries. A plan defaults to `planned`; a state JSON has only `recorded`. `run --diff` compares separate inputs and is not drift. Different Dialect selections, withheld external identity, or indeterminate closures may prevent a no-change claim. Use comparable stages and [read the comparison](https://docs.rootform.dev/guides/compare-architectures/). [Indeterminate preserves uncertainty](https://docs.rootform.dev/concepts/comparisons/#indeterminate-preserves-uncertainty) explains why an unsettled closure never counts as no change.

### A comparison input or saved Form is refused

Each `--diff` operand must be accepted on its own. A refused input stops the run with status `3`; Rootform never treats it as an empty side. A comparison Form reopens with `rootform run comparison.json` and is refused as a `--diff` operand with status `2`. The [refusals above](#malformed-or-unsupported-json-is-refused) apply to both inputs. A saved Form written in another format is refused with `Error: document format "9" is not supported; this build reads format 1` (`Code: DOCUMENT_FORMAT_UNSUPPORTED`); save it again from its original input with the current binary. For any other rejected Form, run `rootform validate form <file>`: it names each problem and exits `1`. `validate form` reads only Forms; given a plan JSON, it exits `3` and suggests saving one first. [Valid partial Form differs from invalid Form](https://docs.rootform.dev/concepts/forms/#valid-partial-form-differs-from-invalid-form) separates missing knowledge from an invalid file.

## Explorer and output files

### A port is occupied or the browser does not open

An occupied port returns status `4` with `Error: port <number> is unavailable` (`Code: SERVER_FAILED`) and suggests `--port 0` or `--no-serve`. Choose an available local port with `--no-browser --port 0`, then open the printed loopback address yourself. `--no-serve` skips the interface and writes requested files. A browser launch failure does not require rerunning analysis. Stop a foreground server with `Ctrl+C`. Rootform binds loopback only; use a self-contained HTML export for remote review instead of exposing the local server.

### An output path collides or cannot be written

If an output resolves to an input, the command exits `2` with `rootform: output "plan.json" resolves to an input`. Choose a different path, including when a link points to the input. An unwritable target exits `4`; already written files may remain after a later output fails. Check standard error, the command's exit status, and directory ownership. In a container, UID/GID `65532:65532` must be able to write the report mount; [container mounts](https://docs.rootform.dev/integrations/oci-image/#run-against-a-project) explain the split.

If the symptom remains, [report a synthetic reproduction](https://docs.rootform.dev/contributing/#report-a-semantic-gap) without credentials, raw plans, state, or private infrastructure.
