Which invisible characters survive ordinary software
We ran 18 invisible characters through 12 transformations that real pipelines apply to text. The genuinely invisible characters survive almost everything. The ones that get cleaned up are the ones that at least look like a space. That is backwards from how most people assume text hygiene works, and it explains why these characters accumulate.
Published 2026-08-12Updated 2026-08-12Research/proposal
The finding in one line
Twelve of the eighteen characters came through every transformation that was not deliberately trying to remove them. That includes JSON, URLs, base64, line trimming, and canonical Unicode normalization. Nothing incidental touches them.
How many of 18 characters survive each transformation
Characters surviving, out of 18
Only the last three do anything decisive, and all three are destructive to ordinary text as well. The top two rows are the transformations text actually passes through in normal use, and they remove nothing.
Source:
Original measurement, AI Watermark Lab
Sample:
18 characters × 12 transformations = 216 tests
Method:
Each character is embedded in a two-line carrier string, the transformation is applied, and the exact code point is checked for afterwards. One character per test, so there are no interaction effects.
Date:
2026-08-12
Limitations:
Only transformations that can be executed and verified in code. It says nothing about Google Docs, Word, Notion, or any application this script cannot run. That is precisely why those are not claimed.
Why the invisible ones survive and the space-like ones don't
Every transformation that removed anything did so for the same underlying reason: it recognised the character as a kind of space. NFKC maps no-break space, thin space, and ideographic space onto an ordinary space, because for compatibility purposes they are one. JavaScript's \s matches the same set plus the byte-order mark. That is why the BOM is the one truly invisible character that a whitespace collapse destroys.
A zero-width space is not a space to any of these rules. It has no width, no compatibility decomposition to anything, and it is not whitespace. So it passes through untouched, and so do the joiners, the bidirectional controls, the soft hyphen, the variation selector, and the tag character.
The practical consequence is the opposite of the intuition. Text that has been through several systems is more likely to carry zero-width characters and less likely to carry unusual spaces, because the ordinary cleanup steps quietly remove one class and never see the other. Finding a zero-width space in a document tells you almost nothing about where it came from. In any case, our census of 96 Claude outputs found none at all, so it did not come from there.
Every transformation, and what it destroyed
Survivors / destroyed, out of 18 characters. 2026-08-12.
Transformation
Survive / destroyed
Where you meet it
Unicode NFC normalization
18 / 0
Default for storage and comparison in many systems
Unicode NFD normalization
18 / 0
Canonical decomposition; used by some filesystems
JSON round-trip
18 / 0
Every API call, config file, and log line
URI encode and decode
18 / 0
Query strings, forms, anything travelling in a URL
The most common 'tidy this up' regex in production
Strip Unicode format chars (\p{Cf})
7 / 11
The targeted removal, which leaves real text alone
Latin-1 storage (lossy)
2 / 16
A legacy column that keeps only each character's low byte
Strip non-ASCII
0 / 18
The naive sanitizer; also destroys every non-Latin script
Every character, and how much it survives
Sorted by durability. The split down the last column is the whole result: no space-like character reaches the top of the table, and no truly invisible character reaches the bottom except the byte-order mark, which JavaScript treats as whitespace.
Transformations survived, out of 12. 2026-08-12.
Character
Survives
Class
U+00AD soft hyphen
10
Truly invisible
U+034F combining grapheme joiner
10
Truly invisible
U+FE0F variation selector 16
10
Truly invisible
U+200B zero width space
9
Truly invisible
U+200C zero width non-joiner
9
Truly invisible
U+200D zero width joiner
9
Truly invisible
U+2060 word joiner
9
Truly invisible
U+061C Arabic letter mark
9
Truly invisible
U+200E left-to-right mark
9
Truly invisible
U+202D left-to-right override
9
Truly invisible
U+2066 left-to-right isolate
9
Truly invisible
U+E0041 tag character
9
Truly invisible
U+1680 Ogham space mark
9
Space-like
U+FEFF byte-order mark
8
Truly invisible
U+00A0 no-break space
8
Space-like
U+202F narrow no-break space
7
Space-like
U+2009 thin space
7
Space-like
U+3000 ideographic space
7
Space-like
Where this study stops
Only transformations that run in code. The question people actually ask, does a mark survive Google Docs, Word, or Notion? That cannot be answered here, so it is not answered here.
Survival means the exact code point is still present. It does not mean the surrounding text is unchanged: NFKC and a whitespace collapse both alter plenty besides.
One character per test. Real text carries combinations, and some transformations behave differently on a sequence than on a single character.
This says nothing about statistical text watermarking, which lives in token choice rather than in characters and cannot be observed by any of these methods.
Data and code
matrix.json (full results, per character and per transformation, with the notes).
matrix.csv (18 rows, one column per transformation).
survive.mjs is the script. Dependency-free; add a transformation and re-run.
Reuse is welcome with attribution and a link back. If you add a transformation we should have included, that is worth telling us. See the corrections policy.