What this tool does
The tool reads your text one Unicode code point at a time and reports: zero-width characters (U+200B zero width space, U+200C non-joiner, U+200D joiner, U+2060 word joiner, U+FEFF byte-order mark, U+180E Mongolian vowel separator); non-breaking and typographic spaces (U+00A0, U+202F, U+2000–U+200A, U+205F, U+3000, U+1680); the soft hyphen U+00AD; bidirectional controls (U+200E, U+200F, U+061C, U+202A–U+202E, U+2066–U+2069), including ones left open at the end of a line, the pattern behind the Trojan Source attack (CVE-2021-42574); line and paragraph separators U+2028 and U+2029; control characters (C0 except tab, line feed and carriage return, DEL and C1); variation selectors and Unicode tag characters, decoding any text hidden in them; private-use characters, unpaired surrogates and noncharacters; blank-looking letters such as U+3164 Hangul filler; and, as warnings, Cyrillic or Greek letters that imitate Latin ones inside a word. Smart quotes, en and em dashes and ellipses are flagged only when you turn that check on. Legitimate uses are not reported as problems: a zero width joiner inside an emoji such as 👨👩👧, U+FE0F after an emoji, flag emoji built from tag characters, joiners between Persian or Indic letters, zero width spaces between Thai words, direction marks in right-to-left text and a single byte-order mark at the start. Positions are line:column, where columns count code points from 1, so an emoji counts as one column. The cleaned copy can remove invisible characters, turn special spaces into ordinary spaces, remove bidi controls, straighten smart punctuation, and apply NFC or NFKC normalisation.
Example input and output
{"status": "active", "price": 42}1:19 U+200B ZERO WIDTH SPACE (ZWSP): Problem
1:31 U+00A0 NO-BREAK SPACE (NBSP): Check
Marked: {"status": "active⟦ZWSP⟧", "price":⟦NBSP⟧42}
Cleaned: {"status": "active", "price": 42}The input looks correct, but JSON.parse rejects it (Unexpected token ' ') because a no-break space is not JSON whitespace. With that fixed, status === "active" would still be false: the value has seven characters.
if (role != "user // admin only ") {1:18 U+202E RIGHT-TO-LEFT OVERRIDE (RLO): Problem — Not closed before the end of the line, so it reorders everything after it on that line: the pattern Trojan Source relies on.
1:20 U+2066 LEFT-TO-RIGHT ISOLATE (LRI): Problem
1:34 U+2069 POP DIRECTIONAL ISOLATE (PDI): Problem
1:36 U+2066 LEFT-TO-RIGHT ISOLATE (LRI): Problem — Not closed before the end of the line, so it reorders everything after it on that line: the pattern Trojan Source relies on.
Marked: if (role != "user⟦RLO⟧ ⟦LRI⟧// admin only⟦PDI⟧ ⟦LRI⟧") {
Cleaned: if (role != "user // admin only ") {In an editor the line appears to end in a comment, but the compiler sees one long string. The cleaned line shows what the code really compares.
curl -H “Accept: application/json” —silent https://api.example.com/v1/items1:9 U+201C LEFT DOUBLE QUOTATION MARK (“): Check
1:34 U+201D RIGHT DOUBLE QUOTATION MARK (”): Check
1:36 U+2014 EM DASH (—): Check
Cleaned: curl -H "Accept: application/json" --silent https://api.example.com/v1/itemsWord processors and chat apps turn " into curly quotes and -- into an em dash. The shell then passes the curly quotes as part of the header and treats —silent as a URL.
Common errors and fixes
- SyntaxError: Invalid or unexpected token
- JavaScript's error for a stray zero width space or another invisible format character in code, usually pasted from a web page, chat or a PDF. Paste the line here, find the ⟦ZWSP⟧ badge and remove it. A zero width joiner or non-joiner inside a name gives no error at all: it silently makes a different variable.
- SyntaxError: invalid non-printable character U+00A0
- Python's message for a no-break space (or U+200B and similar) in source code. Copying code from a web page or typing Option+Space on a Mac adds it. Clean with Special spaces → space ticked, or replace it in your editor.
- Unexpected token '', "{"a":1}" is not valid JSON
- The text starts with a byte-order mark, often from a file saved by a Windows editor as “UTF-8 with BOM”. It shows here as ⟦BOM⟧ at 1:1. Clean removes it; in code, strip it with text.replace(/^\uFEFF/, "") or save the file as UTF-8 without BOM.
- Two strings look identical but === (or a database lookup) says they differ
- One of them contains an invisible character, a non-breaking space, a look-alike letter (Cyrillic а instead of Latin a) or a different Unicode form of an accented letter. Paste both here: the first three are listed with their positions, and NFC normalisation makes é typed two ways compare equal.
- This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below
- GitHub's warning for files with bidi control characters. In right-to-left text they can be legitimate; in code they can hide what really runs (Trojan Source). Paste the file here: controls left open at the end of a line are marked Problem, and Remove bidi controls takes them out.
- invalid byte sequence for encoding "UTF8": 0x00
- PostgreSQL rejects the NUL character (U+0000) in text. It usually comes from binary data or a broken export. The tool shows it as ⟦NUL⟧; clean the data before importing it.
Common use cases
- Finding why copied code fails to compile or a string comparison fails
- Checking code and pull requests for Trojan Source bidi characters
- Revealing text hidden in Unicode tag characters before pasting it into an AI prompt
- Cleaning text copied from Word, PDFs, web pages and chat apps before it goes into data or code
- Spotting spoofed names and links that mix Cyrillic or Greek letters into Latin words
Privacy and security notes
- Your text is scanned and cleaned by JavaScript in this browser tab. It is not sent to DevPrix or stored on a server.
- The marked view shows each finding as a text badge built by the page, and your text is always displayed as text, never as HTML, so pasted markup cannot run.
- Share copies a link that carries your input after the # in the URL. Browsers do not send that part to servers, but anyone you give the link to can read it, so do not share links that contain credentials or personal data.
Frequently asked questions
Why isn't the zero width joiner in my emoji flagged?
Many emoji are several emoji joined by U+200D: 👨👩👧 is man, ZWJ, woman, ZWJ, girl. Removing the joiners would break them into separate pictures, so a joiner between two emoji is left alone. The same goes for U+FE0F after an emoji (it asks for the colourful form, as in ❤️) and for the tag characters inside flags such as Scotland's. The status bar says how many emoji parts were skipped.
Is a zero width non-joiner always a mistake?
No. Persian needs it to write words such as میخواهم correctly, and several Indic scripts use the joiner and non-joiner to choose letter forms. When one sits between letters of such a script it is reported as a Note and kept when cleaning. Between Latin letters, as in an email address or a variable name, it is a Problem.
What is the difference between NFC and NFKC?
Both are Unicode normal forms. NFC only joins a letter and a combining accent into one character where one exists (e + ◌́ becomes é), so the text looks the same but compares reliably. NFKC also replaces compatibility characters with plain ones: the ligature fi becomes fi, superscript ² becomes 2, full-width A becomes A and a no-break space becomes a space. That is useful for search keys and usernames, but it can change meaning, so use NFC for text you will show to people.
How can text be hidden in Unicode?
The tag characters U+E0020–U+E007E mirror printable ASCII but are invisible, so a sentence can carry a second, unseen sentence; this is used to slip instructions to AI models (“ASCII smuggling”). Runs of variation selectors can likewise carry bytes after an emoji. The tool decodes both and shows the hidden text under the finding.
Does it change look-alike letters?
No. A Cyrillic а or Greek ο in a Latin word is reported with the letter it imitates, but replacing it could corrupt text that is really Russian or Greek, so the cleaned copy leaves it as it is. Retype the word, or fix the letter in your editor using the line and column shown.