Where things are stored#

choudoufu writes in three places. Two can both end up as SSM parameters, which is why they get confused. They do different jobs and have different owners.

WhatWhere it livesWho reads itLosing it costs
Ownership markersTwo tags on the resource itselfchoudoufu, and you, with any cloud toolThe resource goes invisible and the next plan proposes a duplicate
Micro-state recordsA local directory beside the module unless you declare a record_store on SSM or S3choudoufu onlyChurn, since the effect re-runs or its value regenerates
ReceiptsOrdinary resources you declare, by convention SSM parametersYou, your reviewers, your incident responderNothing structural. It is your data, in your configuration

The first is the product. The second is plumbing that is there by default and that you point somewhere else when a team needs to share it. The third you write yourself, and choudoufu only lints it.

Ownership markers#

Two tags, tofu-estate and tofu-address, written onto each resource as it is created. Every plan rebuilds prior state by reading them back off the live system.

Only these are authoritative about what you own, and they live on your resources in your account rather than anywhere choudoufu keeps. live/MARKERS.md is the normative spec and the surface external tooling can rely on.

Nothing else on this page is required. An estate of ordinary cloud resources needs markers and nothing more.

The record store#

Some resources have no cloud twin. Nothing in AWS knows a null_resource ran a script, a time_static captured a timestamp, or a random_pet generated a name, so no marker can recover them.

Those persist as micro-state, one small record each. Every estate has a store for them: a live block that names no record_store gets a local one, a .tofu-records directory beside the module, the way stock OpenTofu implies a local state file. Nothing to turn on.

Declare a record_store when you want the records somewhere a team shares, or somewhere that survives the working copy.

# estate.chdf.hcl
estate = "my-estate"

record_store "ssm" {}

The same block goes inside live for the in-terraform form. The label picks the backend.

BackendWhere it writesArguments
localA directory beside the module, .tofu-records by defaultpath
ssmSSM Parameter Store, under a prefix derived from the estate namekey_prefix, region
s3An S3 bucket you already ownbucket (required), key_prefix, region

Four things to know first.

You are not meant to read it. The payload is a self-describing ctyjson envelope for this fork’s own code. Not an operator-facing artifact, and its format is not a contract.

Writes are conditional. A record is written only if it still carries the version the writer read. A losing writer gets a named failure, not a blocking wait and not a silent overwrite.

Losing a record is churn, not a lost estate. The effect re-runs or its value regenerates, and anything reading it plans as a change. It cannot cost you a resource, because identity arguments must be statically evaluable, so a record-backed value can never name one.

Secrets never go here. random_password, random_bytes and every tls_* are refused rather than recorded. Their output is key material, and a record holding a secret would be exactly the thing this design exists to avoid.

Receipts#

A receipt records whether an external effect ran, and with what input.

It is not choudoufu storage. It is an ordinary resource you declare, by convention an SSM parameter at /tofu-receipts/<estate>/<effect> holding a hash. It goes through the ordinary plan and apply cycle, and its diff appearing in a plan tells a reviewer or a CI gate that this apply will trigger something outside the resources being managed.

choudoufu does not write receipts. It lints them, enforcing that the value is a hash or constant and never a SecureString, that nothing references a receipt’s attributes, and that inputs name secrets by pointer rather than by value.

live/RECEIPTS.md has the pattern and the reasoning behind each guard.

Why receipts are not record-store entries#

Enforced rather than advised. A key_prefix whose first segment is tofu-receipts is a configuration error, so a record can never land in the receipts namespace.

Visibility is why. A receipt is AWS-native so its value stays readable with a plain aws ssm get-parameter, by someone with read-only IAM and no choudoufu binary. A record-store payload is tool-internal by design. Moving a receipt onto it would trade aws ssm get-parameter for choudoufu’s internal JSON envelope, strictly worse for the one artifact whose job is being legible to someone not running the tool.

The tempting mistake, now terraform_data is record-backed, is using its triggers_replace as a pseudo-receipt. Do not. It hides the fingerprint in the tool’s own store instead of a declared resource, and collapses a receipt into “did an input change”, with no existence flavour, no hash flavour, and no naming convention the lint rules recognise.

terraform_data is for the graph, ordering an apply, feeding replace_triggered_by, or standing in for a resource that does nothing. Receipts are for external effects. Keep them apart.

Choosing a record store backend#

local for a single operator or a demo, where a directory beside the module is fine and nothing else needs to read it.

ssm when more than one machine runs the estate. No infrastructure to set up, since Parameter Store already exists in the account.

s3 to keep records in a bucket you already operate, with your own versioning and lifecycle rules. You create and configure the bucket. choudoufu only reads and writes keys in it.