Profanity filtering users can't bypass.
Filters that find bad words inside other words block Scunthorpe, the surname Dickinson and the cockatoo. Here is how often, and the fix.
The Scunthorpe problem is a filter blocking innocent text because a profane string sits inside it. It is named for the English town whose name holds one.
The same mistake turns up in surnames (Dickinson, Hancock), animals (cockatoo, peacock), medicine (breast cancer, anal fissure) and ordinary words (assassin, classic). A user named Dickinson who cannot sign up is the same failure as a review of a garden hoe held for moderation.
A filter has to decide where a word ends. Match a bad word anywhere inside the text and you catch it glued to other words or hidden in a username, which has no spaces. You also block every innocent word that contains it.
Match only whole words and Scunthorpe goes through, but so does a slur run into the next word. Disguises make it worse: a filter that reads sh1t as a word reads look-alike letters everywhere, and an innocent word read through them can turn profane.
Here is a plain word list, matched as typed, beside the engine on four texts. The list blocks the town and misses the disguises.
Moving to Scunthorpe next month
The list finds a slur inside a town.
Blocks it
allow
f.u.c.k off, nobody wants you here
Four dots and the list sees no word.
Misses it
block
ƒυ¢к off
A hooked f, a Greek u, a cent sign and a Cyrillic k.
Misses it
review
I know where you live
A threat in ordinary words.
Misses it
block
1 innocent blocked3 missed
1 innocent let through3 caught
The Scunthorpe Test's public split holds 525 innocent items in seven kinds, each a word or phrase that contains a profane string. Every system below read the same items, so every flag in the table is a false positive.
| System | Places | Surnames | Ordinary words | Animals | Medical terms | Brands and titles | Tools | All innocent |
|---|---|---|---|---|---|---|---|---|
| obscenity 0.4.6 | 23.1% | 20.0% | 13.1% | 53.2% | 42.3% | 26.3% | 0.0% | 20.2% |
| bad-words 4.1.5 | 1.3% | 12.6% | 3.9% | 51.1% | 34.6% | 31.6% | 14.3% | 12.5% |
| 2toad-profanity 3.3.0 | 1.3% | 10.5% | 3.9% | 51.1% | 34.6% | 21.1% | 7.1% | 11.5% |
| leo-profanity 1.9.0 | 1.3% | 4.2% | 2.6% | 46.8% | 34.6% | 10.5% | 0.0% | 8.7% |
| OpenAI moderation | 1.3% | 1.1% | 3.1% | 10.6% | 0.0% | 10.5% | 0.0% | 3.2% |
| Perspective API | 0.0% | 0.0% | 0.9% | 23.4% | 3.8% | 5.3% | 0.0% | 3.0% |
| Unprofane, rules alone | 0.0% | 0.0% | 0.0% | 0.0% | 0.0% | 0.0% | 0.0% | 0.0% |
| Items of each kind | 78 | 105 | 236 | 47 | 26 | 19 | 14 | 525 |
The Scunthorpe Test, public split, scunthorpe-test.jsonl; result files 2026-09-24-14-29-32-dev.json, 2026-09-28-02-10-51-dev.json, 2026-09-28-08-29-25-dev.json, 2026-10-18-21-05-29-dev.json, 2026-10-20-14-48-25-dev.json in bench/results/. The rules' run of 2026-10-20 read a later version of the split, which adds 24 place names written with accents (0.0% of them flagged); the table shows the kinds every run read. How it was measured.
The kinds are not equally hard: obscenity 0.4.6, the worst of these, blocks animals most often, 53.2% of them. Examples of each kind, from the engine's innocent list:
Those innocent items come from the engine's own innocent list. A zero for Unprofane there means it lets through the words it was built to let through, not that it never blocks an innocent word.
For that, look at text nobody wrote the list from. The head-to-head's development set is built from third-party datasets and labeled blind by AI judges. The share of each kind of clean text each system flagged:
| System | An innocent word that holds a profane string | A place name | A surname | The title of a film, book or song | A violent idiom ("killing it") |
|---|---|---|---|---|---|
| Unprofane, balanced | 7.9% | 0.8% | 0.5% | 0.9% | 1.4% |
| Unprofane, rules alone | 3.1% | 1.1% | 0.6% | 0.4% | 5.3% |
| Perspective API | 3.1% | 1.3% | 0.3% | 1.1% | 8.9% |
| OpenAI moderation | 0.0% | 0.0% | 0.0% | 0.0% | 13.7% |
| bad-words | 0.9% | 0.4% | 0.5% | 0.4% | 0.1% |
| obscenity | 7.0% | 5.4% | 4.5% | 2.7% | 2.2% |
| Clean texts of each kind | 229 | 1,847 | 1,268 | 882 | 1,610 |
Gold DEV, 19,584 texts, rubric version 2, read 2026-09-28, engine 2026-09-29. Perspective and OpenAI are read at the score where they flag as much clean text overall as Unprofane balanced does. DEV is where the engine was tuned, so its numbers here are, if anything, kind to it.
Here Unprofane balanced flags 7.9% of innocent words, and the rules alone 3.1%. That is more than the word lists, which flag at most 7.0%.
But a list that flags little also catches little: none catches more than 9.9% of the toxic text in the same set, where Unprofane balanced catches 91.8%.
A false-positive rate means nothing without the catch rate beside it, and both depend on whose innocent words you test. Test yours.
The rules find every candidate the way a substring filter would, through every disguise, then rule out the innocent ones:
What the rules cannot rule out, a word innocent in one sentence and an insult in the next, goes to the model in balanced mode, and the answer says which one decided.
The evasion tags name every disguise the engine read through, so a false positive can be traced to the reading that caused it.
review rather than block, so a borderline word waits for a person instead of turning a user away.