Every invisible Unicode character, and what it is for
A reference for every invisible Unicode character used to watermark or hide data in text: zero-width, tag characters, variation selectors, bidi controls.
Unicode contains a lot of characters that render as nothing. Most exist for good reasons. A few of them are also perfect for hiding data in plain sight.
This is a reference to all of them, what they are legitimately for, and how to tell the difference.
The zero-width family
The best known group, and the most commonly used for marking.
U+200B ZERO WIDTH SPACE (ZWSP). Marks a line-break opportunity without producing visible space. Legitimately used to let long URLs wrap. Invisible everywhere. It survives copy and paste, and it is the canonical choice for watermarking.
U+200C ZERO WIDTH NON-JOINER (ZWNJ). Prevents two characters from forming a ligature or joined form. This is orthography, not decoration. Persian requires it (میروم), as do several Indic scripts for conjunct control. Deleting it misspells words.
U+200D ZERO WIDTH JOINER (ZWJ). Forces joining. In emoji it is structural: 👨👩👧 is three people plus two joiners. Remove them and you get three separate emoji.
U+2060 WORD JOINER. Prevents a line break with no width and no break opportunity. The modern, correct character for the job U+FEFF used to do.
U+FEFF ZERO WIDTH NO-BREAK SPACE. The byte-order mark. Legitimate as the first character of a file; anywhere else it is leftover junk or a deliberate marker.
U+180E MONGOLIAN VOWEL SEPARATOR. Reclassified as a format character in Unicode 6.3 and now effectively zero-width.
Tag characters: the high-bandwidth channel
U+E0000 to U+E007F.
This block deserves its own section because it is qualitatively different. U+E0020 through U+E007E mirror printable ASCII exactly: U+E0041 corresponds to "A", U+E0061 to "a".
They were introduced for language tagging, then deprecated almost immediately, leaving behind a block of codepoints that map cleanly onto ASCII and render as absolutely nothing.
That means an entire readable message can be encoded character-for-character and hidden inside an ordinary sentence. Where a zero-width watermark typically encodes a few bits, tag characters carry a full byte each.
This has a name in security work, ASCII smuggling, and it matters beyond watermarking. Language models read the raw character stream, not the rendered glyphs, so an instruction hidden this way is invisible to the human reviewing a document and fully legible to the AI processing it. Hidden instructions in a CV, a support ticket or a web page can be read and acted on by an agent while the approver saw nothing.
The one legitimate use: subdivision flag emoji. 🏴 is a waving black flag followed by the tag characters for "gbsct" and a terminator. Which means a tool cannot simply delete every tag character. It has to parse the sequence structure and keep complete, well-formed flags.
Variation selectors
U+FE00–U+FE0F (VS1–VS16) and U+E0100–U+E01EF (VS17–VS256).
These select an alternative glyph for the character before them. U+FE0F is the one you meet most often. It forces emoji presentation, which is why ❤️ (with it) looks different from ❤ (without).
After a CJK ideograph they select a regional glyph variant and are meaningful. Free-floating, with no base character in front, they are pure payload space: 240 supplementary selectors is enough to encode arbitrary bytes, and this has been demonstrated as a covert channel.
Bidirectional controls
U+202A–U+202E (embeddings and overrides), U+2066–U+2069 (isolates), U+200E/U+200F (LRM/RLM), U+061C (Arabic letter mark).
These control how text is displayed relative to how it is stored. Entirely legitimate, and necessary in fact, in text that mixes right-to-left with left-to-right.
They are also the mechanism behind the "Trojan Source" class of attacks, where source code is made to display in an order different from how a compiler reads it. In plain English prose with no RTL content, they have no business being there.
Private-use areas
U+E000–U+F8FF, U+F0000–U+FFFFD, U+100000–U+10FFFD.
Unicode guarantees these will never be assigned standard meanings. Fonts can map them to custom glyphs, which is how icon fonts and Apple's logo character work.
In text intended to be portable, they have no defined meaning at all. If they are in your document, something put them there deliberately.
Space homoglyphs
Not invisible, but not the character your keyboard produces:
| Codepoint | Name | Where it comes from |
|---|---|---|
| U+00A0 | No-break space | Web pages, word processors |
| U+202F | Narrow no-break space | AI output, French typography |
| U+2009 | Thin space | Typesetting |
| U+2007 | Figure space | Numeric alignment |
| U+2003 | Em space | Typesetting |
| U+3000 | Ideographic space | CJK text |
| U+1680 | Ogham space mark | Ogham script |
These are the most common finding in any real document by a wide margin, and they are almost always innocent. Copying from a PDF or a web page brings them along. Treating them as evidence of watermarking would be overreaching, which is why a good checker separates them into their own confidence tier.
Others worth knowing
U+00AD SOFT HYPHEN. A conditional hyphen shown only at line breaks. PDF text extraction inserts these constantly.
U+034F COMBINING GRAPHEME JOINER. Almost never legitimately needed. Occasionally used to defeat naive text matching.
U+2061–U+2064 (function application, invisible times, invisible separator, invisible plus). Mathematical semantics for equation markup. In prose, contraband.
U+FFF9–U+FFFB. Interlinear annotation. Effectively never seen in normal text.
How to see them
Most word processors show none of this. Your options:
- A code editor. VS Code with "render whitespace" and "highlight non-ASCII characters" enabled will surface most of them.
- A hex editor. Definitive, tedious.
- A dedicated checker. Ours renders each one as a labelled chip in place so you can see the pattern rather than just the count. That matters, because deliberate marks tend to be evenly spaced while accidental ones cluster at line-break positions.
The rule for removal
Context, not category.
A zero-width joiner between two emoji is structural, so keep it. The same character between two Latin letters is contraband, so remove it. A tag character inside a complete flag sequence is part of the flag; the same character in a bare run is a payload.
Tools that run a regular expression over \p{Cf} and delete everything work fine on English and silently corrupt Persian, Hindi, Mongolian, Khmer, Korean, plus every emoji sequence more complex than a single smiley. That is a real cost, paid by users who will not notice until something is already wrong.
Check your own document
Free, no signup. It tells you plainly when there is nothing to find.
Check my text