mcpbeat Sign in

Find Tfcompat Bug Skill for Claude

Find and prove a single runtime divergence between OpenTofu and pulumi-hcl with a failing tfcompat test. Use when the user asks to "find another bug/mismatch", find a tfcompat mismatch, or hunt for behavior that differs from Terraform/OpenTofu.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
4
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/pulumi/pulumi-hcl --skill find-tfcompat-bug

The instruction itself

6 sections, as written by the author

Find & prove the next tfcompat bug

Hunt one genuine runtime divergence between OpenTofu (tofu apply) and pulumi-hcl, and

prove it with a failing tfcompat test. That is the whole job of this skill: find a

real divergence and leave behind a test that fails on master for the right reason. You

stop at the proven failure — you do not fix it. To turn a proven failure into a

shipped fix, hand off to the fix-tfcompat-bug skill.

The divergence can be anywhere the two runtimes can disagree — an expression/function

result (the L1 seam), or resource, module, provider, or lifecycle behavior (the L2

seam). Functions are the easiest place to start, not the only place to look.

Direction matters — only one direction is a bug. A bug is an input that **works in

OpenTofu but does not work (or works differently) in pulumi-hcl**: OpenTofu produces a

value and pulumi-hcl errors or returns something else. That is the case a user migrating

real Terraform/OpenTofu configuration into pulumi-hcl actually hits. The reverse — an

input pulumi-hcl accepts that OpenTofu rejects (pulumi-hcl is more lenient) — is not a

bug to chase here: no OpenTofu config depends on it, and tightening pulumi-hcl to also

reject it removes capability without helping any migration. If the only divergence you can

find is pulumi-hcl being more permissive than OpenTofu, discard it and keep hunting.

Each invocation produces exactly one proven failing test. Don't bundle several.

Ground rules (from CLAUDE.md — do not violate)

  • Confirm assumptions with research. Don't guess. Verify every behavioral claim

against OpenTofu source, the cty stdlib source, the docs, or an empirical tofu

probe — never from memory. A backwards memory of token semantics has already burned

us once.

  • Faithfully match OpenTofu. The goal is parity, not "better". No hacks.
  • The deliverable is new test files only — you ADD tests/tfcompat/ files. Don't edit

runtime source under pkg/ (that's the fixing skill's job), and don't touch staging the

user left.

Source-of-truth locations

The Go module cache root is !go env GOPATH/pkg/mod.

  • OpenTofu funcs: !go env GOPATH/pkg/mod/github.com/pulumi/opentofu@*/lang/funcs/*.go
  • OpenTofu binding map: .../opentofu@*/lang/functions.go
  • OpenTofu docs: .../opentofu@*/website/docs/language/functions/*.mdx
  • cty stdlib: !go env GOPATH/pkg/mod/github.com/zclconf/go-cty@*/cty/function/stdlib/
  • pulumi-hcl runtime: pkg/hcl/ (expression eval, resource / module / provider handling)
  • pulumi-hcl function registry: pkg/hcl/eval/functions.go

(func Functions(baseDir string) at the top binds every name to its impl)

  • Function unit tests: pkg/hcl/eval/functions_test.go
  • tfcompat harness + Case type: tests/testutil/tfcompat/harness.go
  • Empirical probe: use the tofu on your PATH (!which tofu)

Step 1 — Find a candidate divergence

Look for any input the two runtimes can evaluate differently. Two seams, easiest first:

  • L1 — expression / function: a function in pkg/hcl/eval/functions.go with a

hand-rolled Impl rather than a delegated stdlib.*Func. Hand-rolled impls are

where function divergences hide. Diff against the OpenTofu impl/docs, and where cheap,

probe both sides:

  # OpenTofu side — write a tiny main.tf and apply
  echo 'output "x" { value = <expr> }' > /tmp/probe/main.tf
  (cd /tmp/probe && tofu apply -auto-approve)
  • L2 — resource / module / lifecycle: behavior around resources, modules,

providers, count/for_each, pre/postconditions, or the preview / destroy / replace

lifecycle. These exercise a provider (e.g. providers.SimpleProvider) — compare a

small program end-to-end through apply, and where relevant preview and destroy.

Good hunting grounds: encoding/escaping funcs, numeric edge cases, empty-collection

errors, date/time token tables, string normalization, unknowns in preview,

precondition/postcondition error messages, replace/destroy lifecycle, module/provider

passthrough. Look for: wrong error on edge input, precision loss, off-by-one token

mapping, charset handling, escaping rules, a field that diverges only in state, an

operation that succeeds on one path and errors on the other.

Keep the direction in mind (see the intro): the case you want is one where **OpenTofu

succeeds and pulumi-hcl errors or returns a different value**. When an `operation that

succeeds on one path and errors on the other` turns up, check which side errors — if

pulumi-hcl is the one that succeeds and OpenTofu errors, that is pulumi-hcl being more

lenient, which is not the bug you are looking for. Discard it and keep hunting.

Observability caveat: pulumi serializes stack-output numbers as float64. Integer

precision past ~16 significant digits is lost on BOTH paths, so it cannot be exposed via

output comparison. Don't build a case whose only difference is unobservable in outputs —

reach for AssertState/ExpectErr (below), or prove it with a unit test, or pick a

different bug.

Step 2 — Prove it with a failing test

Name the case l1_<name> for an expression/function bug or l2_<name> for a

resource/module/lifecycle bug, and create:

  • tests/tfcompat/<l1|l2>_<name>_test.go
  • tests/tfcompat/testdata/cases/<l1|l2>_<name>/main.tf

Existing cases (pick a fresh name):

!ls tests/tfcompat/testdata/cases

The test (copy the Apache header from any sibling _test.go):

// L1 — pure expression, no providers
func TestL1<Name>(t *testing.T) {
	t.Parallel()
	tfcompat.RunCase(t, "l1_<name>", tfcompat.Case{})
}

// L2 — register the provider(s) the program uses
func TestL2<Name>(t *testing.T) {
	t.Parallel()
	tfcompat.RunCase(t, "l2_<name>", tfcompat.Case{
		Providers: []tfcompat.Provider{
			{Name: "simple", Factory: providers.SimpleProvider},
		},
	})
}

RunCase loads every file under testdata/cases/<name>/ and runs it through both

tofu apply and pulumi up, asserting identical stack outputs. For an L1 case the

fixture is pure locals + output (one output per behavior). **Put the fixture on disk

under testdata/cases/<name>/ and pass an empty tfcompat.Case{} — this is the default,

including multi-file cases** (e.g. a templatefile case ships its main.tf *and* the

template it reads; RunCase loads every file in the directory). The Case struct gives

you more knobs when outputs alone can't show the divergence:

  • Providers / Config — register TF providers / set input variables. Config is the

only channel for values not known until runtime (ports, temp paths): declare a variable

in the fixture and set it here — never generate program text in Go.

  • AssertState — assert on resource fields not reachable via outputs (e.g. Protect).
  • Stages — per-stage behavior matched positionally against the disk fixture: Mode

(StageApply / StagePreview / StageDestroy), ExpectErr (require BOTH runtimes to

fail with a matching error substring — the way to prove an error-behavior divergence),

and AssertOutput. A Stage carries behavior only; program files always come from the

case directory. A flat case dir with N Stages entries runs the same program N times

(e.g. preview then apply); a program that must *change* between stages uses numbered

subdirs testdata/cases/<name>/0/, 1/, ... with one Stages entry per subdir (or

none).

Confirm the bug is real on master:

PATH="$PWD/bin:$PATH" go test ./tests/tfcompat/ -run 'Test(L1|L2)<Name>' -count=1 -v

It must FAIL, and the failure must show the genuine OpenTofu-vs-pulumi difference (a real

output diff or a matched ExpectErr), not a harness/provider/fixture error. If it passes,

the bug isn't real (or isn't observable) — go back to Step 1.

Done — hand off

You now have a proven failing tfcompat test, left uncommitted in the working tree.

Report:

  • The divergence — one line: what OpenTofu does vs. what pulumi-hcl does.
  • The test — name + the file paths you created.
  • The failure output — the captured diff/error proving it fails for the right reason.

To fix the divergence and ship it, continue with the

fix-tfcompat-bug skill. Do not fix it here.

How to use it

Copy the folder

Take pulumi/find-tfcompat-bug from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.