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:
→ safe
→ review
→ dangerous
→ 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:
- At import time, each adapter module is loaded and calls
register_adapter(MyAdapter()). - When a dedicated adapter command runs, its handler loads the JSON and calls
detect_adapter(data). detect_adapteriterates every registered adapter, callingcan_handle(data)on each.- 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 withextract_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_handlecorrectly 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?