Secrets and the captured-evidence contract

August 26, 2026 · View on GitHub

Secret input values stay page-local

An input[type=password] field (or a field named with --secret <name>) becomes a secret parameter. Flow does not send its literal to Python. It masks the bound field region in saved frames. For every other piece of page text, Flow reports it exactly or withholds it and says why. Flow never rewrites captured text. Matching uses only the value a bound element holds at that moment, read live from the DOM; no value is kept after the field stops holding it. A shadow field whose identity can change must use a host with the same declared name or ID; Flow masks the complete host. It refuses an unbound shadow input before it accepts a value. At replay, Flow injects the secret from the environment and fails fast when it is absent:

openadapt-flow record --backend web --url https://your.app --out rec --secret password
export OPENADAPT_FLOW_SECRET_PASSWORD='…'                 # supplied at replay
openadapt-flow replay bundle --backend web --url https://your.app

Identity evidence and reflected evidence

Evidence splits in two. Identity evidence is the DOM selector, the control role, the accessible name, the clicked row's identity characters, and the receiving field's name. It is exact or withheld with a stated reason, because replay compares it against the live page and a rewritten copy would compare against text the page never showed. Reflected evidence is the page URL and the title. It is sampled from Python once the page has settled, never inside the capture-phase listener, which runs before the page's own handlers and so reads the previous action's text.

A URL is reduced by structure, not by rewriting

Within a document, a URL is reduced by structure: Flow reports the origin and the path, keeps every parameter name, and drops the value of any parameter named after a declared secret field — deterministically, whatever the value is. A dropped value becomes empty; Flow removes characters from a URL and never adds characters the page did not show. A single-page application that routes with history.pushState therefore keeps its URL evidence. If the URL Flow is about to report still holds a value Flow can see, it withholds the whole URL and warns you that the application put a secret in its own URL — a defect that exposes it through browser history, logs, proxies and Referer headers with or without Flow.

That reduction does not make a later document safe. A path segment has no parameter name to identify it, so a server that answers a form submit with a redirect to /results/<value> puts the value where structure cannot reach, and the new document holds nothing to match it against. Flow therefore withholds the URL and the title of every document after the one that first held a declared value. A title has no structure to reduce and follows the same rule within a document. meta.json records everything dropped and everything withheld, and the CLI prints it.

What this contract does not cover

This source-time contract does not track an application-defined transform of a secret or an application copy into an unrelated visible element, and it starts at the moment a bound field holds the value: text and pixels captured before then are ordinary recording evidence. Keep every raw recording inside its approved local boundary.

Related: PRIVACY.md for the PHI scrubbing map, phi_at_rest.md for bundle encryption, and RECEIPTS.md for the shareable-artifact allow-list.