A feint.yaml sitting next to your .tf files replaces the README page where
nobody finds the right command again. feint up reads it and brings the whole
environment up, feint down takes it back down. The colleague who clones the
repository gets the same emulator, the same client variables and the same
Terraform, without reconstituting a command line.
What you will get
By the end of this page, your repository will carry its own environment declaration: two commands will bring up an identical lab for everyone, and the emulator will refuse to start rather than deliver an environment different from the declared one.
- Write the minimal declaration that works.
- Read the six steps of
feint upand what each one establishes. - Express what must be true before the environment is called ready.
- Recognise what this file refuses to carry, and why that is a choice.
The problem: flags that live in shell history
feint serve --vm incus-ovn --contracts contracts is a command somebody types
once, half remembers, and types differently the following week. The options
that decide what a colleague's emulator can do, namely the runtime, the
provider, the contracts and the starting state, end up in a shell
history and in a README paragraph. A repository that needs a particular setup
then has no way to say so.
The declaration is that way. It makes the environment versioned alongside the infrastructure code it exists to test, and above all read by a command: a declaration nothing reads is a comment.
The shortest file that works
Three blocks are enough. Everything else has a default.
# feint.yaml, next to main.tfversion: 1
cloud: provider: scaleway
iac: engine: terraform directory: . vars: endpoint: ${feint.endpoint}The line to understand is the last one. Many configurations declare an
endpoint variable whose default points at 127.0.0.1:4599. Aimed at a
port where nothing listens, Terraform blocks up to its own ceiling, the plugin
dies, and the error message blames the provider. The ${feint.endpoint}
substitution is the only one this file knows: the address is written once,
and the engine receives it as TF_VAR_endpoint.
What feint up does, in order
The command runs six steps and says each one out loud. The order is not cosmetic: it puts every possible refusal before the first side effect.
-
Check what the host can deliver, and refuse before starting. A declaration naming
incus-ovnon a machine with no OVN is refused here, naming the missing half, rather than four minutes later half applied. -
Start the emulator with the address, the state and the contracts the file names. It is an ordinary
feint start, sofeint status,feint logsandfeint stopall know about it. -
Export the client environment from the pack itself, exactly the variables
feint env <provider>prints. -
Run the engine in the declared directory, in place, with its output passed straight through.
-
Wait for each
ready:condition, each with a deadline and each named while it waits. -
Print the endpoints and what proved them.
On the repository's examples/quickstart/scaleway, the end of the output looks
like this:
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:address = "203.0.113.2"- waiting: the emulator answers /instance/v1/zones/fr-par-1/servers ok: http:/instance/v1/zones/fr-par-1/servers- waiting: the emulator holds a instance/server ok: resource:instance/server:1
up: http://127.0.0.1:4599 clients: eval "$(feint env scaleway)" page: http://127.0.0.1:4599/_feint/ui proved: http:/instance/v1/zones/fr-par-1/servers, resource:instance/server:1 down: feint downThe proved: line is this command's kept promise. It does not say "it is
ready", it says what was checked in order to claim so, condition by
condition. The full first run, from configuration to destroy, is in Build
your first infrastructure.
The ready: conditions: what must be true to say "ready"
Four forms exist, and all are asserted against the emulator's API, never against Terraform's state file. The difference matters: a state file says what the engine believes it created, the emulator's inventory says what exists.
| Form | What it checks |
|---|---|
http:<path> | the emulator answers below 400 on that path |
tcp:<host>:<port> | a connection is accepted |
resource:<kind>[:<count>] | the inventory holds at least that many objects |
service:<resource name>:<port> | inside a machine, that port answers |
The last form is the newest, and it comes from a dated measurement. One night
in the summer of 2026, a stack's four conditions were all confirmed, then 310
milliseconds later the test suite found port 443 closed: cloud-init had not
reached the command that starts the service. up had not lied, it had
confirmed exactly what it was asked to confirm, and nothing in the file could ask
about the inside of a machine. The service: form closes that hole.
A condition in an unknown form is refused at load, with the list of valid forms:
feint: feint.yaml: line 8: `ready`: "ping:127.0.0.1": "ping" is not a readycondition; the forms are http:<path>, tcp:<host>:<port>,resource:<kind>[:<count>], service:<resource name>:<port>The schema refuses by name, never in silence
This is the property that makes the file usable in a team: nothing is accepted and then ignored. The schema is a closed table, and a key it does not know is rejected at load, with the list of keys the block actually takes:
feint: feint.yaml: line 7: unknown field `emulator.paquets`;`emulator` takes: addr, cleanup, contracts, env, log_level, stateA file that accepts everything and applies half of it is exactly the lie
this tool exists to avoid. A typo on contracts costs you one second here,
instead of half an hour wondering why responses are not being checked.
What the declaration will never carry
Two boundaries are held on purpose, and knowing them saves you looking for a field that will never exist.
| What it is | What describes it | Where it lives |
|---|---|---|
| How to bring the environment up | feint.yaml | next to your .tf files |
| What the infrastructure is | Terraform, OpenTofu | your repository |
| What the state currently is | a snapshot | an artefact |
The day this file grew a block describing a subnet, it would start rewriting
Terraform badly. The day it grew a packages: list, it would start rewriting a
development environment manager badly. Four fields are therefore
deliberately absent, including expose_to_network: putting an emulator that
accepts any credential on the network is a decision the person at the keyboard
makes, never a file they cloned.
Asking for less than the file declares
Three forms cover most daily needs, and each announces the difference rather than hiding it.
feint up # what the file asks forfeint up --runtime off # deliberately less, and the output says sofeint up --no-iac # the control plane only, without the engine--no-iac is the useful form when you want the emulator and nothing else,
for instance to drive the official CLI by hand. The consequence is stated
rather than hidden: the ready: conditions describe what the engine builds, so
they are skipped with it, and the summary then prints proved: nothing
instead of listing conditions nobody evaluated.
- not waiting: --no-iac skipped terraform, and the ready conditions describe what it builds (http:/v2/instance, resource:compute-instance:1, …)
up: http://127.0.0.1:4599 proved: nothingup never downgrades on its own. A declaration naming a runtime the host
cannot deliver is refused before anything starts, and the refusal carries the
ways out rather than only the wall: run the diagnosis, ask explicitly for less, or
change the file. Someone who believes their subnets are separate and finds out
otherwise in production is exactly the accident this refusal prevents.
The Exoscale stack runs, from provider 0.71.0
On the repository's Exoscale stack, feint up brings the whole environment up,
Terraform included. That was not always true, and knowing it saves you from
copying an outdated procedure found elsewhere.
cd examples/stacks/exoscale && feint upFrom 26 August to 5 September 2026, that same command stopped at the
doorstep: the published provider built two clients and only one honoured
EXOSCALE_API_ENDPOINT, so an apply or a destroy split between the emulator
and a paying account. A feint down run in that directory sent five signed
requests to Exoscale's servers.
The fix came from upstream, in version 0.71.0 of the provider, and the
refusal turned into a floor: the stack pins >= 0.71.0, and the emulator
refuses an older provider by its user agent, naming the version to pin.
There is no doorstep veto left for a declaration to hit. The full reasoning
is in What feint proves.
The fields you will use most
The exhaustive reference is generated from the schema itself in the repository. These are the ones covering almost every real declaration.
| Field | Role |
|---|---|
cloud.provider | the pack whose client environment is exported before the engine |
cloud.projects | the projects the emulated account holds, in order |
emulator.addr | the listen address, 127.0.0.1:4599 by default |
emulator.state | the JSON file the store is loaded from and persisted to |
emulator.contracts | the API descriptions every response is checked against |
runtime.mode | off, incus, incus-vm or incus-ovn, off by default |
runtime.images | the machine images this environment needs present |
snapshot.load | a known state loaded as soon as the emulator answers |
iac.engine | terraform or opentofu; absent, up brings the control plane up and stops |
ready | what must be true before the environment is called ready |
The default deserves a sentence: runtime.mode is off because starting
machines is a side effect this project asks for rather than assumes. That is
what lets the examples run on a CI runner with nothing installed. Moving to
real machines is covered in Run real
machines.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
unknown field at load | A key the schema does not carry, usually a typo | Read the list of keys the message prints |
| Terraform hangs then blames the provider | The endpoint points at a silent port | Declare endpoint: ${feint.endpoint} in iac.vars |
up refuses a runtime | The host cannot deliver the declared mode | feint doctor --vm <mode>, or feint up --runtime off |
proved: nothing in the summary | --no-iac skipped the engine and its conditions | Expected: run without the flag to evaluate the conditions |
A FEINT_* variable has no effect | It was exported after the start | Declare it in the emulator.env block |
down reports discarded resources | The emulator ran without emulator.state | feint snapshot save <name> before stopping |
Key takeaways
- A command reads the declaration: a file nothing reads stays a comment.
feint uprefuses before acting, which puts every possible failure before the first side effect.ready:conditions ask the emulator, never the engine's state file.- The schema is a closed table: an unknown key is rejected by name, with the list of valid ones.
- The declaration does not describe the infrastructure: that boundary with Terraform is held on purpose.
runtime.modedefaults tooff, because starting machines is asked for explicitly.