OpenTofu skill for Codex
Terraform to tofu with a no-change plan as proof. Codex moves Terraform repositories to OpenTofu one root module at a time, proving each step with a plan that shows no changes before any apply, and switches CI and wrapper scripts to the tofu binary with regenerated lock files. Afterwards it adopts OpenTofu-only features deliberately, such as KMS-backed state encryption with a plaintext fallback during rollout, and records the lock-in each one adds.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- opentofu Installs to ~/.agents/skills/opentofu/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install opentofu The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project opentofu Installs to .agents/skills/opentofu/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/opentofu.zipunzip -o opentofu.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) opentofu PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $opentofu, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Migrates Terraform configurations to OpenTofu and uses its own features - the tofu CLI, registry.opentofu.org providers, client-side state and plan encryption with PBKDF2 or KMS key providers, early variable evaluation in backends and module sources, .tofu override files, provider for_each, the -exclude flag, OCI registries, ephemeral resources and the enabled meta-argument. Use when switching a repo or CI pipeline from terraform to tofu, encrypting state, deciding whether to adopt OpenTofu-only syntax, or debugging differences between the two tools.
Use it when
- A team is switching from Terraform to OpenTofu
- State must be encrypted client-side before it reaches the backend
- You want OpenTofu-only features like provider for_each or early evaluation
- terraform and tofu produce different plans for the same code
Use something else when
- The work is general module design that applies to both tools: use terraform-modules
- You need imports, moves or lock recovery in state: use terraform-state
How to check the result
tofu plan -detailed-exitcode exits 0 in every migrated root module and no CI job, Makefile or wrapper still calls the terraform binary.
The mistake it prevents
Enabling state encryption or other OpenTofu-only syntax while some pipelines still run Terraform against the same state.
A migration is done when tofu plan shows no changes against the same state Terraform
managed, CI runs tofu with a committed lock file, and the team knows which OpenTofu-only
features they adopted (each one removes the option of going back to Terraform without
edits). New features are adopted on purpose, one at a time, with a plan check after each.
Inspect first
- Read
AGENTS.md, then find every placeterraformis invoked: Makefiles, CI workflows, pre-commit, Terragrunt (terraform_binary), Atlantis, wrapper scripts. - Record the current Terraform version (
terraform version) andrequired_versionconstraints. Migration from Terraform 1.5.x and earlier is direct; from 1.6 and later, follow the OpenTofu docs’ migration guide for that exact version because the two tools diverged after 1.5 (some Terraform-only features must be removed first). - List features in use that may not exist in OpenTofu, or behave differently: HCP
Terraform
cloudblocks and workspaces, Terraform Stacks, Sentinel policies, provider-specific features only published to the HashiCorp registry. Check each one. - Confirm state backups exist (versioned bucket or a
terraform state pullcopy).
tofu init downloads providers from registry.opentofu.org (network). In a sandbox with
network off, ask for approval or use a configured provider mirror. Plans against real
backends need cloud credentials: hand those steps to the user if unavailable.
Migration workflow
- [ ] 1. With Terraform: terraform plan shows no changes (fix drift first)
- [ ] 2. Back up state for every workspace / root module
- [ ] 3. Install tofu at a pinned version; tofu version
- [ ] 4. tofu init -upgrade in one root module (refreshes .terraform.lock.hcl hashes)
- [ ] 5. tofu plan: must show no changes
- [ ] 6. With user confirmation: tofu apply (a no-op apply that writes state with tofu)
- [ ] 7. Switch CI and wrappers to tofu; commit the lock file
- [ ] 8. Repeat per root module; only then adopt OpenTofu-only features
terraform plan -detailed-exitcode # before: expect exit 0
terraform state pull > pre-tofu-$(date +%F).tfstate
tofu init -upgrade
tofu plan -detailed-exitcode # after: expect exit 0
If tofu plan shows changes, stop. Typical causes: a provider version resolved
differently (pin it in required_providers), a Terraform-only language feature, or
pre-existing drift that step 1 missed.
CI change for GitHub Actions:
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.11.0 # pin; bump deliberately
- run: tofu init -input=false
- run: tofu plan -input=false -out=tfplan
The lock file keeps its name (.terraform.lock.hcl). Generate hashes for every platform
CI and developers use: tofu providers lock -platform=linux_amd64 -platform=darwin_arm64.
Choosing what to adopt
| Feature | Version | Adopt when | Lock-in cost |
|---|---|---|---|
| State and plan encryption | 1.7 | State holds secrets and backend encryption alone is not enough (shared buckets, compliance) | High: Terraform cannot read encrypted state |
| Variables and locals in backend and module source/version (early evaluation) | 1.8 | Per-environment backends without -backend-config files or wrapper tools | Medium |
.tofu files (override same-named .tf) | 1.8 | A shared module must support both tools with tool-specific code | Low, by design |
Provider for_each (multi-region, multi-account) | 1.9 | Same resources across many regions or accounts | Medium |
-exclude flag | 1.9 | Applying everything except a broken or slow resource during an incident | None (CLI only) |
| OCI registries for providers and modules | 1.10 | Air-gapped or internal artifact registries | Low |
S3 native locking (use_lockfile) | 1.10 | New S3 backends; drop the DynamoDB table | Low |
| Ephemeral resources, write-only attributes | 1.11 | Short-lived credentials or secrets that must never land in state | Medium |
enabled meta-argument | 1.11 | Zero-or-one resources or modules instead of count = var.x ? 1 : 0 | Medium |
Default: migrate first with zero syntax changes, run on OpenTofu for a release cycle, then adopt features individually.
State encryption
terraform {
encryption {
key_provider "aws_kms" "state" {
kms_key_id = "alias/tofu-state"
region = "eu-west-1"
key_spec = "AES_256"
}
method "aes_gcm" "kms" {
keys = key_provider.aws_kms.state
}
method "unencrypted" "migrate" {}
state {
method = method.aes_gcm.kms
fallback {
method = method.unencrypted.migrate # read existing plaintext state once
}
}
plan {
method = method.aes_gcm.kms
}
}
}
Rollout:
- Back up state. Add the block with the
unencryptedfallback. - With confirmation,
tofu apply(any apply rewrites state encrypted). Inspect the state object: it should no longer be readable JSON with resource attributes. - Remove the
unencryptedmethod and fallback, then setenforced = truein thestateblock so plaintext writes fail. - Store the key outside the state’s blast radius. Losing a PBKDF2 passphrase or KMS key
means losing the state. Key rotation uses
fallbackwith the old key the same way.
terraform_remote_state consumers need the same encryption config (under
remote_state_data_sources) to read encrypted state.
Early evaluation and enabled
variable "env" { type = string }
terraform {
backend "s3" {
bucket = "acme-tfstate-${var.env}"
key = "app/terraform.tfstate"
region = "eu-west-1"
use_lockfile = true
}
}
resource "aws_cloudwatch_log_group" "debug" {
name = "/app/${var.env}/debug"
lifecycle {
enabled = var.env != "production"
}
}
Only variables, locals and literals are allowed in early-evaluated places; resource
attributes and data sources are not. Pass -var env=prod (or TF_VAR_env) to init as
well as plan.
Hard rules
- Never run Terraform and OpenTofu against the same state concurrently or alternately after OpenTofu-only features are in use; Terraform may fail to read it or drop what it does not understand.
- Every migration apply, encryption rollout and backend change requires the user’s explicit confirmation after seeing a no-change plan.
- Keep the Terraform binary and the pre-migration state backup until every root module has been migrated and verified.
- Registry module sources (
hashicorp/consul/aws) resolve against registry.opentofu.org; if a module or provider is missing there, use a fully qualified Git source or report it. - Provider bugs are the same in both tools; do not migrate to fix a provider issue.
Verification
tofu version
tofu validate
tofu plan -detailed-exitcode; echo "exit=$?" # 0 = no changes
tofu providers # sources resolve to registry.opentofu.org
grep -rn "terraform " .github/ Makefile 2>/dev/null # leftover terraform invocations
Output
**Scope:** 6 root modules (envs/*), Terraform 1.5.7 -> OpenTofu 1.11.0
**Per module:** tofu plan exit 0 before the confirmation apply; state serials recorded
**CI:** setup-opentofu pinned to 1.11.0; lock files regenerated for linux_amd64 and darwin_arm64
**Adopted features:** none yet (proposed next: S3 use_lockfile, then state encryption with KMS)
**Rollback path:** Terraform 1.5.7 + pre-migration backups (valid until encryption is enabled) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "OpenTofu"
short_description: "Terraform to tofu with a no-change plan as proof"
default_prompt: "$opentofu Migrate this repo and its GitHub Actions pipeline from Terraform to OpenTofu" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.