Written 6 August 2026
Invisible characters in AI text
Paste an answer from ChatGPT into a spreadsheet, a code editor, or a form validator, and it sometimes fails in a way that makes no sense: the text looks correct, the comparison says it is not. The cause is usually a handful of Unicode code points that occupy no visual space. Here is what is actually in there, where it comes from, and why the popular explanation is wrong.
Seeing them at all
The fastest check needs no tools. Copy a paragraph of AI output, then run it through a hex dump:
$ pbpaste | hexdump -C | grep -E 'c2 a0|e2 80 8b|ef bb bf'
00000040 61 6e 64 c2 a0 74 68 65 6e 20 74 68 65 20 6d 6f |and..then the mo|Those two bytes, c2 a0, are a non-breaking space wearing the costume of a normal one. In a text editor the line reads and then the mo with nothing out of place. To grep, to a CSV parser, and to a string equality check, that word boundary is a different character than the one you typed.
What tends to be in there
Not every invisible character is equally likely, and they are not equally harmful. These are the ones worth knowing by sight:
| Code point | Name | Why it matters |
|---|---|---|
| U+00A0 | Non-breaking spaceBy far the most common. Comes from HTML rendering, not the model. | By far the most common. Comes from HTML rendering, not the model. |
| U+202F | Narrow no-break spaceAppears around punctuation, learned from French-typography training data. | Appears around punctuation, learned from French-typography training data. |
| U+200B | Zero-width spaceGenuinely zero width. Survives copy-paste and breaks exact-match search. | Genuinely zero width. Survives copy-paste and breaks exact-match search. |
| U+200D | Zero-width joinerLoad-bearing inside emoji. Removing it splits π¨βπ©βπ§ into three people. | Load-bearing inside emoji. Removing it splits π¨βπ©βπ§ into three people. |
| U+FEFF | Byte order markUsually a file artifact. At the start of a CSV it corrupts the first header. | Usually a file artifact. At the start of a CSV it corrupts the first header. |
| U+00AD | Soft hyphenInvisible until a line wraps, then it prints as a hyphen. | Invisible until a line wraps, then it prints as a hyphen. |
| U+2009 | Thin spaceOne of nine fixed-width spaces that are not U+0020. | One of nine fixed-width spaces that are not U+0020. |
| U+202E | Right-to-left overrideReverses display order. The basis of the Trojan Source attack. | Reverses display order. The basis of the Trojan Source attack. |
| U+FE0F | Variation selector-16Forces emoji rendering. Also a known channel for hidden payloads. | Forces emoji rendering. Also a known channel for hidden payloads. |
| U+E0041 | Unicode tag characterA deprecated block that can encode an entire hidden ASCII message. | A deprecated block that can encode an entire hidden ASCII message. |
Where they actually come from
The common explanation is that the model inserts them as a watermark. That claim is durable, widely repeated, and almost certainly wrong.
It fails an easy test. Ask a model the same question twice and diff the invisible characters in both answers. A watermark has to survive light editing and has to encode something, so it would need a stable, recoverable structure. What you find instead is non-breaking spaces clustered around line wraps and punctuation, in positions that change between runs. That is not a signal. That is typography.
The real sources are duller, and knowing them tells you how to avoid the problem:
- The copy path, not the model. Chat interfaces render answers as HTML. When you select and copy, you are copying rendered markup, and the browser hands the clipboard whatever the layout used for spacing. That is where the bulk of
U+00A0comes from. The same text pulled from an API response is usually clean. - Training data. Models learn punctuation spacing from text that included it. French typography puts a narrow no-break space before a colon or a question mark, so
U+202Fshows up in output that has nothing to do with French. - Upstream documents. Soft hyphens and byte order marks come from PDFs and exported files. If you pasted a source document into the prompt, the model may reproduce what it was given.
- Emoji. Zero-width joiners and variation selectors are how multi-person and skin-tone emoji are assembled. They are supposed to be there.
What breaks
The failures share a shape: something compares two strings that look identical and concludes they are different.
- Spreadsheet imports. A byte order mark at the start of a CSV attaches itself to the first column header, so a lookup for
idmisses a column displayed asid. - Deduplication. Two rows that differ only by a zero-width space are distinct keys. A unique constraint accepts both, and the duplicate is invisible in every report.
- Search. Exact-match search fails on a word that contains a zero-width character. So does find-and-replace, which is why manual cleanup tends to miss it.
- Code. A non-breaking space in indentation is a syntax error in Python and a silent style violation nearly everywhere else. The error message points at a line that looks fine.
- Form validation. A trailing non-breaking space in an email field passes a naive trim, which only strips
U+0020, and then fails the regex.
The part that is a security problem
Two categories in that table are not typography. They are attack surface.
Directional overrides
Bidirectional control characters tell a renderer to display text in a different order than it is stored. Source code is stored as one sequence and displayed as another, which means a reviewer and a compiler can disagree about what a program does. This was published in 2021 as Trojan Source and assigned CVE-2021-42574; compilers and code hosting platforms now warn on these characters, but plain text pipelines generally do not.
Tag characters
The block from U+E0000 to U+E007F mirrors printable ASCII. It was deprecated for its original purpose, renders as nothing, and survives most copy operations intact. An arbitrary ASCII string can be encoded into it and hidden inside ordinary-looking text.
This matters specifically because of AI. If a model reads a document that carries a hidden instruction in tag characters, a human reviewing that document sees nothing unusual, while the model receives text it may treat as an instruction. That is a prompt injection channel that passes visual review. Any pipeline that feeds user-supplied text to a model should be stripping this range before the text ever reaches a prompt.
When stripping is the wrong move
Most advice on this topic stops at delete everything invisible. That advice will corrupt valid content.
- Emoji sequences. Remove the zero-width joiners from a family emoji and you get three separate people. Remove
U+FE0Fand some emoji revert to a monochrome text glyph. - Right-to-left text. Arabic and Hebrew mixed with Latin script or numbers rely on directional marks to display correctly. Stripping them mangles the text for the people who can actually read it.
- Indic and Persian script. Zero-width non-joiners are orthographically required in Persian and several Indic scripts. They are not decoration.
A defensible default is to delete the characters that carry no meaning in your content, normalize the fixed-width spaces to a plain U+0020, and leave the rest alone. That is the distinction between delete and normalize in the table above, and it is why a blanket regex is not the right instrument.
Practical detection
Ordered by how quickly they answer the question:
- Hex dump.
hexdump -Corxxd. Definitive, because it shows bytes rather than glyphs. - Editor settings. VS Code flags invisible and ambiguous characters in source files by default and can render whitespace explicitly.
- A Unicode-aware regex. In JavaScript,
/[\u200B-\u200F\u2060\uFEFF\u00AD]/gucatches the common zero-width set, though a complete rule also needs the fixed-width space range\u2000-\u200Aand the tag block\u{E0000}-\u{E007F}. - A visual scan. The invisible character tool on this site shows each code point in place with a count, so you can see what is there before deciding what to remove. It runs in the browser, which matters if the text is something you would not paste into a server you do not control.
The short version
The invisible characters in AI output are mostly non-breaking spaces picked up from the browser during copy, not a watermark. They are harmless to read and reliably destructive to anything that compares strings. A small subset, the directional overrides and the tag character block, is a genuine security concern and deserves stripping on principle when text crosses a trust boundary. And a different small subset is load-bearing, so the correct operation is rarely delete everything.
If the text came from a chat interface and is headed somewhere strict, checking it takes a few seconds and saves a debugging session that starts with the words but it looks fine.
Frequently asked questions
Does ChatGPT add invisible characters to watermark its output?
There is no public evidence that any major model inserts zero-width characters as a watermark, and the claim does not survive a simple test: regenerate the same answer twice and the invisible characters do not form a stable pattern. The characters people find are almost always non-breaking spaces introduced by the browser when you copy from the chat interface.
Why does my text look fine but fail an exact-match search?
A zero-width space or a non-breaking space inside a word makes it a different string at the byte level. The rendered text is identical, so the mismatch is invisible until you compare code points rather than glyphs.
Is it always safe to strip invisible characters?
No. Zero-width joiners are structural inside emoji sequences, and directional marks are required for correct display of Arabic and Hebrew text mixed with Latin script. Removing them corrupts content that was never broken. Blind stripping is only safe for Latin-script text with no emoji.
How can I see invisible characters without a tool?
On the command line, pipe the text through a hex dump and look for byte sequences like c2 a0 for a non-breaking space or e2 80 8b for a zero-width space. In an editor, enable rendering of whitespace and control characters; VS Code also flags ambiguous and invisible characters in source files by default.
Do invisible characters affect how AI models read text?
Yes, they consume tokens and can split a word into several tokens, which slightly changes how the model processes it. The effect on output quality is usually negligible, but the token cost is real in long documents.
Related tools
Invisible Character Remover
Detect and delete zero-width spaces, non-breaking spaces, and other hidden Unicode characters.
AI Text Cleaner
Remove em dashes, curly quotes, Markdown symbols, and hidden characters from ChatGPT and other AI text.
Fancy Text to Normal Converter
Convert π―πΌπΉπ±, ππ‘ππππ, and βββ‘ββββ Unicode lettering back into plain, searchable text.
Text Anonymizer
Mask emails, phone numbers, card numbers, and API keys in text before you share it or paste it into an AI.