0003 — Secrets are redacted structurally at the recorder, and content types are not trusted¶
Status: Accepted · Date: 2026-09-11
Context¶
scout's output is meant to be shared: a report handed to the team that runs the server, a HAR file attached to a bug. It therefore holds the one thing that must never travel — the credentials the run was made with, and the tokens the run obtained.
Redacting at the print site is the obvious approach and the wrong one. There are many print sites (text, Markdown, JSON, NDJSON, HAR, the stderr banner) and one more is always being added. And the secrets are not all known up front: a client-credentials run receives an access token from the token endpoint, a login receives a refresh token, dynamic registration receives a client secret. A redactor that only knows what the operator typed misses all of those.
The first test that serialised the telemetry after a full run found both kinds of leak: the dynamically registered client secret, and every issued access token, each sitting in a captured response body.
Decision¶
Redaction lives in one place, telemetry.Redactor, and is applied when
an event is recorded, before any renderer sees it. It works in three
layers:
- By name.
Authorization,Cookie,Set-Cookieand any header whose name containskey,tokenorsecretare masked whatever their value (the scheme word,BearerorBasic, is kept so the report shows what was attempted). Query and form parameters namedcode,state,access_token,refresh_token,client_secret,code_verifier,token,api_key,passwordare masked. - Structurally. A JSON body is parsed and the keys
access_token,refresh_token,id_token,client_secret,registration_access_token,code,code_verifier,password,api_keyandsecretare replaced, recursively. Their values are registered as secrets at the same time, so from that event onward they are masked wherever they appear — in a later body, in a URL, in an error string. - By value. Every registered secret (operator-supplied ones before the first request; discovered ones as they arrive) is replaced wherever it occurs, longest first, in raw and URL-escaped form.
Content types are not trusted. A token endpoint that answers
text/plain still carries an access_token. The recorder decides
whether a body is JSON by looking at its first non-space byte, not at the
Content-Type header. Bodies are buffered for this purpose even when
--capture-bodies is off, so secret registration does not depend on
whether the operator asked to keep bodies.
Consequences¶
Every renderer is safe by construction, and a new one cannot leak what
it never receives. The test that matters is a whole-run assertion
(TestFullRunClientCredentials): serialise every event and grep for
every secret the fake server ever minted.
The cost is a JSON parse of every response body, capped at BodyCap,
and a redactor that must be reachable from the recorder — Recorder.New
creates one so a recorder without a redactor cannot exist.
What would make this wrong¶
A secret that arrives in a shape none of the three layers recognise: a
binary token in a custom header whose name contains none of the trigger
words, or a JSON key a server invents. The by-name lists are the weak
point and are expected to grow. A server that returns a credential in a
place scout does not look is, for now, an accepted gap; the mitigation
is that --capture-bodies is off by default, so the unrecognised body
is never written to a report unless the operator asks.