CLAUDE WATERMARK REMOVER ยท PRACTICAL TEST
Why does fromCharCode turn hexadecimal 1F600 into a different character?
After our marked hex value is cleaned, fromCodePoint(0x1F600) produces ๐ while fromCharCode produces U+F600. Validate the parsed range and choose a code-point API. fromCodePoint also accepts surrogate values, so success alone does not enforce scalar-value policy.
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 hexadecimal value direct string / keep / invisible true | "1Fโ600"U+0031 U+0046 U+200B U+0036 U+0030 U+0030 | "1F600"Removed 1 invisible character | {"parsedHex":"NaN","fromCharCode":"\u0000","fromCodePoint":"RangeError"}{"parsedHex":"128512","fromCharCode":"๏","fromCodePoint":"๐"} |
| Valid astral hex direct string / keep / invisible true | "1F600"U+0031 U+0046 U+0036 U+0030 U+0030 | "1F600"No selected invisible characters found | {"parsedHex":"128512","fromCharCode":"๏","fromCodePoint":"๐"}{"parsedHex":"128512","fromCharCode":"๏","fromCodePoint":"๐"} |
| Above Unicode maximum direct string / keep / invisible true | "110000"U+0031 U+0031 U+0030 U+0030 U+0030 U+0030 | "110000"No selected invisible characters found | {"parsedHex":"1114112","fromCharCode":"\u0000","fromCodePoint":"RangeError"}{"parsedHex":"1114112","fromCharCode":"\u0000","fromCodePoint":"RangeError"} |
| Surrogate code point direct string / keep / invisible true | "D800"U+0044 U+0038 U+0030 U+0030 | "D800"No selected invisible characters found | {"parsedHex":"55296","fromCharCode":"\ud800","fromCodePoint":"\ud800"}{"parsedHex":"55296","fromCharCode":"\ud800","fromCodePoint":"\ud800"} |
A cleaned number can still use the wrong constructor
The first source spells hexadecimal 1F600 with U+200B interrupting the digits. Our observer prepends 0x and uses Number, which initially produces NaN. In that state fromCodePoint throws RangeError while fromCharCode returns NUL. After selected deletion, the numeric value is 128512. fromCodePoint now yields the intended emoji control, but fromCharCode yields the different U+F600 character. The plain hex reference shows the same constructor disagreement without any cleanup. We retain both strings rather than displaying only a successful numeric conversion, because the wrong constructor can accept a number and still generate the wrong sequence.
Range failure and surrogate acceptance are different checks
Hexadecimal 110000 is above the Unicode maximum used by fromCodePoint and triggers RangeError. The fromCharCode path produces NUL for that control. Hexadecimal D800 is accepted by both constructors and returns a lone surrogate. That result is why checking only for an exception is insufficient when an application requires Unicode scalar values. A caller with that contract must also exclude the surrogate range explicitly. We do not label the D800 output a recovered visible character or a valid emoji. JSON preserves its escaped representation, and the companion well-formedness guide explains how to inspect such a string.
Keep the numeric notation with a code-point report
An inspector label such as U+1F600 is a hexadecimal code-point notation, not decimal text and not an instruction to construct a single sixteen-bit unit. Parse only the documented format, validate the complete field and preserve the original notation beside any reconstructed string. Our observer inputs omit U+ and deliberately add 0x before conversion, making the radix unambiguous. An actual importer should reject unexpected prefixes or punctuation according to its own grammar. The cleaner does not parse code-point labels, validate ranges or choose constructors. It only removes the selected marker that interrupted one of our four authored hex fields.
This is reconstruction rather than rendering analysis
The measurements run locally in Node and involve no font selection, image generation, clipboard write or external lookup of a character. We do not test every out-of-range number, fractional argument or coercion type. The specific outputs expose a constructor boundary that matters when rebuilding text from an inspector or diagnostic log. They do not establish that every accepted integer represents a suitable application character. Retain exact values when reproducing the test, especially the escaped surrogate and NUL outputs. No string here came from a Claude conversation, and the experiment does not decode any watermark or attribute the source of an invisible character.
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 String.fromCodePoint 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.