CLAUDE WATERMARK REMOVER · PRACTICAL TEST
Why does filtering an empty JSON property name remove the entire export?
Our naive replacer rejects key empty on its first root call, so JSON.stringify returns undefined. A fresh per-call first-invocation guard preserves the root and removes only the actual empty-name property. Record call order before filtering.
Open the Claude text cleaner · Full measured data
Measured inputs and outputs
These are locally constructed test strings, not evidence that Claude inserts these characters. We executed the saved homepage script snapshot with a minimal DOM harness and compared exact strings. The invisible-checkbox state and dash mode appear in each row. The observations below test this page's specific question. Destination observations run independently on the exact input and cleaned strings in the recorded Node runtime. We make no detector-score or statistical-watermark removal claim.
| Fixture and mode | Input string and code points | Output and cleaner status | Observed before / after |
|---|---|---|---|
| Marked value direct string / keep / invisible true | "AB"U+0041 U+200B U+0042 | "AB"Removed 1 invisible character | {"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"AB\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]}{"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"AB\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]} |
| Plain value direct string / keep / invisible true | "AB"U+0041 U+0042 | "AB"No selected invisible characters found | {"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"AB\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]}{"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"AB\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]} |
| Empty value direct string / keep / invisible true | "" | ""No selected invisible characters found | {"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]}{"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]} |
| Retained joiner direct string / keep / invisible true | "AB"U+0041 U+200D U+0042 | "AB"No selected invisible characters found | {"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"AB\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]}{"naiveType":"undefined","naive":"undefined","naiveCalls":[""],"guarded":"{\"text\":\"AB\"}","calls":[{"key":"","type":"object"},{"key":"","type":"string"},{"key":"text","type":"string"}]} |
The first empty name belongs to the wrapper call
The observer serializes an object with two own fields: an empty property name and text, both carrying the supplied string. The naive replacer returns undefined whenever its key is empty. Its call log contains only one empty key, and the recorded result type is undefined. No child property was visited. The guarded comparison accepts its first invocation and then applies the empty-name filter. It produces an object containing text and records three invocations: root object, empty-name string member and text string member. The call sequence explains the missing export.
An empty value is a separate question from an empty key
The empty-value control still exports a text member holding an empty string under the guarded policy. It is the name being filtered, not the value length. Plain AB and the retained U+200D control use the same invocation order. In the marked fixture cleanup changes the carried string to AB, but the naive root rejection still returns undefined. We save both typeof and String of that result so the diagnostic is not confused with the literal JSON string containing the word undefined, or with a null value in a JSON document.
Reset any invocation state for each serialization
The first flag is created inside each observer call, so every independent serialization starts in the same state. Reusing a closure whose flag was consumed by an earlier export would be a different experiment and can select a different policy. For a real filter, define how the root should be handled before filtering child names and verify the complete output type. The guard in this page is limited to this plain object and this exact empty-name rule; it is not a universal serializer or a schema validator for arbitrary graphs.
A public question motivates an independent trace
Our research notes link a historical public question about the empty root key passed to a JSON replacer. This page supplies original call logs and a locally authored two-property object, without copying community answers. Earlier coverage uses an array whitelist to select members, whereas this example runs a function that can stop the entire export. No actual conversation is sent anywhere. The homepage callback only edits the supplied text. A serialization return value neither identifies a Claude response nor tests a statistical watermark or AI detector.
Reproduce this test
Save reproduce.cjs and tested-app.js in the same folder. Run the command below with Node.js. The harness prints its runtime, script SHA-256 and every measured row. Compare those rows with the original record. Using a newer script or runtime creates a new experiment; retain the version information with your rerun.
node reproduce.cjsReference and next check
ECMAScript JSON.stringify provides the relevant primary definition. The table and fixture analysis are original measurements. For broader inspection, use our Unicode inspector. Read the scope distinction before interpreting cleanup as a watermark result.