> ## Documentation Index
> Fetch the complete documentation index at: https://sponson.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR 0018: Once-only outputs resolve to { keep: true } after the run that revealed them

> Architecture decision record 0018. Status: Proposed. Introduced with the planetscale adapter; amended (see "Amendment: proof by fingerprint, and

Date: 2026-10-11

## Status

Proposed. Introduced with the `planetscale` adapter; amended (see "Amendment: proof by fingerprint, and
`--recreate`" below) before release.

## Context

Some providers reveal a value only in the answer that creates a resource. PlanetScale returns a branch password's
plaintext in the create answer and `null` everywhere else; the same holds for Stripe webhook signing secrets and many
API keys ([docs/coverage.md](/reference/coverage), "Once-only outputs").

Outputs flow between lines through the engine: an unchanged line's outputs come from its `read`, and sensitive
outputs are never written to receipts ([ADR 0009](/contributing/adr/0009-single-point-redaction)), so nothing carries them from one
run to the next. A `from:` reference to an output the producing line no longer has stays a pending marker. Before this
decision that had two bad outcomes for a password referenced by a Vercel variable: the second apply of an unchanged
plan reached `apply` with a pending marker (`INTERNAL`), and a plan showed the dependent as pending for ever.

The tempting fixes are both wrong. Storing the plaintext somewhere (receipts, a side file) breaks invariant 1.
Re-creating or renewing the password whenever a later run needs the value rotates a credential that running
deployments may still use, behind everyone's back.

## Decision

`OutputSpec` gets `once?: boolean`: the provider reveals the value only when the resource is created. An op declares
it together with `sensitive`.

* In the run that creates the resource, a `once` output is an ordinary output: dependent lines receive it.
* When the producing line's resource already exists and the run did not reveal the output (in apply: always after the
  line is processed; in plan: when its diff creates nothing), a `from:` reference to the output resolves to
  `{ keep: true }` ([ADR 0007](/contributing/adr/0007-keep-true-for-adoption)), recorded as input state `kept` with the reference.
  A dependent that already holds the value is unchanged, so re-applying an unchanged plan writes nothing.
* A dependent that has nothing to keep (its adapter rejects the keep marker with `PARAM_INVALID`, as `diffValue`
  does) would need the value itself. The line is refused with the new code `OUTPUT_UNAVAILABLE`, before anything is
  written, and the message says how to get a new value: delete the producing resource in the provider (missing drift,
  re-created by the next apply, which then passes the new value on) or rename it in the plan.
* Nothing in the engine or the adapters re-creates a resource to get a once-only value back.

## Consequences

* Dependents must be in the same plan and applied in the same run as the resource they read a once-only output from.
  A line added later that references it is refused until the resource is deliberately re-created.
* A console edit of a dependent that holds a once-only value is not detected: the value is kept, whatever it is.
  Sponson cannot know the value, so it cannot compare it.
* If a run creates the resource and then dies before a dependent is written, the value is lost; the next run refuses
  the dependent with `OUTPUT_UNAVAILABLE` and the user re-creates the resource. A rollback after a failed line
  deletes what the run created, so the usual failure leaves nothing half-done.
* Adding the field is backwards compatible: ops that do not declare it behave exactly as before.

## Amendment: proof by fingerprint, and `--recreate`

Resolving every later reference to `{ keep: true }` trusts the dependent blindly. `{ keep: true }` means "whatever is
live", which is right for adopted values but wrong here in four cases, each of which the first version accepted
silently:

* **a dependent added after the producer was created** (a second variable reading the same password): if it happens
  to exist already, it is kept with whatever it holds, which was never this password;
* **an adopted producer** (the password existed before Sponson managed it): no dependent ever received its value
  from Sponson;
* **a dependent edited in the console** since it was written: its hash changed, but `{ keep: true }` keeps the edit,
  even under `--reconcile`;
* **a dependent that must be (re)created**: caught only when its adapter happens to reject the keep marker with
  `PARAM_INVALID`.

Decision:

* **Fingerprints, never values.** When a line's write returns a `once` output, the engine records a keyed
  fingerprint of it (`ledger.keyed(sha256(value))`, the scope's HMAC key, as for secret fingerprints in
  [ADR 0014](/contributing/adr/0014-secrets-are-references)) on the producer's ledger entries, `onceFingerprints: { <output> }`.
  When a dependent is written with the value, its entries record `onceInputs: { "<line>.<output>": <fingerprint> }`.
  Runs that cannot see the value carry both forward; a create starts a new fingerprint.
* **Keep only what is proven.** In a later run, a reference resolves to `{ keep: true }` only when every resource of
  the dependent carries the producer's current fingerprint (`holdsCurrentValue` in `packages/core/src/engine/once.ts`);
  otherwise it stays pending and the line is refused with `OUTPUT_UNAVAILABLE`. The line is also refused when its
  live hash moved since Sponson wrote it (edited outside Sponson: keeping would keep the edit), when its resource does
  not exist, when its diff would create a resource, or when its adapter rejects the keep marker. Nothing is written.
* **`--recreate <line>`** (`RunOptions.recreate`, a flag of `apply`, so the three commands of
  [ADR 0002](/contributing/adr/0002-three-commands) stay three; the MCP `sponson_apply` tool takes `recreate` as a comma-separated
  string) deletes the named line's resources and creates them again in the same run, so the new value reaches every
  dependent. It refuses (`USAGE`) to delete anything Sponson did not create in this scope (adopted or another scope's
  resources), and names only lines active in the environment. A recreated resource replaces one that existed before
  the run, so rollback does not remove it (invariant 2 holds by resource identity); the line notes
  `recreated: true`, and the deletion is not reported as `missing` drift. `plan` with the same option shows the line
  as `create` and its dependents as pending. Recreating rotates a credential running deployments may still use: it is
  only ever done when a human asks.
* **Error message and hint.** `OUTPUT_UNAVAILABLE` says why the line cannot keep the value and lists the ways out:
  recreate the producer, delete it in the provider, rename it, or keep the value in a secret manager and reference
  it with `{ secret: … }`. The CLI hint names `--recreate`.
* **Not done: persisting the value to a secret sink.** An opt-in "write the revealed value to a configured secret
  manager" (Doppler, 1Password, AWS Secrets Manager) would let later runs and new dependents read it by
  `{ secret: … }`. It needs write access to the secret manager from CI and a new interface next to `SecretSource`
  (`SecretSink.put(ref, value)`), and makes Sponson a writer of secrets, which ADR 0014 avoids. Left for its own
  record; the refusal message already points at the manual equivalent.

Consequences of the amendment:

* The four cases above are refused instead of silently kept. Plans with a dependent written in the same run as its
  producer behave exactly as before; the PlanetScale scenarios needed no change of expectations (their only new
  step uses `--recreate` as the remedy).
* Ledgers written by the first version have no fingerprints, so their dependents are refused once; the first
  version was never released.
* A pull request scope that inherits its head branch's ledger (succession) cannot compare fingerprints keyed with the
  other scope's key: its dependents are refused until recreated. Rare (an agent applied locally before the pull
  request existed), and safe.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.