Docs / Adapters

One risk gate for every infrastructure tool.

readtheplan ships cross-tool infrastructure gates and a public adapter system for adding Packer, CloudFormation, or custom in-house plan formats. This guide covers the built-in integrations and the BaseAdapter extension contract.

What are adapters?

An adapter translates a specific IaC tool's plan output into readtheplan's internal ResourceChange representation so the rest of the engine — risk classification, compliance mapping, agent gating, evidence envelopes — works identically regardless of the source tool.

Terraform and OpenTofu work natively with strict dependency-lock analysis and first-party AWS, GCP, Azure, complete HashiCorp Kubernetes, Helm, New Relic, and PagerDuty catalogs, Cloudflare, Datadog, Grafana, GitHub, GitLab, HashiCorp Vault, and HCP Terraform/TFE provider rules. Built-in adapters cover CloudFormation and AWS CDK, Azure Bicep source and ARM What-If, Kubernetes, Ansible playbooks and project configuration, Salt states and project configuration, Nix flakes/locks and NixOS modules, Microsoft DSC and PowerShell DSC, CFEngine policy and Augments, Jenkins pipelines/JCasC, Pulumi, Chef recipes, projects, and runtime configuration, Puppet manifests and projects, GitHub Actions, GitLab CI, CircleCI, Docker Compose and Buildx Bake, HashiCorp Nomad, HashiCorp Packer, HashiCorp Vagrant, cloud-init, Dockerfiles, Azure Pipelines, and Bitbucket Pipelines. Rendered Helm, Kustomize, and Crossplane resources flow through the Kubernetes gate. Terragrunt and internal tools can use the same public adapter interface.

Use readtheplan scan . to discover high-confidence inputs and combine the matching adapters into one project-level gate. Use an individual command when a file name is intentionally unconventional or when you want to force a specific parser.

Built-in commands

readtheplan scan --framework soc2 .
readtheplan cloudformation changes.json
readtheplan cdk cdk.out/manifest.json
readtheplan terraform-lock .terraform.lock.hcl
readtheplan terraform-state state.json
readtheplan azure whatif.json
readtheplan bicep main.bicep
readtheplan kubernetes rendered.yaml
readtheplan pulumi preview.json
readtheplan pulumi-project Pulumi.yaml
readtheplan ansible playbook.yml
readtheplan ansible-project ansible.cfg
readtheplan ansible-project inventory.yml
readtheplan ansible-project execution-environment.yml
readtheplan ansible-project ansible-navigator.yml
readtheplan salt state.sls
readtheplan salt-project master
readtheplan nix flake.nix
readtheplan dsc configuration.dsc.yaml
readtheplan cfengine promises.cf
readtheplan opa policy.rego
readtheplan sentinel policy.sentinel
readtheplan vagrant Vagrantfile
readtheplan cloud-init user-data.yml
readtheplan docker-bake docker-bake.hcl
readtheplan terraform-stack stack.tfdeploy.hcl
readtheplan dockerfile Dockerfile
readtheplan azure-pipelines azure-pipelines.yml
readtheplan bitbucket-pipelines bitbucket-pipelines.yml
readtheplan jenkins Jenkinsfile
readtheplan jenkins-jcasc jenkins.yaml
readtheplan jenkins-project plugins.txt
readtheplan teamcity .teamcity/settings.kts
readtheplan chef default.rb
readtheplan chef-project Policyfile.rb
readtheplan chef-project Berksfile
readtheplan chef-project Berksfile.lock
readtheplan chef-project client.rb
readtheplan chef-project chef-server.rb
readtheplan puppet site.pp
readtheplan puppet-project Puppetfile
readtheplan puppet-project puppet.conf
readtheplan puppet-project bolt-project.yaml
readtheplan puppet-project inventory.yaml
readtheplan github-actions .github/workflows/deploy.yml
readtheplan gitlab-ci .gitlab-ci.yml
readtheplan circleci .circleci/config.yml
readtheplan travis-ci .travis.yml
readtheplan drone-ci .drone.yml
readtheplan woodpecker-ci .woodpecker.yml
readtheplan concourse pipeline.yml
readtheplan bamboo bamboo-specs/bamboo.yml
readtheplan codebuild buildspec.yml
readtheplan cloud-build cloudbuild.yaml
readtheplan codepipeline codepipeline.json
readtheplan sops .sops.yaml
readtheplan docker-compose compose.yml
readtheplan nomad job.nomad.hcl
readtheplan packer image.pkr.hcl
readtheplan skaffold skaffold.yaml
readtheplan devspace devspace.yaml
readtheplan tilt Tiltfile
readtheplan cue deploy_tool.cue
readtheplan jsonnet main.jsonnet
readtheplan tanka environments/production/spec.json
readtheplan helmfile helmfile.yaml.gotmpl
readtheplan terramate terramate.tm.hcl
readtheplan spacelift .spacelift/config.yml
readtheplan ytt config.yml
readtheplan vendir vendir.yml
readtheplan kbld kbld.yml
readtheplan imgpkg .imgpkg/images.yml
readtheplan kapp kapp-config.yml

CloudFormation, Azure What-If, Kubernetes, and Pulumi consume structured diffs. AWS CDK consumes synthesized Cloud Assembly and asset manifests without executing the app. Bicep source receives conservative static analysis before compilation. Ansible playbooks, Galaxy requirements, static YAML/INI inventory, dynamic inventory plugin configuration, static Salt SLS input, and rendered Salt master/minion, top, and roster files consume YAML structurally; Terraform/OpenTofu dependency locks use strict literal HCL validation, while state analysis accepts stable show -json output or read-only raw v4 snapshots and never emits values or modifies state; Nix lock graphs use strict JSON while flake and NixOS module source uses a conservative syntax-aware scanner; DSC v3 documents use strict JSON/YAML and PowerShell DSC source is scanned without execution; CFEngine policy source is scanned without evaluation and Augments use strict JSON; SOPS policy and encrypted YAML/JSON/dotenv/INI metadata are inspected without decryption or key-service access; ansible.cfg and INI inventory are parsed without interpolation, while YAML inventory plugins are never executed or allowed to contact their APIs, while templated Salt SLS input falls back to a conservative scanner. Docker Compose and Buildx Bake definitions are parsed structurally without resolving external files, build contexts, caches, or registries, while Nomad accepts HCL/JSON jobspec source or the scheduler's structured job-plan API response and Packer consumes saved human or machine-readable inspect output. Jenkinsfiles, Chef recipes/projects/runtime configuration, Puppet manifests/projects, GitHub Actions, GitLab CI, and CircleCI use conservative syntax-aware scanning; Chef Berksfile Ruby directives and modern/legacy Berksfile.lock graphs are parsed without dependency resolution, while client.rb, Workstation config.rb/knife.rb, solo.rb, chef-server.rb, and supported .d fragments are parsed statically without evaluating Ruby; puppet.conf is parsed as duplicate-safe sectioned configuration for agent, server, and CA trust and execution settings; Jenkins JCasC YAML is parsed structurally for controller identity, credentials, agents, libraries, and script authority: unknown or unexpanded constructs become review, and source files are never executed. Skaffold Config YAML is parsed without resolving imports, building images, rendering manifests, executing hooks/actions, or contacting Kubernetes clusters.

Why write one?

  • You use an internal provisioning tool and want readtheplan's risk analysis on its plans.
  • You have a custom IaC tool and want compliance gates and audit evidence.
  • You want to extend the type-map for CloudFormation or Kubernetes resources not yet covered.

Skaffold pipelines

Review build, render, deploy, verify, and custom-action boundaries statically:

readtheplan skaffold --framework soc2 skaffold.yaml

DevSpace projects

Review executable pipelines, image builds, deployments, and live-development mutations without invoking DevSpace or contacting a cluster:

readtheplan devspace --framework soc2 devspace.yaml

Tilt development environments

Review executable Tiltfile behavior without evaluating Starlark, loading extensions, building images, or contacting Docker and Kubernetes:

readtheplan tilt --framework soc2 Tiltfile

CUE configuration and workflows

Review source, module dependencies/replacements, and executable workflow tasks without evaluating CUE, downloading OCI modules, or running cue cmd:

readtheplan cue --framework soc2 deploy_tool.cue
readtheplan cue cue.mod/module.cue

Jsonnet and Grafana Tanka

Scan Jsonnet source, Tanka Environment metadata, and jsonnet-bundler manifests and locks without evaluating imports, native callbacks, Helm/Kustomize, kubeconfig, or Kubernetes cluster state:

readtheplan jsonnet --framework soc2 main.jsonnet
readtheplan tanka environments/production/spec.json
readtheplan jsonnet jsonnetfile.lock.json

Helmfile orchestration

Review templated state and dependency locks without executing processes, reading files, resolving secrets, downloading charts, loading kubeconfig, or contacting Kubernetes:

readtheplan helmfile --framework soc2 helmfile.yaml.gotmpl
readtheplan helmfile helmfile.lock

Terramate orchestration and generation

Review stacks, safeguards, generated infrastructure, scripts, output sharing, and cloud synchronization without traversing the project, generating files, or executing Terramate, Terraform, OpenTofu, or Terragrunt:

readtheplan terramate --framework soc2 terramate.tm.hcl
readtheplan terramate backend.tf.tmgen

Spacelift runtime orchestration

Review repository and single-stack runtime overrides, workflow hooks, runner images, environment inputs, tool versions, Terragrunt behavior, and module tests without contacting Spacelift, pulling images, or executing infrastructure tools:

readtheplan spacelift --framework soc2 .spacelift/config.yml

Carvel supply chain and deployment

Follow the Carvel path from fetched inputs through ytt rendering, image build/locking, OCI bundles, and kapp live-resource ownership without fetching, building, pushing, rendering, or contacting Kubernetes:

readtheplan ytt --framework soc2 config.yml
readtheplan vendir vendir.yml
readtheplan kbld kbld.yml
readtheplan imgpkg .imgpkg/images.yml
readtheplan kapp kapp-config.yml

Packer image builds

Gate native Packer HCL/JSON source directly, or use saved inspect output, without initializing plugins or starting a build:

readtheplan packer --framework soc2 image.pkr.hcl
packer inspect -machine-readable image.pkr.hcl > packer-inspect.txt
readtheplan packer packer-inspect.txt

Source mode classifies plugin provenance, builders, communicators, base inputs, guest and host provisioners, artifact post-processors, publishing, variables, secrets, and unresolved expressions. Inspect mode retains its explicit configuration limitation. Run packer validate only inside the caller's existing trust boundary.

Docker build graphs, Compose, and Nomad

Gate container runtime boundaries and scheduler decisions without starting a container or contacting a cluster.

readtheplan docker-bake docker-bake.hcl
readtheplan docker-compose compose.yml

# POST the job specification to /v1/job/:id/plan with Diff enabled,
# save the JSON response, then review it locally:
readtheplan nomad job.nomad.hcl

Bake checks target graphs, contexts, inheritance, secret/SSH forwarding, privileged entitlements, caches, outputs, publication, source policy, and attestations without invoking Buildx. Compose checks image immutability, builds, commands, privileged execution, host/container namespaces, capabilities, mounts, devices, secrets, external file boundaries, security profiles, and published ports. Nomad checks jobspec source and placement failures, allocation stops and replacements, rollout operations, task drivers, mutable images, commands, and secret-bearing fields from the scheduler plan.

Azure Bicep source and ARM What-If

Gate Bicep source locally before compilation, then ask Azure Resource Manager to retain full resource payloads for operation-level confirmation:

readtheplan bicep --framework soc2 main.bicep
bicep lint main.bicep
bicep build main.bicep

az deployment group what-if \
  --resource-group production \
  --template-file main.bicep \
  --result-format FullResourcePayloads \
  --no-pretty-print --output json > whatif.json

readtheplan azure whatif.json

Source analysis covers broad scopes, modules, RBAC/policy/locks, Deployment Scripts, public access, complete-mode deletion, secure parameters and outputs, and external compiler boundaries without restoring modules or reading files. The What-If gate maps create, modify, delete, ignore/no-change, deploy-only, unsupported, and potential changes map into the shared gate. Full before/after resource payloads feed Azure VM, AKS, storage, RBAC, and network-security rules. ResourceIdOnly Deploy results, unresolved potential changes, nested-deployment diagnostics, and unsupported predictions require manual review rather than receiving a false-safe result.

Rendered Kubernetes and infrastructure controllers

Review the rendered artifact that Kubernetes will receive. readtheplan accepts one manifest, kind: List, multi-document YAML, JSON resources, or an explicit old/new manifest diff wrapper.

helm template web ./chart -f values-prod.yaml > rendered.yaml
readtheplan kubernetes rendered.yaml

kubectl kustomize overlays/production > rendered.yaml
readtheplan kubernetes rendered.yaml

Known workloads, services, RBAC, secrets, storage, and network-policy kinds use their resource-aware rules. Unknown custom resources—including Crossplane managed resources—default to review because their controller determines the real cloud-side effect. CRDs, API services, and admission webhook configurations are dangerous because they extend cluster control-plane behavior.

Argo CD Applications, ApplicationSets, and AppProjects receive deeper GitOps rules. Automated pruning and wildcard source, destination, resource, or project-role scopes are dangerous; scoped source/destination and generator changes require review. Flux sources, Kustomizations, HelmReleases, image update automation, and webhook Receivers also receive native rules for source trust, immutability, pruning, remediation, Git writes, and event ingress. Tekton Tasks, Pipelines, Runs, remote resolvers, workspaces, and Triggers receive native execution, identity, storage, and webhook rules. Cluster API Clusters, ClusterClasses, Machines, control planes, bootstrap configurations, health checks, runtime hooks, add-ons, and infrastructure-provider objects receive cluster and machine lifecycle rules. Karpenter NodePools, NodeClaims, and NodeClasses receive provisioning, disruption, drain, IAM, image, networking, storage, and bootstrap rules.

The BaseAdapter interface

Every adapter extends the abstract BaseAdapter class from src/readtheplan/adapters/base.py. Four members are required:

class BaseAdapter(ABC):

    @property
    @abstractmethod
    def adapter_name(self) -> str:
        """Unique name, e.g. 'pulumi' or 'cdk'."""
        pass

    @abstractmethod
    def can_handle(self, input_data: dict) -> bool:
        """Return True if input_data matches this tool's format."""
        pass

    @abstractmethod
    def extract_changes(self, input_data: dict) -> list[dict]:
        """Flatten the plan into a list of per-resource dicts."""
        pass

    @abstractmethod
    def normalize_change(self, raw_change: dict) -> ResourceChange:
        """Map one raw change dict to a ResourceChange dataclass."""
        pass

adapter_name

A short, kebab-case identifier unique to your adapter. Used as the registration key and surfaced in agent-gate output under the adapter field. Example: "pulumi", "aws-cdk", "crossplane".

can_handle(input_data) -> bool

Probe the raw plan dictionary for format-specific signatures. Be strict enough that you don't false-positive on other tools' JSON. CloudFormation checks for a "Changes" list or "old_template" and "new_template"; Kubernetes checks for "old_manifests" and "new_manifests", or a "resources" array with manifest kinds.

extract_changes(input_data) -> list[dict]

Extract a flat list of per-resource change dictionaries from the raw plan JSON. Each element is an adapter-normalized dict — it doesn't have to be a ResourceChange yet, but it should contain all the fields normalize_change needs (action, resource type, address, replacement metadata, etc.).

You can also attach an optional "_metadata" dict (containing "before" and "after" properties) that the rules engine uses for deep resource-specific escalation (e.g., IAM policy diff, S3 public access check, RDS version bump).

normalize_change(raw_change) -> ResourceChange

Convert one extracted change dict into a ResourceChange dataclass:

@dataclass(frozen=True)
class ResourceChange:
    address: str        # Logical or physical resource identifier
    resource_type: str  # Normalized type, e.g. "aws_s3_bucket"
    actions: tuple      # One or more of ("create", "update", "delete")
    risk: str           # "safe" | "review" | "dangerous" | "irreversible"
    explanation: str    # Human-readable reason for the risk level

The actions tuple drives the risk taxonomy:

("create",)

safe

("update",)

review

("delete", "create")

dangerous

("delete",)

irreversible

The analyze() template method on BaseAdapter calls extract_changes → iterates → normalize_change → optionally _apply_resource_rules for deep risk escalation. You generally don't override analyze() — just provide the four abstract members.

Built-in Pulumi support

Generate a preview without exposing secret values, then pass the resulting artifact to the local gate:

pulumi preview --json > preview.json
readtheplan pulumi preview.json
readtheplan pulumi --framework soc2 preview.json
readtheplan pulumi-project Pulumi.yaml
readtheplan pulumi-project Pulumi.prod.yaml
readtheplan pulumi-project PulumiPolicy.yaml

The adapter accepts Pulumi preview digest JSON and newline-delimited streaming engine events. It reads oldState/newState inputs, converts Pulumi provider types into the shared rule vocabulary, and normalizes nested camelCase input names before applying resource-aware rules. Root stack bookkeeping is ignored.

The separate pulumi-project gate strictly parses project, stack, and policy-pack YAML without executing Pulumi. It reviews runtime/compiler commands, entry-point and config paths, backend state, package and local-plugin provenance, secret schemas and encrypted stack values, ESC imports, and policy execution. Pulumi YAML resource declarations also receive the shared deep resource rules.

Create is initially safe, update and import require review, replacement is dangerous, and delete is irreversible. Deep rules can escalate that baseline—for example, a security-group update opening an internet-wide CIDR becomes dangerous. The command exits 0 for proceed, 1 for warn, and 2 for block.

Do not add --show-secrets when creating review artifacts. readtheplan does not need plaintext secrets to classify the preview.

How adapters register & auto-detect

The module src/readtheplan/adapters/__init__.py maintains an in-memory registry — a dict[str, BaseAdapter] keyed by adapter_name.

_registry: dict[str, BaseAdapter] = {}

def register_adapter(adapter: BaseAdapter) -> None:
    _registry[adapter.adapter_name] = adapter

def get_adapter(name: str) -> BaseAdapter:
    return _registry[name]

def detect_adapter(input_data: dict) -> BaseAdapter | None:
    for adapter in _registry.values():
        if adapter.can_handle(input_data):
            return adapter
    return None

The flow works as follows:

  1. At import time, each adapter module is loaded and calls register_adapter(MyAdapter()).
  2. When a dedicated adapter command runs, its handler loads the JSON and calls detect_adapter(data).
  3. detect_adapter iterates every registered adapter, calling can_handle(data) on each.
  4. The handler verifies the detected adapter type, then calls that adapter's analyzer. The first registry match wins, so order matters if two adapters claim the same input.

Important: Make your can_handle() check precise enough that it won't accidentally claim Terraform, CloudFormation, or Kubernetes output. When in doubt, check for multiple format-specific keys. Order of registration in __init__.py determines priority.

Testing an adapter

Each adapter should test strict format detection, every supported operation, type normalization, before/after metadata flow into deep rules, the full agent-gate contract, compliance catalogs, invalid input, and CLI exit codes. Use the shipped Pulumi tests as the structured-plan reference and the Jenkins tests as the conservative scanner reference.

pytest tests/test_pulumi_adapter.py -v
pytest tests/test_jenkins_adapter.py -v

Submitting an adapter as a PR

Once your adapter is implemented and tested, open a pull request against the readtheplan repo. Follow these guidelines:

Checklist

  • One adapter per PR. If you're adding multiple adapters, submit them separately for focused review.
  • Include tests. All unit tests for can_handle, normalize_change, and at least one integration test with extract_changes + analyze.
  • Include a fixture. A small, anonymized plan JSON in tests/fixtures/ for your tool, with a test that validates the agent-gate contract.
  • Update __init__.py. Import and register your adapter. Add it to __all__.
  • Update the type map. If your adapter maps resource types, include coverage for the most common resources in your tool.
  • No regressions. Run the full test suite before opening the PR: pytest.

PR description template

## Summary

Adds a new adapter for [Tool Name] — [one-line description].

## Changes

- `src/readtheplan/adapters/[tool].py` — new adapter file
- `src/readtheplan/adapters/__init__.py` — registration and export
- `tests/test_[tool]_adapter.py` — unit and integration tests
- `tests/fixtures/[tool]_sample.json` — example plan fixture

## Testing

- `pytest tests/test_[tool]_adapter.py -v` — all tests pass
- `pytest` — full suite passes, no regressions

## Adapter detail

| Field          | Value            |
|----------------|------------------|
| `adapter_name` | `[tool]`         |
| Input format   | `[tool] plan`    |
| CLI command    | `detect` / custom |

## Type map coverage

Lists the resource types currently mapped in the adapter.

Review expectations

Maintainers will review:

  • Detection accuracy — does can_handle correctly identify your tool's format without false-positives?
  • Risk correctness — are create/update/delete/replace mapped to the right risk tiers?
  • Type normalization — are resource types mapped to the canonical aws_* naming convention?
  • Test coverage — are edge cases (unknown actions, missing fields, empty plans) covered?
  • Fixture quality — is the fixture realistic and representative?