When the tool shows you ***, never patch from the read
A display is not a file. That sounds obvious until you notice most automation never sees the file — it sees a rendering of it, and writes that back.
Two things make a rendering lie without ever being broken. One is masking, applied on display. The other is truncation, applied to every tool that pipes command output into a summary. Both leave the underlying file intact. Both produce an edit that is wrong the moment you trust them.
Masking replaces the value on the way out, not in the file
The fleet runs a secret guard whose whole job is to keep live values out of a
transcript. Its central hook, transform_tool_result, replaces every known value in
any tool result with a [REDACTED:…] marker naming the key, before that result
reaches the model, the session store or the logs.
The file on disk still contains the real value. The transcript contains a placeholder. Build an edit from the transcript — “replace the value I saw with the new one” — and the value you saw is the placeholder, so the placeholder is what gets written. The secret is now a broken literal in a secrets store, and nothing failed.
The same shape shows up in CI logs. This fleet’s runner honours ::add-mask:: —
probe run 1647 printed the literal as *** — and the deploy workflow pipes every
switch, rollback and journal command through a mask() filter that rewrites
AGE-SECRET-KEY-… to [redacted-age-key]. So *** is not a hypothetical; it is a
rendering this infrastructure produces, in two different places, on purpose.
Why the guard exists sets the stakes. An agent once ran sops -d
secrets/secrets.yaml “just to list the key names”, the command returned every
value, and all 48 live fleet secrets were in the conversation — already sent
off-host. Nothing undoes that, and the only remedy left was rotating everything.
Built-in redaction does not catch it either: it matches credential-shaped strings
(sk-…, AKIA…, high-entropy blobs), while sops values are opaque passphrases,
pre-shared keys, PEM bodies and plain hex.
Making the transcript safe is the guard’s job. It is not a licence to edit from the transcript, because the transcript is the thing that was falsified.
Truncation has the same failure with no mask involved
Command output is auto-truncated with the full text saved to a file, and large reads return a continuation offset instead of the rest. The tool tells you — there is a footer saying so. That footer is also the easiest thing in the output to scroll past.
An edit keyed on a truncated read has the same defect as one keyed on a masked value, one step further on: a replace-all against the portion you could see silently drops everything past the cutoff. Nothing errors. You replaced every occurrence you were shown.
The procedure that avoids both
- Re-read the exact region immediately before editing. Not the file you read five minutes ago and not the summary of it — the current lines, fresh.
- Key the edit on unique surrounding context, never on the value. Anchor on the neighbouring key name, the line above, the structure around it. The value is the one part of a secret’s entry you were never meant to hold; the key name is public.
- Change a secret by name reference inside the encrypted store, not by replacing
a literal. This is where sops helps, and where the
.sops.yamlcreation rules decide the outcome by path: the store’s rule ispath_regex: secrets/[^/]+\.(yaml|json|env|ini)$, with separate rules for thesecrets/age-escrow/*.ageidentities. Encrypt a temp file outsidesecrets/and no rule matches:sops --encryptproduces a plaintext file with no SOPS metadata, throws no error, and exits 0. - Verify the encryption, not the command. After any edit the key must read back
as ciphertext —
ENC[— not as a plaintext value. The reliable shape is decrypt intosecrets/, edit,sops --encrypt --in-placeon a file still insidesecrets/, verify, then replace. - Inventory by name only.
sops -d secrets/secrets.yaml | grep -oE '^[a-zA-Z0-9_.-]+:'prints the matched part, which is the key name — values never reach output. To answer “did this change / is it non-empty”, compare digests and counts (sha256sum,grep -c,wc -c), never values.
The review habit that catches it after the fact
Any diff containing ***, ..., or a run of dots in a value position was built from
a rendering, and should be rejected on sight. So should a diff where a secrets file’s
value position has lost its ENC[ envelope. Both are cheap to check now and
impossible to check later.
What you can check
- Whether your tooling masks on read. If a value arrives as
***or a bracketed name, that tool is telling you the file was not what you received. - Whether the file on disk still holds the real value. Read the name, not the value: a count or a digest comparison.
- Whether a recent diff touched a secrets file and contains a placeholder. That is a one-line search.
The layer underneath — where the encrypted store lives, who the recipients are, and how a host decrypts at activation — is on how the stack is built. The same discipline runs through the audit work: a claim you cannot inspect is a claim about intentions, which is why the audit page publishes signed artifacts rather than a status.