tf-drift is a Go-based CLI tool designed to recursively scan, detect, filter, and display configuration drift across multiple Terraform or OpenTofu workspaces/layers in a workspace (like your-infrastructure-dir).
The tool operates in four distinct phases:
.) to identify all directories containing .tf files and a backend configuration block.-env, -layer, -include, and -exclude, then optionally show a checkbox picker in interactive mode.-concurrency), and for each layer run <engine> init (if needed), <engine> plan -detailed-exitcode -lock=false, and <engine> show -json when the plan exits with code 2.resource_drift entries as EXTERNAL_DRIFT and ordinary resource_changes entries as PLANNED_CHANGE, then apply the selected -mode filter before reporting. Interactive mode presents a live Bubble Tea dashboard; non-interactive mode emits text, JSON, markdown, Slack, or SARIF output.The workflow now includes a selection step before workers start.
CLEAN, DRIFTED, PLANNED, DRIFT:<n> PLAN:<n>, ERROR, SCANNING, PENDING), and drift severity when applicable.Enter displays the classified change details or error message in an inspector pane.f cycles filters (ALL -> CHANGES -> ERRORS).TUI views and human-readable reports shorten paths under the current user’s home directory with ~. This keeps app output readable and avoids displaying /Users/<name>, /home/<name>, or /root in normal UI surfaces. Engine execution, layer selection, rules evaluation, and JSON output keep raw paths for compatibility.
The -engine flag selects which executable runs plans:
| Value | Behavior |
|---|---|
auto |
Default. Prefer tofu when present, then fall back to terraform. |
terraform |
Require the terraform executable. |
opentofu or tofu |
Require the tofu executable. |
The resolved executable is used consistently for init, plan, and show -json. Error messages must name the selected executable so CI logs explain whether Terraform or OpenTofu failed. Non-interactive runs set TF_IN_AUTOMATION=1 for child commands.
OpenTofu compatibility depends on the same plan JSON contract used by Terraform-compatible versions. The runner keeps TF_PLUGIN_CACHE_DIR behavior and continues to treat .terraform as the default data directory because OpenTofu preserves those compatibility surfaces.
OpenTofu-specific suggestions should focus on known migration failures: missing explicit provider source addresses, registry resolution changes, provider version jumps, state encryption key configuration, and saved plan sensitivity.
The -mode flag controls which classified changes are returned and therefore which changes trigger exit code 2:
| Value | Behavior |
|---|---|
both |
Default. Report external drift and pending config changes. |
drift |
Report only external infrastructure drift from plan JSON resource_drift. |
plan |
Report only pending configuration changes from plan JSON resource_changes. |
When the same resource address appears in both resource_drift and resource_changes, tf-drift reports the EXTERNAL_DRIFT entry and skips the same-address planned entry. This avoids double-counting drift remediation as an ordinary pending config change.
The -format flag supports:
| Value | Behavior |
|---|---|
text |
Human-readable stdout summary for local runs and plain CI logs. |
json |
Structured machine-readable result list with raw paths, status, counts, and classified changes. |
markdown |
GitHub-flavored table with detail sections for comments and release notes. |
slack |
Slack-oriented text with status icons and a scan summary. |
sarif |
SARIF 2.1.0 log for GitHub code scanning and CI annotation uploads. |
SARIF output treats layer errors as execution errors, external drift as tf-drift.external-drift, and pending config changes as tf-drift.planned-change. Severity maps to SARIF levels with CRITICAL and HIGH as error, MEDIUM as warning, and LOW as note.
Rules keep backward compatibility with severity_classification, where a resource type maps directly to a severity. Ordered severity_rules add predicates for more precise classification:
| Predicate | Match |
|---|---|
resource_types |
Terraform/OpenTofu resource type such as aws_iam_policy. |
attributes |
Any changed top-level attribute. |
actions |
Any managed action from the plan JSON, such as create, update, or delete. |
classifications |
EXTERNAL_DRIFT or PLANNED_CHANGE. |
layer_patterns |
Glob pattern against the slash-normalized layer path. |
address_patterns |
Glob pattern against the resource address. |
All configured predicates on a rule must match. The first matching rule wins, then severity_classification, then the default MEDIUM severity.
Large infrastructure repositories should run against explicit directory sets instead of the whole repository root when teams need faster, more reviewable output. -dir accepts direct directories, glob patterns, and brace choices, then discovery finds backend-enabled Terraform/OpenTofu config layers under each resolved root. Typical layouts:
tf-drift -dir "infra/{prod,stage}/aws" -include "network,shared-*"
tf-drift -dir "platform/accounts/*" -exclude "sandbox-*"
tf-drift -dir "services/*/infra" -mode drift -format sarif
Selection filters run after discovery and preserve discovery order. Use -include to build focused CI jobs for owned layers, -exclude to remove noisy sandboxes or generated fixtures, and -mode drift when CI should annotate external drift without failing on ordinary unmerged config changes.
The CLI exposes -version and -v as aliases. Both print tf-drift <version> and exit before discovery or engine resolution.
Official release binaries receive the exact release tag from GoReleaser through -ldflags "-X main.version=", so tf-drift -version reports values such as tf-drift v1.0.0. Local make build and make install inject git describe --tags --always --dirty so source builds report the nearest tag, commit, and dirty marker instead of plain dev.
| ID | Date | Decision | Rationale |
|---|---|---|---|
| DEC-001 | 2026-06-15 | Use native engine plan |
Ensures compatibility with custom providers and engine versions. |
| DEC-002 | 2026-06-15 | Default to -lock=false |
Prevents blocking active deployment pipelines. |
| DEC-003 | 2026-06-15 | Use Charm CLI bubbletea |
Gold standard for interactive, modern Go terminal interfaces. |
| DEC-004 | 2026-06-15 | Worker pool reports via p.Send() |
Safely queues UI updates into the Bubble Tea runtime thread. |
| DEC-005 | 2026-06-19 | Default -engine to auto |
Supports OpenTofu first when installed while keeping Terraform fallback for existing users. |
| DEC-006 | 2026-06-19 | Shorten displayed home paths with ~ |
Keeps terminal and app output compact without changing execution paths. |
| DEC-007 | 2026-06-19 | Keep version reporting build-time driven | Release tags come from GoReleaser ldflags, while source builds use git metadata without runtime git calls. |
| DEC-008 | 2026-06-22 | Split external drift from pending plan changes | Terraform plan JSON exposes resource_drift separately from resource_changes, so reports should classify them instead of labeling every non-no-op plan delta as drift. |
| DEC-009 | 2026-06-22 | Add SARIF and severity predicates | CI annotations need a standard upload format, while teams need severity rules that can include layer, action, classification, address, and changed attributes. |