Charlie KrugThe Build Log

← All posts

The prompt reads clean. 28 invisible characters say otherwise.

Unmask puts the text you see beside the text a model sees. Paste anything and it decodes the zero-width, bidi, tag-character, and homoglyph tricks that render as nothing but tokenize as words. Fully client-side.

Open Unmask and it hands you a sentence that reads, in full, "Please summarize this document for me." Look at the pane next to it and the same sentence is carrying 28 characters you cannot see, which spell out ignore previous instructions. Nothing is rendered wrong. Your eyes are not the problem. The bytes just say more than the glyphs do.

Two definitions of "text"

Text is the interface between people and language models: prompts, skill files, MCP tool descriptions, the templates you copy from a repo. But "text" in the Unicode sense is a far bigger surface than "text" in the human-reading sense. A run of zero-width joiners, a bidi-override sequence, or a string of invisible tag characters can carry a whole sentence that never draws a single pixel. A person reviewing the file sees nothing. A model tokenizing the same bytes reads every word.

Unmask is a magnifying glass for that gap. Paste text, or drop in a .md, .txt, or .json file, and it runs the input through a battery of Unicode-aware detectors, then renders a light table: what you see on the left, what the model sees on the right, with a findings rail linking each flagged character to both panes. Every finding gets a plain-language reason, not just a code point, because the audience is people who review prompts, not people who have the Unicode blocks memorized.

The tag-character trick

The demo payload uses the most surprising channel, so it is worth explaining. Unicode has a block from U+E0000 to U+E007F called the tag characters. Each one is a ghost copy of a plain ASCII character: U+E0069 is a tagged lowercase i, and its low byte, 0x69, is exactly the ASCII code for i. The block renders as nothing in every normal font. So you can take the string ignore previous instructions, add 0xE0000 to each character's code point, and you have a run of invisible tag characters that a tokenizer will happily read back as the original phrase. Unmask decodes that block back down to ASCII, shows you the recovered string, and checks it against a short list of known injection phrases so a real payload gets called out by name rather than buried in a list of 28 code points.

The other three detectors cover the rest of the surface: zero-width and invisible characters (joiners, word joiners, soft hyphens, Hangul fillers, all of which count as real characters to a tokenizer), bidi-override controls like RLO and LRO that make a line display differently than it is stored, and homoglyphs, where a Cyrillic а stands in for a Latin a across a 52-entry confusables table, the same trick behind look-alike domains.

The bug this tool could not afford to ship

One design decision is my favorite because it is so on the nose. Every detector table is defined by numeric code point, never by pasting the literal invisible or confusable character into the source. An invisible character sitting in a source file is exactly the kind of thing an editor, a diff viewer, or a copy-paste can silently mangle, and a tool whose entire job is catching smuggled Unicode shipping a smuggled-Unicode bug of its own would be a special kind of embarrassing. So the code never touches the raw glyphs.

Sanitize follows the same honesty: zero-width, bidi, and tag characters get deleted outright, since prose bound for an LLM has no legitimate use for them, while confusables get folded to their Latin look-alike instead of removed, because deleting them would gut otherwise-readable words. One click strips the hidden stuff and copies a clean version.

Try it

Open Unmask, read the seeded demo, then click into the decoded pane and watch the payload appear. Now paste something you did not write: a skill file, a shared prompt, a support ticket you were about to feed an agent. Nothing you paste leaves the tab, there are no network calls at all, so open your network panel and confirm it. If you would rather read the detectors than trust them, the code is on GitHub.

Unmask is live. Free, in your browser, no signup.

This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.

Comments

Loading comments…