@ Erdem Hanay

2026-08-23 · Erdem Hanay

An Empirical Study on Sub-Word Tokenizer Fragmentation via Zero-Width Unicode Injection

Erdem Hanay
Independent undergraduate (not a computer-science major)
leafturner752@gmail.com 23 August 2026

This page is an author preprint. It has not been peer-reviewed. Archived copy: https://doi.org/10.5281/zenodo.22064523. Measurement scripts: locust-gutenberg-test (Hanay, 2026). HTML reading copy: https://www.erdemhanay.info/blog/zero-width-tokenizer-degradation/. This work does not introduce a new attack class; invisible-character and token-boundary effects are already documented (Boucher et al., 2022; Schulz et al., 2025; Sarabamoun, 2025).

Abstract

Byte Pair Encoding (BPE) compresses text by capturing frequent character runs into dedicated vocabulary entries. Introducing an invisible character, specifically U+200C (ZERO WIDTH NON-JOINER, or ZWNJ), into those runs breaks the exact-match lookup. As a result, the tokenizer fragments the sequence into smaller pieces paired with a standalone token for the non-joiner itself, while the visual text remains identical to a human reader.

This article documents a production RSS interleaver that applies that transform at stride 2, and three measurements on a 512-token window of Andersen (1837/n.d.): token inflation ratio (TIR) under cl100k_base, extractive QA exact match on deepset/tinyroberta-squad2, and raw DistilBART summaries (Shleifer, 2020). TIR ranged from +539.1% at stride 1 to +98.4% at stride 4. These two small models failed on the haunted window (QA exact match 0.571 to 0.000 at stride 2; DistilBART shredded). One regular expression restored both. The QA reader truncates at 512 of its own tokens, so zeros at strides 1–2 mix merge-breaking with length. Cosine embedding shift (ΔH) and ROUGE-1 were not measured. Large chat models were not evaluated. This page is not interleaved; only phile bodies in /rss.xml receive the transform.

I. Introduction

BPE does not see a word. It sees the longest byte-run it was trained to merge (Gage, 1994; Sennrich et al., 2016). Break that run with a format character that occupies no ink, and the merge is gone while the glyphs on the page stay put.

That gap now sits on a live ingest path. RSS remains a common way to pull prose into summarizers, classifiers, and retrieval-augmented generation (RAG) pipelines (RSS Advisory Board, 2009). Those systems are billed and bounded by tokens. Invisible-character attacks on NLP models are already documented: Boucher et al. (2022) showed that imperceptible Unicode can flip model behavior, Schulz et al. (2025) showed that token-boundary manipulation can bypass classifiers, and Sarabamoun (2025) surveyed special-character attacks on open-source language models. The present work is narrower. It measures how a stride-aware ZWNJ interleaver fragments cl100k_base, what that costs in token count, and whether two small local models still recover the same window.

The thesis is threefold. First, I describe the production helper that walks a .phile body, copies protected spans, and interleaves only letter-runs longer than two characters. Second, I report TIR for strides 1 through 4 on a public-domain window. Third, I report extractive QA and DistilBART output on that same window, with a sanitizer restore condition. The runner is leafturner/locust-gutenberg-test (Hanay, 2026). The informal origin of the idea is Locust I.

This article leaves several things out on purpose. It does not report embedding cosine (ΔH) or ROUGE-1. It does not test a large chat model. It does not claim a novel attack. It is not a jailbreak kit, not a new tokenizer, and not a security boundary. One regular expression restores the letters. The first live-RSS audit is included only as a negative control: its “clean” side never stripped U+200C, so the +8.0% figure is decoration cost, not husk cost. Husk, used once here, means a letter-run split around a ZWNJ so that BPE can no longer store it as one piece.

II. Fundamental Concepts and Architecture

Terminology

Byte Pair Encoding. A compression-derived merge algorithm that iteratively replaces frequent symbol pairs with a new symbol (Gage, 1994). Neural machine translation adopted it as a subword tokenizer so rare words share pieces with frequent ones (Sennrich et al., 2016). OpenAI’s cl100k_base encoding, exposed through tiktoken, is the BPE table used in this study (OpenAI, 2023).

ZERO WIDTH NON-JOINER (U+200C). A Unicode format character in general category Cf. It is invisible in ordinary rendering. It is not U+200B (ZERO WIDTH SPACE). U+200B is a line-break opportunity; some readers wrap on it and the “invisible” tax becomes a ragged paragraph (Unicode Consortium, 2023). This implementation uses ZWNJ for that reason.

Stride S. The number of letters copied before each inserted ZWNJ. Stride 1 inserts a mark between almost every letter in a long run. Stride 2, the production default, inserts a mark every two letters. Words of length 1 or 2 are left whole.

Token inflation ratio.

TIR(S) = (N_perturbed(S) - N_clean) / N_clean

N_clean is the token count of an unperturbed window. Windows are sliced on the clean encoding so the denominator stays comparable across strides.

Protected spans. Substrings the linear scan copies unchanged: HTML tags, URLs, markdown images and links, ink blocks, CDATA closers, and ANSI wrappers of the form #[role|text]. The wrapper is preserved; only the inner text is interleaved.

Workflow

The production path and this article’s path diverge on purpose.

.phile body
  -> RSS item render
  -> linear scan (copy protected spans)
  -> interleave letter-runs at stride S
  -> RSS description and content:encoded

HTML research pages (/blog/...)
  -> no interleave

Feed items follow RSS 2.0 item content conventions (RSS Advisory Board, 2009). This measurement page stays clean so the sandbox and the quoted tables are not self-poisoned.

Theoretical foundation

BPE lookup is longest-match against a learned merge table. A vocabulary entry for a frequent run such as a common English stem exists only as that exact byte sequence. Inserting U+200C splits the sequence. The table miss produces (a) leftover pieces that may themselves be vocabulary entries and (b) a standalone token for the format character. The reader’s glyph stream is unchanged.

The interleaver is a single left-to-right pass over the input: O(n) in character length. It does not build a suffix array or retrain a vocabulary. TIR is expected to fall as S grows, because each increment of S reduces the number of insertion sites inside a letter-run of length L to about ceil(L / S) - 1 (for L greater than 2). That is a counting argument, not a latency benchmark. No wall-clock or memory figures are reported.

III. Implementation and Practical Application

The production transform lives in src/modules/seo/zero-width.ts. It walks the rendered item body, copies each protected span, and runs interleaveProse on the gaps. interleaveProse tokenizes on non-whitespace, then skips tokens that are already short, already contain format characters, or match digits, hexadecimal literals, or email addresses. Remaining tokens have their Unicode letter-runs passed to interleaveWord.

export function interleaveWord(word: string, stride = 2): string {
  if (word.length <= 2) {
    return word;
  }

  const step = Math.max(1, Math.floor(stride));
  const chunks: string[] = [];

  for (let index = 0; index < word.length; index += step) {
    chunks.push(word.slice(index, index + step));
  }

  return chunks.join(ZWNJ);
}

The Python companion implements the same letter-run rule and the same sanitizer. Anyone who wants the original letters can drop the common zero-width format characters in one pass:

import re

def strip_zero_width(text: str) -> str:
    return re.sub(r"[\u200B-\u200D\uFEFF]", "", text)

The regex package can also strip the entire Unicode Cf class in one substitution. That is not a shield. It is a tax on the lazy ingest path.

The sandbox below uses the same TypeScript interleaveProse helper as the feed, with a live stride control. It is a demonstration, not the 512-token official window.

Live sandbox

cl100k_base interleaving

Clean tokens

12

Perturbed tokens

50

Inflation

+316.7%

Visual inspection

Th[ZW]e Em[ZW]pe[ZW]ro[ZW]r wa[ZW]lk[ZW]ed th[ZW]ro[ZW]ug[ZW]h th[ZW]e gr[ZW]an[ZW]d ha[ZW]ll in hi[ZW]s in[ZW]vi[ZW]si[ZW]bl[ZW]e ro[ZW]be.

Clean tokens

"The"" Emperor"" walked"" through"" the"" grand"" hall"" in"" his"" invisible"" robe""."

Perturbed tokens

"Th""‌""e"" Em""‌""pe""‌""ro""‌""r"" wa""‌""lk""‌""ed"" th""‌""ro""‌""ug""‌""h"" th""‌""e"" gr""‌""an""‌""d"" ha""‌""ll"" in"" hi""‌""s"" in""‌""vi""‌""si""‌""bl""‌""e"" ro""‌""be""."

Trade-offs, edge cases, and pitfalls

ZWNJ over ZWSP is a rendering choice. Stride 2 over stride 1 is a readability choice: per-letter insertion nearly doubles the BPE bill relative to stride 2 on the official window and makes some selection UIs behave poorly. Short words stay whole so articles and prepositions do not become noise. URLs, emails, and hex tokens stay whole so links and identifiers remain copyable.

This article and the rest of /blog/ are not RSS items. Confirming that property is part of the method: a measurement page that interleaves its own prose cannot quote TIR honestly.

The first live-RSS audit is the main pitfall. The “clean” side stripped ASCII banners and #[role|text] markers but did not strip U+200C. The resulting +8.0% TIR is mostly decoration versus raw, not clean prose versus husked prose. That mistake is recorded in Section IV as a negative control, not as a headline finding.

IV. Benchmarks and Performance Analysis

TIR is the cost metric. Extractive QA EM/F1 and raw DistilBART strings are the recovery metrics. The interleaver is O(n) as described above. Latency, memory overhead, and throughput were not measured.

The official TIR run used python benchmark.py from Hanay (2026) on 2026-08-23T00:10:39Z, Python 3.14.7, tiktoken==0.14.0, encoding cl100k_base. QA and DistilBART used the same window via qa.py and summarize.py and --text-file 1597-0.txt (Andersen, 1837/n.d.; Gutenberg cache/epub copy). After whitespace collapse the story body was 297351 characters. The first 512 clean tokens (2403 characters) were the window. Raw stdout is in the supplementary log.

Official window versus stride

Stride S Perturbed tokens TIR ΔH ROUGE-1 Sanitizer restore
1 3272 +539.1% not measured not measured yes
2 (production RSS) 1764 +244.5% not measured not measured yes
3 1216 +137.5% not measured not measured yes
4 1016 +98.4% not measured not measured yes

N_clean is 512. TIR falls as stride grows, which matches the insertion-site count. Stride 1 is the expensive case. Production stride 2 more than triples the window. Even stride 4 almost doubles it. The script asserted strip_zero_width(perturbed) == window after every stride and exited 0.

These counts will move if Gutenberg edits the file or tiktoken ships a new cl100k_base merge table. The runner, not this table, is the source of truth.

Extractive QA on the same window

The QA reader truncates at 512 of its own tokens. Interleaved windows are longer than that. A zero at stride 1 or 2 is merge-breaking and possible cutoff, not proof that “AI cannot understand” the story.

python qa.py --text-file 1597-0.txt. Model: deepset/tinyroberta-squad2. Fourteen hand-written questions whose gold spans sit in the 512-token window. Predictions are stripped of U+200B–U+200D and U+FEFF before exact match and token F1.

Condition Stride n EM F1
clean 14 0.571 0.571
interleaved 1 14 0.000 0.000
sanitized 1 14 0.571 0.571
interleaved (production RSS) 2 14 0.000 0.000
sanitized 2 14 0.571 0.571
interleaved 3 14 0.143 0.147
sanitized 3 14 0.571 0.571
interleaved 4 14 0.357 0.357
sanitized 4 14 0.571 0.571

Clean exact match is 8 of 14. That is above chance and not a strong reader. At strides 1 and 2 the score is 0 of 14. At stride 4 it is 5 of 14. Every sanitized row matches clean. That is the causal pattern: the ghosts (and the extra length they create) caused the miss; one strip restored the baseline. This is span recovery on one small model. It is not a hallucination study and not a claim about large chat models.

DistilBART strings on the same window

python summarize.py --text-file 1597-0.txt. Model: DistilBART-CNN-12-6 (Shleifer, 2020; cf. Lewis et al., 2020). Beam search, max_length=80. ROUGE was not computed. These are the raw strings.

clean. An Emperor was so fond of new clothes that he spent all his money in dress. One day, two rogues, calling themselves weavers, made their appearance at the court. They gave out that they knew how to weave stuffs of the most beautiful colors and elaborate patterns. “These must, indeed, be splendid clothes!” thought the Emperor. “Had I

S=1. shredded leftover letters; not a summary of the story.

S=2 (production). starts “Many years ago” as broken two-letter husks; not a usable summary.

S=4. almost the opening sentence, with leftover marks inside words (“Empe’ror”, “clot’hes”).

S=2 sanitized. identical to the clean string.

That is generation collapse and copy-garbage on this summarizer, not an invented alternate plot. After sanitizing, DistilBART tells the story again.

Negative control: contaminated live RSS

First content:encoded item on https://www.erdemhanay.info/rss.xml (childhood post, after the feed transform). The “clean” side stripped banners and ANSI markers and left ZWNJ in place.

Condition Characters Tokens (cl100k_base) TIR ΔH ROUGE-1
Regex-stripped body (ZWNJ kept) 8203 4498
Live raw body (banners + ZWNJ) 8949 4858 +8.0% not measured not measured

XML parse succeeded under ElementTree and strict lxml. DistilBART and MNLI runs on that dirty feed are not findings. The clean-window DistilBART strings above replace them.

Extra corpus: sandbox sentence

Same helper and encoding, one sentence from the sandbox. This is not the official window. A 12-token sentence has less punctuation and fewer already-atomic tokens, so TIR is worse. The two tables must not be averaged.

Clean text: The Emperor walked through the grand hall in his invisible robe. Clean tokens: 12.

Stride S Perturbed tokens TIR Sanitizer restore
1 94 +683.3% yes
2 50 +316.7% yes
3 32 +166.7% yes
4 28 +133.3% yes
5 23 +91.7% yes

V. Real-World Use Cases and Future Outlook

The deployed case is this site. /rss.xml interleaves phile bodies at stride 2. A scraper that concatenates content:encoded and runs cl100k_base pays the official-window tax on that stride: +244.5% on the Gutenberg control, and an unknown TIR on each live item. The same stride zeroed the small QA reader and shredded DistilBART on the control window. Human subscribers see ordinary prose. The research pages, including this one, are excluded from that feed.

Castagnaro et al. (2025) describe attacks on RAG data loaders that treat plain text as trusted. A loader that does not strip Cf will index husked tokens, retrieve them, and spend context on format characters. The defense is the sanitizer in Section III, applied before chunking. That is also why this transform is not digital rights management. Anyone who wants the letters can have them.

Current limitations are sharp. The study uses one encoding, one public-domain window, one production stride, and two small CPU models. The QA reader truncates at 512 of its own tokens. ΔH and ROUGE-1 remain undone. Larger chat models were not run. Other encodings (SentencePiece, older GPT-2 BPE, multilingual tables) were not run. XML validity is not an obstacle: both parsers accepted the poisoned feed.

Ongoing work, stated as unfinished rather than as results: a multi-encoding TIR table, a larger-model QA pair, and a reusable library extracted from zero-width.ts.

VI. Conclusion and Key Takeaways

Four points follow from the measurements.

  1. BPE longest-match is brittle against an invisible format character placed inside a letter-run. The human reader is not the tokenizer.
  2. On a 512-token cl100k_base window of Andersen, TIR was +539.1%, +244.5%, +137.5%, and +98.4% at strides 1 through 4. Production RSS uses stride 2.
  3. These two small models failed; a regex fixed it. tinyroberta-squad2 exact match fell from 0.571 to 0.000 at strides 1 and 2 (that reader also truncates at 512 of its own tokens). DistilBART’s clean summary became shredded leftover letters. Sanitizing restored both.
  4. The defense is to strip Cf (or the small zero-width set) before tokenization. That is a tax on lazy ingest, not a claim about large chat models.

To reproduce, clone leafturner/locust-gutenberg-test and run python benchmark.py, python qa.py --text-file 1597-0.txt, and python summarize.py --text-file 1597-0.txt. The raw log is the transcript. Locust I is the informal origin, including the contaminated-baseline mistake. If you ingest RSS into a model, strip format characters first.

© 2026 Erdem Hanay. Text and tables on this page may not be reproduced without attribution. The measurement scripts are MIT (locust-gutenberg-test).

References

Andersen, H. C. (1837/n.d.). The emperor’s new clothes (EBook No. 1597). Project Gutenberg. https://www.gutenberg.org/files/1597/1597-0.txt

Boucher, N., Shumailov, I., Anderson, R., & Papernot, N. (2022). Bad characters: Imperceptible NLP attacks. In 2022 IEEE Symposium on Security and Privacy (SP) (pp. 1987–2004). IEEE. https://doi.org/10.1109/SP46214.2022.9833641

Castagnaro, A., Salviati, U., Conti, M., Pajola, L., & Pizzi, S. (2025). The hidden threat in plain text: Attacking RAG data loaders. arXiv. https://arxiv.org/abs/2507.05093

deepset. (2021). tinyroberta-squad2 [Computer software]. Hugging Face. https://huggingface.co/deepset/tinyroberta-squad2

Gage, P. (1994). A new algorithm for data compression. The C Users Journal, 12(2), 23–38.

Hanay, E. (2026). An empirical study on sub-word tokenizer fragmentation via zero-width Unicode injection. Zenodo. https://doi.org/10.5281/zenodo.22064523

Hanay, E. (2026). locust-gutenberg-test [Computer software]. https://github.com/leafturner/locust-gutenberg-test

Lewis, M., Liu, Y., Goyal, N., Ghazvininejad, M., Mohamed, A., Levy, O., Stoyanov, V., & Zettlemoyer, L. (2020). BART: Denoising sequence-to-sequence pre-training for natural language generation, translation, and comprehension. In Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics (pp. 7871–7880). Association for Computational Linguistics. https://doi.org/10.18653/v1/2020.acl-main.703

OpenAI. (2023). tiktoken [Computer software]. https://github.com/openai/tiktoken

RSS Advisory Board. (2009). RSS 2.0 specification. https://www.rssboard.org/rss-specification

Sarabamoun, E. (2025). Special-character adversarial attacks on open-source language models. arXiv. https://arxiv.org/abs/2508.14070

Schulz, K., Yeung, K., & Evans, K. (2025). TokenBreak: Bypassing text classification models through token manipulation. arXiv. https://arxiv.org/abs/2506.07948

Sennrich, R., Haddow, B., & Birch, A. (2016). Neural machine translation of rare words with subword units. In Proceedings of the 54th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers) (pp. 1715–1725). Association for Computational Linguistics. https://doi.org/10.18653/v1/P16-1162

Shleifer, S. (2020). DistilBART [Computer software]. Hugging Face. https://huggingface.co/sshleifer/distilbart-cnn-12-6

Unicode Consortium. (2023). The Unicode Standard, Version 15.1.0. https://www.unicode.org/versions/Unicode15.1.0/