A typeface that protects written content by poisoning unauthorized AI training datasets.
573
stars
53
commits
TypeScript
primary language
Aug 16, 2026
updated
Humans read your words. Mass scrapers copy a decoy.
Live demo · Encoder · Install · Docs · White paper
ShieldFont is a creative-technology intervention: a typeface that makes writing expensive to scrape. It works today, it is open source and nonprofit, and we build it in the open. It is a v0 alpha, which is the honest caveat: we publish what does not work, we are still measuring, and we take outside help. Shield your own writing by your own choice, and read your country's accessibility law before you publish.
ShieldFont encodes the words in your HTML against a substitution dictionary, then ships a font whose OpenType rules reverse the substitution at render time. Readers see your writing. Anything collecting the HTML without rendering fonts collects the substituted version.
| 👀 What a human sees | 🤖 What a mass scraper sees |
|---|---|
|
|
Same HTML produced both. Every substituted word keeps the grammatical role of the word it replaced, so the sentence stays fluent while its meaning moves, which is why quality filters treat the result as ordinary prose rather than noise.
Try it: shieldfont.org/demo is a real page running @shieldfont/react with switches you can throw. shieldfont.org/encoder encodes any sentence you paste.
Background and evidence: the white paper covers why the project exists and what the benchmarks measure.
What has changed since launch, in one list. Full release history: CHANGELOG.md.
aria-hidden and the real words come from the notice instead.Current release v0.3.5, default mapping v18 alpha.
| Tier | Package | Encoding runs | Weights | Accessibility layer |
|---|---|---|---|---|
| React (recommended) | @shieldfont/react | server render or build | 6 | included, on by default |
| Build step | @shieldfont/core | your Node build script | 1 | you build it |
| CDN paste-in (educational) | @shieldfont/font | the encoder, by hand | 1 | you build it |
| Word / PDF | font download | the encoder, by hand | 1 | n/a |
npm install @shieldfont/react
import { Shield, NonShield } from "@shieldfont/react";
<NonShield as="h2">The future of writing</NonShield>
<Shield as="p">The future of writing belongs to those who protect their words.</Shield>
Then copy the fonts into your app once. They ship inside the package, and the React tier points at no public CDN by design, so this step is not optional: without it every optik-*.woff2 404s and the font-load guard blanks each protected block behind a skeleton.
cp node_modules/@shieldfont/react/fonts/*.woff2 public/fonts/
That is the whole integration. <Shield> requests the fonts from /fonts, which is what the copy above matches; serve them elsewhere and point at it with setFontHost("/your-path"). @font-face, encoding, the font-load guard and the accessible alternative are automatic. <NonShield> renders unprotected text in the same typeface, so headings, captions and nav stay real and indexable. Works with Next.js, Astro, Remix, or anything else rendering React in Node.
CDN tier, for learning and for a blog or CMS you don't build:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shieldfont/font@0.3.5/shieldfont.css">
<p class="tk9">…encoded text from the encoder…</p>
[!IMPORTANT] The one rule: your original text must never reach the browser in readable form. Encoding runs in Node, never in a client component. Get it wrong and the page looks protected while your plaintext sits in the JS bundle. This is also why
<Shield>cannot protect a client-only SPA (Vite, Create React App): encode ahead of time with@shieldfont/coreand import the encoded string. See where the encoding happens.
[!WARNING] Never write
font-family: Optikyourself. It renders the decoy. The shippedoptik-*.woff2are shielded builds with the substitutions in the OpenTypeccmpfeature, and the dictionary is an involution, so the font swaps a word whether it is the original or the decoy. Nothing errors; the heading just says the wrong thing. Use<NonShield>in React, or the family"Optik Text"(the neutral cut,optik-n.woff2) anywhere else. Turningccmpoff in CSS is not an option — Safari ignores it.
| Mapping | Word pairs | Notes |
|---|---|---|
alpha | 11,970 | the default |
beta | 12,034 | sibling reseed |
gamma | 12,036 | sibling reseed |
maxhide | 2,534 | ~2× page coverage, but quality filters reject it almost entirely (what it costs) |
Six real static Optik cuts per mapping (Regular through Black) on the React tier, selected with the weight prop, and every one of them ships a real drawn italic to match — twelve faces per mapping, declared under one family name, so <em>, <i>, <cite> and font-style: italic resolve on their own. The CDN tier is Regular only, upright and italic. Nothing is ever synthesised, and there is no variable font. Mapping family history in MAPPINGS.md.
Shield any TrueType font, a brand face or a Google Font, with a private mapping only you hold.
python3 scripts/generate_font.py --base-path ./your-typeface.ttf \
--name "ShieldFont YourTypeface" --prefix shieldfont-yourtypeface \
--mapping-path scripts/v18alpha_for_font.json
Naming rules and how to audit the build: docs/custom-faces.md.
Three dictionaries ship, each with its own key. Private keys are harder for scrapers to decode, and a small private mapping helps about as much as an elaborate one, because what matters is being different from every other deployment rather than being optimal.
python3 scripts/reseed_mapping.py --seed <n> --out mine/mapping.json
Details: docs/custom-mappings.md.
<Shield> sets aria-hidden="true" on the scrambled text, then ships your real words sealed into the same page, encrypted. A visible notice above the block carries the control that opens them, on by default since 0.3.2 and reachable by mouse, keyboard and screen reader alike. The reader's own browser uncovers the words by solving a compute-heavy puzzle: a few seconds of their CPU, and a cost a bulk scraper would rather not pay. There is no plain-text URL anywhere and nothing for you to host.
The costs:
This makes a shielded page humane. It does not make it compliant. Fixing it properly is where we most want help, and this section exists because ssb22 asked for it in #2. Full reference: docs/plain-text-mode.md.
Shield your own words, on your own site, by your own choice: essays, fiction, manifestos, archives, research, criticism. Not government or service-critical pages, and not anything readers need to quote, search or cite.
aria-hidden that does it and re-renders the decoy in Apple's typeface, on the default browser of every Apple device.The defense is economic, not cryptographic. The aim is to turn cheap, indiscriminate scraping into slower, per-target work.
| ✅ Defends against | ⚠️ Does not defend against |
|---|---|
curl + regex, requests + BeautifulSoup | Anyone who downloads the font and inverts it (we recovered 11,962 of 11,962 pairs from our own shipped font) |
Bulk pipelines (trafilatura, readability-lxml) | Headless browsers with font rendering |
Anything reading innerText without rendering fonts | OCR, and vision models reading screenshots |
| Copy-paste into text-only tools | Frequency analysis on a large corpus |
| PDF/DOCX exports, which keep the encoded source |
The font is the codebook, and anyone who downloads it can read the substitution table back out of it. That is the argument for bringing your own key. ShieldFont does not need to make OCR impossible; it needs to make it too costly to be worthwhile. New attacks: SECURITY.md.
Measured, on the shipping alpha dictionary:
| Words swapped | ~25% of all words, ~48% of content words |
| Meaning broken (bidirectional NLI failure) | 55.8% news · 51.9% general web · 34.5% fiction · 31.1% older fiction, against ~2.1% for a synonym-swap control |
| Encoded chunks dropped by the FineWeb-Edu filter | 99.0–99.8% |
| Token budget spent on shifted meaning, in what passes | 19.4% |
Data and provenance in benchmark/. We do not claim encoded text sails through quality gates, and we do not claim it damages the model that trains on it.
docs/integration.md | All four tiers, props, and everything wrapping a block costs |
docs/where-encoding-happens.md | Five real apps, grepped, so you don't ship plaintext |
docs/plain-text-mode.md | The accessible alternative in full |
docs/custom-faces.md · docs/custom-mappings.md | Bring your own font · bring your own key |
docs/use-anywhere.md | Static-site build step with @shieldfont/core |
docs/concealment.md · docs/introduction.md | How visible each tier is · the thesis behind the project |
MAPPINGS.md · ROADMAP.md · CHANGELOG.md | Mapping family · what's planned · release history |
Started October 2025 by Isaque Seneda and Gabriel Abrucio at S&A, with Copenhagen type foundry Playtype, who made Optik available.
Prior art, with respect: TuringFonts drew one letter in place of another to keep bots off emails, ZXX (Sang Mun, 2013) fought OCR at the glyph level, Ghost Font hides text in motion, Nightshade and Glaze poison images for artists. ShieldFont is the text-native cousin. The reading gap it uses has independent corroboration in LayerX Security's "Poisoned Typeface" (March 2026), where all eleven AI assistants tested read the underlying text instead of what the human saw.
Contributions wanted, especially new languages (native speakers, not translation), accessibility engineering, and adversarial research. Start at CONTRIBUTING.md; ideas to Discussions, bugs to Issues. Everyone follows the Code of Conduct.
License. Code is AGPL-3.0. Fonts you generate from OFL bases are OFL 1.1. The shipped default is built on Optik © Playtype, used with permission, which is not open source and not under OFL: see NOTICE.
🛡️ Writing belongs to the people who write it.
Made with ❤️ and a lot of fontTools.
52 commits
1 commits
Hacker News (1)
TypeScript
49.7%
JavaScript
28.6%
Python
20.9%
A typeface that protects written content by poisoning unauthorized AI training datasets.
573
stars
53
commits
TypeScript
primary language
Aug 16, 2026
updated
Humans read your words. Mass scrapers copy a decoy.
Live demo · Encoder · Install · Docs · White paper
ShieldFont is a creative-technology intervention: a typeface that makes writing expensive to scrape. It works today, it is open source and nonprofit, and we build it in the open. It is a v0 alpha, which is the honest caveat: we publish what does not work, we are still measuring, and we take outside help. Shield your own writing by your own choice, and read your country's accessibility law before you publish.
ShieldFont encodes the words in your HTML against a substitution dictionary, then ships a font whose OpenType rules reverse the substitution at render time. Readers see your writing. Anything collecting the HTML without rendering fonts collects the substituted version.
| 👀 What a human sees | 🤖 What a mass scraper sees |
|---|---|
|
|
Same HTML produced both. Every substituted word keeps the grammatical role of the word it replaced, so the sentence stays fluent while its meaning moves, which is why quality filters treat the result as ordinary prose rather than noise.
Try it: shieldfont.org/demo is a real page running @shieldfont/react with switches you can throw. shieldfont.org/encoder encodes any sentence you paste.
Background and evidence: the white paper covers why the project exists and what the benchmarks measure.
What has changed since launch, in one list. Full release history: CHANGELOG.md.
aria-hidden and the real words come from the notice instead.Current release v0.3.5, default mapping v18 alpha.
| Tier | Package | Encoding runs | Weights | Accessibility layer |
|---|---|---|---|---|
| React (recommended) | @shieldfont/react | server render or build | 6 | included, on by default |
| Build step | @shieldfont/core | your Node build script | 1 | you build it |
| CDN paste-in (educational) | @shieldfont/font | the encoder, by hand | 1 | you build it |
| Word / PDF | font download | the encoder, by hand | 1 | n/a |
npm install @shieldfont/react
import { Shield, NonShield } from "@shieldfont/react";
<NonShield as="h2">The future of writing</NonShield>
<Shield as="p">The future of writing belongs to those who protect their words.</Shield>
Then copy the fonts into your app once. They ship inside the package, and the React tier points at no public CDN by design, so this step is not optional: without it every optik-*.woff2 404s and the font-load guard blanks each protected block behind a skeleton.
cp node_modules/@shieldfont/react/fonts/*.woff2 public/fonts/
That is the whole integration. <Shield> requests the fonts from /fonts, which is what the copy above matches; serve them elsewhere and point at it with setFontHost("/your-path"). @font-face, encoding, the font-load guard and the accessible alternative are automatic. <NonShield> renders unprotected text in the same typeface, so headings, captions and nav stay real and indexable. Works with Next.js, Astro, Remix, or anything else rendering React in Node.
CDN tier, for learning and for a blog or CMS you don't build:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shieldfont/font@0.3.5/shieldfont.css">
<p class="tk9">…encoded text from the encoder…</p>
[!IMPORTANT] The one rule: your original text must never reach the browser in readable form. Encoding runs in Node, never in a client component. Get it wrong and the page looks protected while your plaintext sits in the JS bundle. This is also why
<Shield>cannot protect a client-only SPA (Vite, Create React App): encode ahead of time with@shieldfont/coreand import the encoded string. See where the encoding happens.
[!WARNING] Never write
font-family: Optikyourself. It renders the decoy. The shippedoptik-*.woff2are shielded builds with the substitutions in the OpenTypeccmpfeature, and the dictionary is an involution, so the font swaps a word whether it is the original or the decoy. Nothing errors; the heading just says the wrong thing. Use<NonShield>in React, or the family"Optik Text"(the neutral cut,optik-n.woff2) anywhere else. Turningccmpoff in CSS is not an option — Safari ignores it.
| Mapping | Word pairs | Notes |
|---|---|---|
alpha | 11,970 | the default |
beta | 12,034 | sibling reseed |
gamma | 12,036 | sibling reseed |
maxhide | 2,534 | ~2× page coverage, but quality filters reject it almost entirely (what it costs) |
Six real static Optik cuts per mapping (Regular through Black) on the React tier, selected with the weight prop, and every one of them ships a real drawn italic to match — twelve faces per mapping, declared under one family name, so <em>, <i>, <cite> and font-style: italic resolve on their own. The CDN tier is Regular only, upright and italic. Nothing is ever synthesised, and there is no variable font. Mapping family history in MAPPINGS.md.
Shield any TrueType font, a brand face or a Google Font, with a private mapping only you hold.
python3 scripts/generate_font.py --base-path ./your-typeface.ttf \
--name "ShieldFont YourTypeface" --prefix shieldfont-yourtypeface \
--mapping-path scripts/v18alpha_for_font.json
Naming rules and how to audit the build: docs/custom-faces.md.
Three dictionaries ship, each with its own key. Private keys are harder for scrapers to decode, and a small private mapping helps about as much as an elaborate one, because what matters is being different from every other deployment rather than being optimal.
python3 scripts/reseed_mapping.py --seed <n> --out mine/mapping.json
Details: docs/custom-mappings.md.
<Shield> sets aria-hidden="true" on the scrambled text, then ships your real words sealed into the same page, encrypted. A visible notice above the block carries the control that opens them, on by default since 0.3.2 and reachable by mouse, keyboard and screen reader alike. The reader's own browser uncovers the words by solving a compute-heavy puzzle: a few seconds of their CPU, and a cost a bulk scraper would rather not pay. There is no plain-text URL anywhere and nothing for you to host.
The costs:
This makes a shielded page humane. It does not make it compliant. Fixing it properly is where we most want help, and this section exists because ssb22 asked for it in #2. Full reference: docs/plain-text-mode.md.
Shield your own words, on your own site, by your own choice: essays, fiction, manifestos, archives, research, criticism. Not government or service-critical pages, and not anything readers need to quote, search or cite.
aria-hidden that does it and re-renders the decoy in Apple's typeface, on the default browser of every Apple device.The defense is economic, not cryptographic. The aim is to turn cheap, indiscriminate scraping into slower, per-target work.
| ✅ Defends against | ⚠️ Does not defend against |
|---|---|
curl + regex, requests + BeautifulSoup | Anyone who downloads the font and inverts it (we recovered 11,962 of 11,962 pairs from our own shipped font) |
Bulk pipelines (trafilatura, readability-lxml) | Headless browsers with font rendering |
Anything reading innerText without rendering fonts | OCR, and vision models reading screenshots |
| Copy-paste into text-only tools | Frequency analysis on a large corpus |
| PDF/DOCX exports, which keep the encoded source |
The font is the codebook, and anyone who downloads it can read the substitution table back out of it. That is the argument for bringing your own key. ShieldFont does not need to make OCR impossible; it needs to make it too costly to be worthwhile. New attacks: SECURITY.md.
Measured, on the shipping alpha dictionary:
| Words swapped | ~25% of all words, ~48% of content words |
| Meaning broken (bidirectional NLI failure) | 55.8% news · 51.9% general web · 34.5% fiction · 31.1% older fiction, against ~2.1% for a synonym-swap control |
| Encoded chunks dropped by the FineWeb-Edu filter | 99.0–99.8% |
| Token budget spent on shifted meaning, in what passes | 19.4% |
Data and provenance in benchmark/. We do not claim encoded text sails through quality gates, and we do not claim it damages the model that trains on it.
docs/integration.md | All four tiers, props, and everything wrapping a block costs |
docs/where-encoding-happens.md | Five real apps, grepped, so you don't ship plaintext |
docs/plain-text-mode.md | The accessible alternative in full |
docs/custom-faces.md · docs/custom-mappings.md | Bring your own font · bring your own key |
docs/use-anywhere.md | Static-site build step with @shieldfont/core |
docs/concealment.md · docs/introduction.md | How visible each tier is · the thesis behind the project |
MAPPINGS.md · ROADMAP.md · CHANGELOG.md | Mapping family · what's planned · release history |
Started October 2025 by Isaque Seneda and Gabriel Abrucio at S&A, with Copenhagen type foundry Playtype, who made Optik available.
Prior art, with respect: TuringFonts drew one letter in place of another to keep bots off emails, ZXX (Sang Mun, 2013) fought OCR at the glyph level, Ghost Font hides text in motion, Nightshade and Glaze poison images for artists. ShieldFont is the text-native cousin. The reading gap it uses has independent corroboration in LayerX Security's "Poisoned Typeface" (March 2026), where all eleven AI assistants tested read the underlying text instead of what the human saw.
Contributions wanted, especially new languages (native speakers, not translation), accessibility engineering, and adversarial research. Start at CONTRIBUTING.md; ideas to Discussions, bugs to Issues. Everyone follows the Code of Conduct.
License. Code is AGPL-3.0. Fonts you generate from OFL bases are OFL 1.1. The shipped default is built on Optik © Playtype, used with permission, which is not open source and not under OFL: see NOTICE.
🛡️ Writing belongs to the people who write it.
Made with ❤️ and a lot of fontTools.
Hacker News (1)
52 commits
1 commits
TypeScript
49.7%
JavaScript
28.6%
Python
20.9%