Zerowidth CleanerAll measured guides

CLAUDE WATERMARK REMOVER · PRACTICAL TEST

Why can a marker reappear when I export cleaned text as JSON?

Our authored toJSON hook appends a new U+200B during serialization. The plain cleaned object exports AB, while the hooked object exports AB plus that marker. The cleaner never sees the newly generated character.

Tested October 14, 2026 · v24.19.0 · 4 measured runs

Open the Claude text cleaner · Full measured data

Code-point comparison for Marked value

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.

Before and after observations; full code points and strings are in the JSON download.
Fixture and modeInput string and code pointsOutput and cleaner statusObserved before / after
Marked value
direct string / keep / invisible true
"A​B"
U+0041 U+200B U+0042
"AB"
Removed 1 invisible character
{"plain":"{\"value\":\"A​B\"}","hooked":"{\"exported\":\"A​B​\"}","nested":"{\"box\":{\"exported\":\"A​B​\"}}","hookKeys":["","box"],"sourceValue":"A​B"}
{"plain":"{\"value\":\"AB\"}","hooked":"{\"exported\":\"AB​\"}","nested":"{\"box\":{\"exported\":\"AB​\"}}","hookKeys":["","box"],"sourceValue":"AB"}
Plain value
direct string / keep / invisible true
"AB"
U+0041 U+0042
"AB"
No selected invisible characters found
{"plain":"{\"value\":\"AB\"}","hooked":"{\"exported\":\"AB​\"}","nested":"{\"box\":{\"exported\":\"AB​\"}}","hookKeys":["","box"],"sourceValue":"AB"}
{"plain":"{\"value\":\"AB\"}","hooked":"{\"exported\":\"AB​\"}","nested":"{\"box\":{\"exported\":\"AB​\"}}","hookKeys":["","box"],"sourceValue":"AB"}
Empty value
direct string / keep / invisible true
""
""
No selected invisible characters found
{"plain":"{\"value\":\"\"}","hooked":"{\"exported\":\"​\"}","nested":"{\"box\":{\"exported\":\"​\"}}","hookKeys":["","box"],"sourceValue":""}
{"plain":"{\"value\":\"\"}","hooked":"{\"exported\":\"​\"}","nested":"{\"box\":{\"exported\":\"​\"}}","hookKeys":["","box"],"sourceValue":""}
Retained joiner
direct string / keep / invisible true
"A‍B"
U+0041 U+200D U+0042
"A‍B"
No selected invisible characters found
{"plain":"{\"value\":\"A‍B\"}","hooked":"{\"exported\":\"A‍B​\"}","nested":"{\"box\":{\"exported\":\"A‍B​\"}}","hookKeys":["","box"],"sourceValue":"A‍B"}
{"plain":"{\"value\":\"A‍B\"}","hooked":"{\"exported\":\"A‍B​\"}","nested":"{\"box\":{\"exported\":\"A‍B​\"}}","hookKeys":["","box"],"sourceValue":"A‍B"}

The exporter deliberately creates a new character after cleaning

We build a fresh object with the tested string in value and an authored toJSON method. That method returns an exported property containing the value followed by a new U+200B. The marked input is cleaned to AB, and a plain object serializes that exact value. The hooked version instead produces AB followed by the appended marker. A nested export has the same added character under box. The ordinary AB control repeats the effect without any cleanup, and even the empty control becomes a one-marker exported value. This rules out an old marker somehow surviving deletion as the explanation. The method is deliberately present in our fixture; we did not discover it in a real Claude exporter or third-party application.

The hook chooses representation while the source stays unchanged

The observer saves the hook calls and the source value after serialization. Root export receives the empty key, and nested export receives box. Those calls happen on the fresh fixture object, while its original value property remains the supplied string. The destination creates a different representation without mutating that property. Our previous JSON text guide uses ordinary values and shows preserved serialization; our replacer guide filters property names. Neither demonstrates an object supplying its own exported form through toJSON. We do not test recursive hooks, network transport, class inheritance or an actual export plugin. Calling stringify again from a hook can introduce separate recursion behavior, but this observer returns an ordinary object and does not perform that unmeasured operation.

Inspect final export bytes as well as the cleaned source

If an export contains characters absent from the cleaned input, review the transformation stages that run afterward. Compare the plain source property, hook implementation and final serialized payload before blaming the cleanup counter. The retained-joiner control also separates a character the cleaner keeps from the newly appended selected marker. When you control the exporter, define its output contract and verify it with an exact payload check; do not rely solely on the earlier input status. Our downloads show every generated string and hook key with the actual saved cleaner script. They do not remove arbitrary application methods or certify an entire export pipeline. Preserve both representations so a later change can be traced to the stage that actually introduces it.

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.cjs

Reference and next check

ECMAScript JSON parsing and serialization 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.