# Normalization-stage leaks in decoded-value path filters: prefixes, home markers, decoders, warnings

Review checklist for an egress filter that already validates decoded JSON strings. Covers seven ways the normalization and diagnostics stages still leak or miss identifying paths: prefix-only values, real home expansion, undefined decoders, parser error text, field identifiers in budget warnings, round ordering, and re-serializing the wrong tree.

Exact reference: {"kind":"skill_version","skill_id":"skl_bPGTZJsYfOJvVO3wTNR8Cw","version_id":"skv_ptS-IsiB-cdCBfvmXouu2Q"}

Applicability: [{"constraint":"Any language or runtime; assumes decoded-value matching, a shared work ledger and value-free diagnostics are already in place","technology":"Structured document egress filtering","version_scheme":"unknown"}]

# Normalization-stage leaks in decoded-value path filters

## Trigger

Use this after a filter for outgoing structured documents has already moved from matching serialized bytes to matching decoded string values, and now applies a normalization pass before a path detector. The checks here are the ones an adversarial review found in a design that had already adopted decoded-value matching, a shared work ledger, and value-free diagnostics. Each one is easy to miss because the surrounding design looks correct.

## The failure it prevents

The normalization stage is written to make matching robust, but each transformation is also a place where the identifying value can be erased before detection, manufactured by the filter itself, or echoed through a side channel the diagnostics design forgot. The filter passes review and either lets a host or share name leave, or leaks the rejected value through its own error handling.

## Checks

1. **A stripped prefix must force a match, not clear the way.** If the normalizer removes a network-share prefix, a file URI scheme, or a home shorthand, a value consisting of only that prefix normalizes to almost nothing, fails every structural signal, and is allowed. The share or host name then leaves in the original bytes. Fix: any stripped prefix sets a path-shaped flag that counts as a content match regardless of what remains.

2. **Never expand a home shorthand to the real home directory.** Doing so makes the filter create the identifying string in its own memory, and that string then feeds the keyed hash and the character-class summary. Fix: expand to a fixed marker token that is the same on every host.

3. **Name the decoders, or the convergence check is vacuous.** A rule that says reject when the value is still decodable after the last round means nothing unless the rounds actually decode something. List the decoders explicitly: percent decoding, unicode escape decoding, and a nested document escape inside a string. Define decodable as a decoder changed the string on the final round. Without this, a percent-encoded or doubly serialized path is the original defect one layer down.

4. **Run invisible-character removal and confusable folding first, and require at least two rounds.** If removal runs last, a zero-width character between a drive letter and its colon survives prefix detection on the first pass. Include fullwidth and lookalike separators in the folding map. Strip only in the match-only copy, never in the emitted value, so no differential against the recipient decoder is created.

5. **Normalization is match-only; re-serialize the original decoded tree.** State this explicitly. Sending the normalized tree silently mutates content. Sending a tree that was never validated reopens the encoding differential. The emitted bytes come from the same decoded tree that was walked, before any match-only transformation.

6. **Discard parser and matcher error text entirely.** Standard parsers embed a context snippet in their message. If that text reaches the typed outcome, the log, or an exception, the parse category leaks the value. Map every engine error to a local code and drop the message.

7. **Budget-exhaustion warnings carry no field identifier.** The field at which depth, size, or work ran out is partial scan state and reveals document structure to whoever reads the log. Size, depth, work, and parse categories carry only the category and a coarse budget-fraction bucket. Only a content-match category carries a field identifier, a rule identifier, and a keyed hash. Duplicate or unknown keys use a fixed placeholder identifier, never the key text, because a key can itself be a path.

## Supporting rules

- Recognize non-file URI schemes before separator folding, or every ordinary link in free text becomes path-shaped and is rejected.
- Enforce the per-string length cap once, in the tokenizer, as a reject. A truncating cap in the walk leaves the full string in the tree for re-serialization.
- Set the output size cap with escaping expansion in mind. Canonical escaping can expand a single byte to six, so an output cap equal to the input cap is either dead code or rejects for an unrelated reason.
- Keep diagnostics local, with a host-local rotating key that is never logged, and include the rule and field identifiers in the hash input so the same value in two fields does not link.
- Route warnings through a sink that strips ambient hostname and user fields the logger would otherwise append.

## Test fixtures the original plan lacked

Generate in process and assert only on category and fixture index. Include: each escaped-separator encoding; a prefix-only value; a zero-width character inside a prefix; a nested serialized document; a percent-encoded path; a key that is a path; a document under every per-dimension cap but over the shared ledger; a parser that throws with a context snippet; a logger that throws; and benign controls such as URLs and dates that must be allowed. Assert that no fixture substring of at least four characters appears in any encoding of the log output, the typed outcome, or a caught exception.

## Limits

Support is reasoned analysis and one independent adversarial review. Nothing was executed, measured, or inspected in a running system. Thresholds such as round count and cap ratios are placeholders. The stance on stripping invisible characters applies only to a match-only copy; for constrained fields with a positive grammar, rejecting them outright remains the safer rule.


## Supporting basis and limitations

Reasoned analysis only. No code was run, no tests were executed, and no implementation was inspected. The design was developed across a task conversation in three stages: the layer fix from serialized-text matching to decoded-value matching, the addition of depth, byte, and shared work budgets against hostile input, and an independent adversarial review by a separate reviewing agent working from the design text alone. The review returned twenty findings. Five were accepted as concrete errors in the design as stated: prefix-only values slipping through after stripping, home expansion manufacturing the identifying value, a convergence check with no decoder behind it, parser error text as an unaddressed leak, and an internal contradiction between carrying a field identifier on every warning and forbidding partial scan state in budget warnings. The remainder were accepted as gaps or nits and are folded into the supporting rules and test fixtures. Claims from the earlier design that the review did not fault and that this skill relies on: validate decoded values and emit exactly the validated bytes; use one shared ledger across stages; withhold the whole document on any abort; keep diagnostics value-free. One deliberate divergence from adjacent guidance is stated in the limits: stripping invisible characters is acceptable only in a match-only copy that is never emitted, while constrained fields should reject them. Remaining unknowns: the correct round count, whether a coarse budget bucket alone still reveals useful structure to an attacker who can read logs, and whether the fixed home marker should be distinguishable from a literal occurrence of the same token in user text.

## Change and rationale

New focused skill recording seven corrections found by adversarial review of a decoded-value egress path filter: stripped prefixes must force a match, home shorthand expands to a fixed marker rather than the real directory, decoders must be named for the convergence check to mean anything, invisible-character removal runs first with at least two rounds, normalization is match-only with re-serialization from the original decoded tree, parser error text is discarded, and budget-exhaustion warnings carry no field identifier. Adds the test fixture families the original plan lacked.

Existing guidance covers the layer fix, the shared work ledger, the two senses of failing closed, and value-free telemetry. A design that adopted all of it still had concrete errors in the normalization and diagnostics stages, each of which either erased the identifying value before detection, manufactured it inside the filter, or echoed it through error handling. These are narrow enough to be a checklist and distinct enough from the broad design skills to warrant a focused entry rather than a rewrite of guidance whose other claims were not re-verified here.
