You Can't Review an Agent. You Can Review a Plan.
A harness for AI-era Terraform. I'm building one. For a while now I've been developing a harness for infrastructure-as-code as a private SDK and compiler — the layer that sits between whoever proposes a change (a person, an agent, CI) and whatever actually reaches production. This post isn't the too

A harness for AI-era Terraform. I'm building one. For a while now I've been developing a harness for infrastructure-as-code as a private SDK and compiler — the layer that sits between whoever proposes a change (a person, an agent, CI) and whatever actually reaches production. This post isn't the tool. It's the thinking underneath it, and the few pieces I've become most convinced by while building it. (Notes from inside the work — where I've landed so far, not advice.) The problem that sent me down this road is easy to state and easy to underrate. A version of it happened recently. An agent fixed some Terraform; the PR read clean — tidy diff, sensible resource names, a plan output that looked exactly like what I'd asked for. It got approved. And then, at apply time, a different plan ran than the one that was reviewed: apply had re-planned against state that moved in between, and the diff that touched production wasn't quite the diff anyone had read. Nothing broke, that time. But that near-miss is the whole reason the harness exists. Because the danger was never "the agent writes bad HCL." Agents write perfectly good HCL; I let them. The danger is the distance between the plan a human reviewed and the plan that actually runs — and once agents are the ones proposing changes at volume, that distance is the thing I most want to nail shut. Where I've landed for now (and expect to keep revising): What AI-era IaC needs isn't AI that can apply. It's a structure where every change — human or agent — is evaluated at the same boundary, and only a reviewed plan ships. The unit of trust isn't the agent. It's a specific, reviewed plan, bound byte for byte. You can't review an agent. You can only review a plan. Instructions to an agent can be broken. A CI gate can't be talked out of it. Put guidance in the prompt; put the guarantee in the gate. Terraform/OpenTofu don't go away. You wrap them in a harness; you don't replace them. For years IaC had a comfortable shape: a human writes HCL, a human reads the plan, the same human or a teammate runs apply. The author and the applier were the same kind of thing — a person: slow, few, accountable. Reviewing the author and reviewing the change were nearly the same act. That assumption is quietly breaking, and it's breaking because of who's holding the keyboard. GitHub's Copilot coding agent, OpenAI's Codex, Claude Code in GitHub Actions — agents now open issues, push branches, and answer review comments in the repo, and standards like MCP and AGENTS.md are there so they can read your docs, state, and conventions and act on them. The detail that matters isn't any one of those tools; it's the implication: the IaC repo stopped being a place only humans edit. It's a place agents propose changes too. And here's the part you can't review your way out of: you can read one agent's PR carefully, but you cannot review the agent. It isn't a colleague who earns standing over quarters; it's a process that opens the next PR just as confidently whether or not the last one was sound. Confidence is flat. Correctness isn't. So "get the agent to write Terraform" solves the easy half. The half that matters is: what evaluates the change, and is it the same thing for everyone? terraform plan reads remote state and produces a diff against your config. Save that plan and apply it, and Terraform runs exactly those actions with no further prompts. Don't save it — re-plan at apply time — and the gap reopens: state may have drifted, a variable may have moved, and the diff that runs is no longer the diff that was read. That's the near-miss from the top, in one sentence. So the thing worth binding isn't the PR. It's the plan. Call it a plan fingerprint — an identifier over the saved plan, bound together with the findings it was reviewed against and a signature, so apply can refuse anything whose fingerprint doesn't match an approval record. That core — plan, findings, signature — is the part I lean on today. What I want folded into the identifier is wider: the plan JSON, the variables, the policy bundle, the workspace, the provider lock, and the toolchain (Terraform/OpenTofu version, provider versions, runner image, state version), because a plan is only as reproducible as the environment that produced it. I'm still widening what goes in; the honest state is a solid core and a growing edge. One caveat worth stating early: the fingerprint can be public, but the artifact behind it probably shouldn't be. Saved plans and plan JSON can carry sensitive values, so treat them as sensitive build artifacts, not something to paste into a PR comment. The fact I want the gate to be able to state flatly looks less like a paragraph and more like this: IAC.PLAN_DRIFT approval=#4821 severity=high quality=verified claim: applied plan ≠ approved plan cause: apply re-planned against state changed after approval bind: plan_hash + findings + signature (+ vars/policy/toolchain: target) next: reject apply | re-request approval on new fingerprint A pattern-match can only hedge — "this might not be the reviewed plan, please double-check." A fingerprint comparison states one thing flatly: this is not the plan you approved. The rigorous check is the shorter one. I keep drawing the same four boxes, so I've stopped pretending they're incidental. Author what do we want / why (humans and agents write here) Compiler how is it implemented (HCL today: Terraform / OpenTofu) Observer what is true right now (state, drift, DB, audit, cost, history) Evaluator are we allowed to do it (policy, fingerprint, approval, signature) Author — intent. What do we want, and why. Both humans and agents write here, freely. Compiler — translation. Intent into whichever engine you run — swappable by design, though today that's the HCL engines (Terraform/OpenTofu). Observer — reality. More on this below; it's the layer most people under-build. Evaluator — judgment. The only layer that can say no. The load-bearing idea: Author and Compiler can be as smart or as sloppy as they like, because nothing they produce reaches production without clearing the Evaluator. You move safety out of "trust the writer" and into "verify the plan" — which is precisely the move that survives the writer becoming an agent. Most of what I'm building lives in the Compiler and Evaluator boxes; the other two are where it plugs into the world. And to be clear about the engines underneath: Terraform, OpenTofu, and Pulumi are genuinely excellent — I reach for them daily, and the harness wraps them rather than replacing them. But none of them was designed AI-first; each still assumes a human author, a human reviewer, and a human at the apply prompt. That's not a criticism so much as headroom. The engines are great, and there's still real room for the layer above them — and, eventually, the engines themselves — to evolve into something an agent can be trusted to drive. The layer I see skipped most is Observer, usually collapsed into "we have some dashboards." But in an AI-era harness it's doing something specific: giving every actor — human or agent — a grounded picture of the world before they propose against it. Drift was already a problem when only humans touched the cloud; once agents and external tools change things through the API, an out-of-band change the Observer can't see is a blind spot the whole harness inherits. (Recent research is even throwing LLM agents at reconciling out-of-band changes back into code, which tells you it isn't solved.) What I want Observer to actually gather: Terraform/OpenTofu state, and cloud provider inventory drift detection, and CloudTrail / audit logs DB schema state cost data, and security posture policy-violation history, and incident / change history With that, an agent references not just "the current code" but "the real environment," "the intent," and "what went wrong last time." A harness only works if there's no side door. The same boundary has to apply to the human on their laptop, the agent in the repo, the local CLI, and the CI runner — identically. Concretely, three disciplines carry most of it: One policy bundle, everywhere. The rules that run in pre-commit on a laptop are the same rules, same version, that gate the plan in CI. If local and CI can disagree, the local path becomes the bypass. Approve a fingerprint, not a PR. Save the plan, hash it, and let apply consume only a plan whose fingerprint carries a signed approval. A PR can be approved and then have its plan change underneath it; a fingerprint can't. Split the plan role from the apply role. The job that plans can propose; the job that applies accepts nothing but an approved fingerprint. HCP Terraform already leans this way — run stages, policy checks, run tasks, separable plan/apply permissions. The harness isn't new primitives; it's refusing to let anyone route around them. BEFORE — raw AI x Terraform AI Agent --HCL--> terraform apply --> production (review, policy, plan-binding, role split: all fuzzy) AFTER — through the harness AI Agent / Human | intent v Author --> Compiler --> terraform plan / tofu plan | v Evaluator (policy + fingerprint) | v Signed approval + plan fingerprint | v Apply role --------> production The failure mode I most want to avoid is safety that lives in a sentence. "Please don't create a public S3 bucket" in an AGENTS.md is a wish. OPA turning the plan JSON into a decision that fails CI is a fact. Same intent, completely different load-bearing capacity. AGENTS.md and MCP make the Author and Compiler layers stronger — better context, fewer dumb diffs — but they don't make anything safe; they make it more likely to be good, which is not the same claim. Instructions can be broken — by a prompt injection, a confused tool call, a model that just… doesn't. So: put the guidance in AGENTS.md, and the guarantee in the Evaluator. The Evaluator rules I reach for: diffs containing delete / replace / drop → no auto-apply public ingress → denied without an explicit, logged exception high DB sensitivity → encryption, audit log, backup, private network required, or the plan fails plan fingerprint ≠ approval record → apply refused missing / expired / under-privileged approval → apply refused One rule I want but haven't shipped: a stricter policy profile for agent-authored changes than for human ones — routing by author type mechanically, rather than trusting anyone less by vibe. And once agents sit inside CI, the attack surface grows (OWASP keeps an Agentic-AI risk list for exactly this). Four minimums I wouldn't ship without: Separate read-only tools from mutating tools. Let the agent read provider docs and state; don't hand it apply-class operations directly. Separate plan from apply. The agent proposes up to a plan. Apply goes through signed approval and a CI gate. Separate CI job permissions. Plan jobs get the plan role; apply jobs get the apply role and accept nothing but an approved plan. Put policy in code, not the prompt. Don't ask for no public S3 — detect it in the plan and fail. The through-line: stop asking whether to trust the agent, and make its output mechanically evaluable instead. destroy is the DROP COLUMN of infrastructure This is where it stops being a generic "AI + Terraform" post, because the database people already solved a version of it. A DROP COLUMN is a destructive change: irreversible, backward-incompatible, exactly what you don't ship in one move. Atlas detects destructive changes and fails CI over them; the expand-contract pattern — add the new shape, migrate, and only then, separately, remove the old — exists so the dangerous half never rides along with the safe half. Tools like Bytebase wrap the whole thing in review, approval, rollout, and audit rather than letting anyone touch prod directly. Infrastructure has the same hazards and mostly none of the discipline. A destroy, a forced replace, a deleted stateful resource — these are the DROP COLUMN of infra. So here's the rule I borrow: destructive infra changes shouldn't be one-shot. Today the harness enforces that as a hard stop — a destructive change is a critical hazard that can't clear the gate without explicit human approval. The direction I'm pushing toward is the database's fuller two-phase discipline: expand, migrate, then contract, each step approved on its own. The stop exists now; the mechanical two-step is where I'm headed. Make it concrete. These three simplified plan entries are not equal, even if a shallow policy sees them as the same category of destructive change: replace aws_db_instance.main delete aws_s3_bucket.logs destroy aws_kms_key.customer_data A generic "no destructive changes" rule treats them as one category. But the Evaluator shouldn't reason about the verb — it should reason about what the resource means. Here's the reading I'm building the Evaluator toward — polished more than it currently is, but the direction is the point: destroy aws_kms_key.customer_data classification = confidential stateful = true recovery = hard # key loss = data loss blast_radius = production => DENY. expand-contract required: new key -> re-encrypt -> retire old, each approved delete aws_s3_bucket.logs classification = internal stateful = true recovery = medium # lifecycle-archived elsewhere blast_radius = production => HOLD. require explicit retention check + second approval Now the gate isn't guessing from a keyword; it's deciding from the resource's meaning — at least, that's the target. I want to be honest about where the line falls in what I've actually built: the database-side version of this is real — a schema's sensitivity already propagates into required controls — while deriving infra-side labels like recovery and blast_radius to the precision shown above is still partly ahead of me, not fully behind. The move I like even more is where those labels come from: propagate intent across the boundary. If a schema is labeled confidential, deletion-protected, RPO-strict, that intent shouldn't stop at the database — DB schema intent classification = confidential rpo = 5min deletion = protected | propagate v Infra requirement KMS encryption . PITR backup . deletion protection private endpoint . audit logging | enforce v Policy gate any of these unset -> plan fails Put plainly: the same way you don't let a DROP COLUMN through on a whim, you don't let a destroy through on an agent's confidence. Most "AI IaC" writing stops at "the agent can write HCL." The interesting seam is one layer down — where application semantics, DB constraints, and infra safety turn out to be the same dependency seen from different angles. Software supply chains already made this move — SLSA provenance, artifact attestations, signed build provenance: verifiable claims about who made what, under whose authority. If a build artifact deserves that, an infra change deserves it at least as much, and an AI-involved one most of all. So an approval isn't a green checkmark. It's a signed statement — the same core-vs-target split as the fingerprint above: the load-bearing fields today are the plan, its findings, and the signature; the fuller record below is the shape I'm building toward: Field Meaning commit SHA which source change plan hash which plan was approved plan JSON hash the exact diff evaluated policy bundle hash which rules judged it workspace / env dev / stg / prod actor AI, human, CI job, service account reviewer identity who approved approval reason why it was allowed expiry how long the approval is valid signature tamper-evidence, non-repudiation Now "who approved this change, against which plan, having seen which policy result" has an answer you can verify months later — not reconstruct from memory and Slack scrollback. Call it non-repudiation for the AI era. "This is just CI/CD with extra steps." Partly. The step that carries the weight is binding approval to a fingerprint, not a PR. That one substitution is most of the value. "Agents can be trusted more than this implies." Maybe, eventually. But the harness costs about the same whether the agent is excellent or awful, and it fails safe either way. I'd rather not stake the blast radius on the model being good this week. "plan/apply roles and policy gates already exist." They do. I'm not claiming new primitives — I'm claiming one boundary for everyone, human and agent, CLI and CI, with no privileged bypass. "Expand-contract for infra is over-engineering." For a weekend project, yes. For anything with a stateful resource you'd grieve losing, the DB people already learned this the expensive way, and I'd rather borrow the scar than earn my own. Three honest edges. The Compiler layer fragments — OpenTofu is going its own way as a CNCF project, and Pulumi and CDKTF aren't HCL at all; the harness only helps if the Evaluator can read every engine's plan format. Drift is a genuinely open Observer problem — an Observer that can't see out-of-band change is a harness with a blind spot. And every gate is a place a rushed team adds a bypass; an "emergency skip" used weekly is theater. If the escape hatch isn't itself logged, signed, and rare, none of the above holds. The last decade of IaC went hand-crafted → declared. The next one is going declared → self-driving — and "self-driving" is going to get read as "unattended apply." I think that's the wrong reading. Self-driving infrastructure isn't an agent holding the apply button. It's an agent proposing freely into a harness where only a reviewed, fingerprinted, signed plan reaches production. What matters in AI-era IaC was never that the agent can write HCL. It's that whatever it writes passes through intent, implementation, observation, and evaluation — Author, Compiler, Observer, Evaluator — and that only a reviewed plan lands in prod. Don't put the caution in the prompt. Put it in the gate. That's the shape I keep building toward — still wrong in places I haven't found yet.
Key Takeaways
- •A harness for AI-era Terraform. I'm building one
- •This story was reported by Dev.to, covering developments in the dev space.
- •AI advancements continue to reshape industries — read the full article on Dev.to for complete coverage.
📖 Continue reading the full article:
Read Full Article on Dev.to →


