- Spam replies pad every character with invisible zero-width characters. On screen the text looks normal; as a string it is unrecognizable.
- No keyword or regex can match it, because the phrase you are looking for is not contiguous any more.
- The fix is two-part: strip invisible characters before matching, and separately flag posts that are mostly invisible padding.
- X Spam Blocker does both, and the second rule keeps working when the spam rewrites its script.
This one is worth understanding even if you never install anything, because it explains a frustration almost everyone who has tried to filter spam on X has run into: a reply that is unmistakably spam, that visibly contains the exact phrase in your filter list, and that your filter does not catch. The post is not lying to you. Your filter is looking at a different string than the one you can see.
The symptom
Say you have looks better than me in your keyword list, and a reply comes in that reads, plainly, nobody looks better than me. Your filter does not fire. You check for typos. You check that scanning is on. You copy the text out of the post and paste it into your filter list, and that does not work either — in fact now nothing matches.
At that point the explanation is almost always padding. The visible text and the underlying text are not the same thing, and when you copied the post you copied the padding along with it.
How the trick works
Unicode has a number of characters that are deliberately invisible. They exist to do jobs other than being seen: joining two emoji into one, telling a script how adjacent letters should connect, marking a change of text direction. They render as nothing, occupy no width, and survive copy and paste.
So a spammer inserts one between every character of the message. A phrase of eight characters becomes a string of twenty-five, of which seventeen are invisible. To a reader it is eight characters. To a filter it is a sequence in which no two letters of your keyword are ever adjacent.
| Visible | Actual characters | |
|---|---|---|
| What the reader sees | 8 | — |
| What the filter reads | 8 | 25, 17 of them invisible |
| Contains your keyword? | Yes | No |
It costs the spammer nothing — it is a find-and-replace in whatever script posts the replies — and it defeats every naive filter at once.
The characters involved
In samples taken from live reply spam, the padding is nearly always one of these three, rotated to avoid looking mechanical:
| Code point | Name | Legitimate job |
|---|---|---|
| U+200C | Zero-width non-joiner | Keeps letters from joining in Persian, Arabic, Devanagari |
| U+200D | Zero-width joiner | Joins emoji into one glyph, e.g. a family or a profession |
| U+2060 | Word joiner | Prevents a line break without adding a space |
There are others in the same family — the byte order mark U+FEFF, the direction marks U+200E and U+200F, the soft hyphen U+00AD, the Mongolian vowel separator U+180E, the bidirectional overrides in the U+202A–U+202E range. A thorough filter has to account for all of them, because whichever one it misses is the one that gets used next.
See it for yourself in twenty seconds
You do not need any tools for this. Open a spam reply, select its text, copy it, and paste it somewhere that will tell you the length — a browser console is easiest:
const s = `paste the copied reply here`
console.log(s.length)
console.log((s.match(/[--]/g) || []).length)A normal sentence returns a second number of zero. Padded spam returns something close to the first number. In one sampled thread, four replies out of eleven carried 52 to 67 invisible characters each — around 80% of the raw string — while the other seven measured exactly zero. There is no grey zone in the middle; that is what makes it detectable.
Why every text filter fails on it
It is worth being precise about this, because the failure is not a bug in any particular filter. Three defences all break at once:
- Exact keywords. Dead immediately. The substring does not exist.
- "Loose" regexes that allow a gap between characters — something like
b.{0,3}e.{0,3}t— survive one filler character per gap and die at six. - Copy-pasted keywords. Copying the phrase out of the spam brings its padding with it, so your new keyword only matches that one post, with that exact arrangement of fillers.
There is a fourth failure that is easy to miss. In JavaScript, a quantifier like .{0,3} counts UTF-16 code units, not characters — and one emoji is two code units. So a loose regex written to tolerate three characters of gap actually tolerates one emoji, which is why spacing a word out with emoji beats a loose pattern too.
Fix one: strip the characters before matching
The right place to solve this is before any rule runs. X Spam Blocker builds a cleaned copy of the text it is about to match: invisible characters removed, then Unicode NFKC normalization so fullwidth, circled and sub/superscript letters fold back to plain ones. It then builds a third, tighter copy with all spaces, punctuation and emoji removed, leaving only letters and digits.
Rules are tried against all three, and your keywords are normalized the same way. That has three useful consequences:
- Your existing keyword list starts working on padded spam without you changing anything.
- A keyword you copied out of a spam reply, padding included, still works as a filter.
- Spacing a word out with emoji — the variant that beats loose regexes — is handled by the tight copy, which drops emoji entirely.
Nothing on the page is modified. The cleaning applies only to the copy used for matching.
Fix two: detect the shape, not the words
Stripping fixes today's spam. It does not fix next month's, because the wording will change and your keyword list will not have caught up. So there is a second, independent rule that ignores wording altogether:
If at least 30% of a post's characters are suspicious invisible ones, and there are at least 6 of them, and the post is at least 8 characters long — flag it. No keyword required.
That is the whole rule, and its value is that heavy invisible padding is itself the spam signal. Nobody writes a normal post that is a third invisible characters. The measured gap is enormous: the sampled spam replies scored 53% to 60% on the narrowed character set, and ordinary posts scored zero. A threshold of 30% sits in the middle of a very wide empty band.
In the panel it is the Also flag posts padded with hidden characters switch, on by default. When it fires, the candidate and the log entry read "Hidden-character obfuscation" with the measured percentage, so you can tell it apart from one of your own keywords. Keyword matches take priority, so when both would fire you get the specific word — which is more useful when you are debugging a rule.
The emoji problem, and why the rule is narrower than it looks
Here is the part that makes or breaks a rule like this. The zero-width joiner U+200D is not only a spam tool — it is the glue in composite emoji. A family emoji is three visible people joined by two U+200D characters. Measure it naively and you get 40% invisible characters: a false positive on the most innocent post imaginable.
So the detection rule uses a deliberately narrower character set than the stripping does. Three whole categories are excluded from counting as evidence:
- U+200D, the zero-width joiner — because composite emoji are full of it.
- U+FE00–U+FE0F, the variation selectors — the invisible character that makes ❤️ render in colour.
- U+E0020–U+E007F, the tag characters — used by subdivision flags such as 🏴.
Removing those three leaves the padding spam still scoring in the high fifties, because U+200C and U+2060 alone are enough to convict. Verified against emoji family sequences, coloured hearts, the rainbow flag, profession emoji, subdivision flags, Persian text using U+200C as real orthography, and very short posts padded on purpose, all of them measure 0.0%.
That is the trade worth copying if you build something similar: a stripping rule can be greedy, because a false strip costs nothing. A detection rule has to be stingy, because a false flag costs you a real account.
The related tricks, while you are here
Invisible padding is the most common evasion but not the only one, and the same normalization handles most of the family:
- Fullwidth and decorated letters. Fullwidth text, circled letters, mathematical bold — all fold back to plain letters under NFKC.
- Emoji as spacers. A word broken up with emoji between the characters, which the tight copy solves by deleting them.
- Homoglyphs. Cyrillic а for Latin a, Greek ο for o. NFKC does not fold these — they are genuinely different letters — so they stay a real gap. A homoglyph substitution is also, usefully, something the shape rule can be pointed at in future.
For the filters that sit on top of all this, see blocking Twitter accounts by keyword or regex, or the ready-made lists in blocking crypto scam and spam bots on X.
Frequently asked questions
What are zero-width characters?
Real Unicode characters that take up no space when rendered. They exist for legitimate reasons — joining emoji together, controlling how Persian and Devanagari letters connect, marking text direction — but because they are invisible and copy-paste cleanly, they are also a convenient way to break a text filter.
Can X detect this itself?
X clearly filters some of it, since the same spam is often removed eventually. But invisible characters are legitimate in enough contexts that a platform cannot simply ban them, and reply spam that survives long enough to be seen has already done its job. A filter on your own side does not have to wait for anyone.
Does stripping invisible characters break normal posts?
Not in a way you would notice, because the stripping only happens to the copy used for matching — nothing on the page is altered. Detection is stricter: the character set used for the hidden-padding rule deliberately leaves out emoji joiners and variation selectors, so an emoji-heavy post is never flagged for it.
What ratio counts as obfuscated?
Thirty percent or more of the string being suspicious invisible characters, with at least six of them, and the text at least eight characters long. Measured spam replies come in at 53–60%. Ordinary posts, including ones full of emoji and flags, measure zero.
Why does the log say a percentage instead of a keyword?
Because no keyword matched — the post was flagged for its shape. The log shows "Hidden-character obfuscation" with the measured ratio, which tells you the rule that fired was the shape rule rather than one of your own.
Block spam accounts on X in bulk, after you confirm
Free Chrome extension. No account, no server, no tracking.


