ShieldFont: the font swaps a word whether it is the original or the decoy
A typeface that protects written content by poisoning unauthorized AI training datasets.
At a glance
- What is it?
- ShieldFont encodes the words in a page against a substitution dictionary and reverses the substitution inside the font at render time, so a reader sees the real text and a scraper that never renders fonts gets fluent nonsense. It is a self-described alpha under a copyleft licence, and the readme is unusually candid about where it fails.
- Who is it for?
- ShieldFont suits someone publishing written work who wants scraping to cost more than it is worth, and who will keep the encoding on a server. Before you ship it, respect the one rule it states plainly: the original text must never reach the browser in readable form, so encoding belongs in a server render or a build step rather than in a client component.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The font swaps a word whether it is the original or the decoy
The mechanism has one property that explains almost every warning in the documentation. The substitution dictionary is an involution, which means applying it twice returns the original word, so the font cannot tell which of the two it is looking at: if you hand it the real word it produces the decoy, and if you hand it the decoy it produces the real word. That is what lets the same page show the truth to a human and the decoy to a scraper. It also creates the sharpest footgun in the project. If you set the font family yourself on an element, you get the decoy, silently, and nothing errors, because nothing is wrong from the font's point of view. The readme is emphatic about this and gives the two safe routes: use the component that marks text as unprotected in React, or name the neutral cut of the family, a different file in the same package that cannot substitute anything.
Encoding must run in Node, or the plaintext ships in the bundle
One rule governs the whole integration, and breaking it produces a page that looks protected while leaking everything. Your original text must never reach the browser in a readable form, which means the encoding step runs on a server or during a build and never inside a client component. The failure mode is quiet: the decoy renders correctly, the notice appears, and the real sentence is sitting in the JavaScript bundle where any scraper can read it. That constraint is also why the recommended React component cannot protect a client-only single-page application, and the readme names two of them by name. For those cases the answer is a different package that you call from your own build script, then import the already-encoded string. The distinction between the tiers is not a preference; it is the difference between a working defence and a decorative one.
Reader Mode is now documented browser by browser
The most useful change since launch is a correction rather than a feature. The documentation used to overstate what the project does, and it now says plainly what happens when a reader turns on the built-in reader mode: two of the major browsers drop the protected block entirely, and the third shows the scrambled version instead. That is a real limitation with a real consequence, since the whole premise depends on the page rendering with the shipped font, and reader mode is a path around that. The response documented alongside it is a change of recommendation rather than a workaround. Anyone actually shipping a site is pointed at the server-rendering package, and the paste-in tier is explicitly left for trying the idea out. That is the honest position: the educational path is not the supported one, and the readme now says so.
What a screen reader keeps is the decoy, deliberately
The accessibility design is the part of this project with the most thought behind it, and one decision is counterintuitive. When a screen reader encounters a protected block, what it is given is the scrambled text, not the real words, because a decoy read aloud is fluent, grammatical and wrong, which the project argues is worse than silence. The scrambled text is therefore marked as hidden from assistive technology, and the real words arrive through the visible notice instead, which carries a control that opens them. That control is reachable by mouse, by keyboard and by screen reader, and it used to be clipped off the page, which the changelog lists as a fixed defect. One press opens every block on the page. Testing is stated unevenly on purpose: the path is driven against a real screen reader in continuous integration and checked by hand with one other, while a third is explicitly untested.
Six weights, and a font copy step that is not optional
The recommended tier ships six weights and points at no public content delivery network, by design. That choice has a cost the readme spells out: you copy the font files out of the installed package into your own public directory once, and if you skip it every font request returns not found and the font-load guard blanks each protected block behind a skeleton placeholder. So the page does not fall back to readable text; it falls back to nothing, which is the correct failure and a confusing one if you did not know the step existed. If you would rather serve the files from somewhere else, there is a call to set the font host and the component asks for fonts from the path you give it. The copy step is one command:
cp node_modules/@shieldfont/react/fonts/*.woff2 public/fonts/The reason for refusing a public CDN is the same reason the whole project exists: a font served from a shared host is one more place your encoding assumption could be broken by someone else's configuration.
The pipeline is TypeScript and Python in the same repository
The monorepo has three workspace packages and the build script builds two of them, with the third distributed as a stylesheet and a font for the paste-in route. What is interesting is how the typeface itself is produced. There is a Python script that takes a base font from an upstream repository at a pinned tag, caches it, and emits the project typeface with substitutions baked into a specific OpenType feature. The three Python dependencies needed for that are declared in a requirements file sitting next to the package manifest at the root of a TypeScript repository, which is the one piece of tooling in this project that is not JavaScript. The development dependencies tell you what the project's testing story is: a virtual screen reader driver for automated accessibility checks, an accessibility rule engine, a browser automation library, and a bundler. Four of the five test scripts in the manifest run audits rather than unit tests.
Two licence files, a v0 alpha label and no releases
The licensing is split because the assets are not the code. The manifest declares a copyleft licence with the or-later clause for the packages, and the repository root carries a second, separate licence file covering the fonts, which is the arrangement you would expect for a typeface derived from an upstream font with its own permissive terms. There is also a notice file, which the copyleft licence family expects. On maturity the project is explicit rather than coy: it calls itself a version zero alpha, says it publishes what does not work, says it is still measuring, and asks for outside help. The current release is v0.3.5 with a default mapping labelled as an alpha, and there are no tagged releases on the repository at all, so the manifest is the only version signal. Evidence lives in a benchmark directory and a white paper that says what the benchmarks measure rather than only reporting numbers.
Editorial conclusion
ShieldFont suits someone publishing written work who wants scraping to cost more than it is worth, and who will keep the encoding on a server. Before you ship it, respect the one rule it states plainly: the original text must never reach the browser in readable form, so encoding belongs in a server render or a build step rather than in a client component. Read the accessibility section first, because the readme says which screen readers are tested and which are not, and it asks you to check your own country's accessibility law before publishing. And treat the version as an alpha whose caveats are documented rather than smoothed over.
Frequently asked questions
What is Shield Font and how does it work?
It encodes the words in your HTML against a substitution dictionary and ships a font whose OpenType rules reverse that substitution at render time. Readers see the real text; anything collecting the HTML without rendering fonts collects the decoy.
Why does ShieldFont warn against setting the font family yourself?
The substitution dictionary is an involution, so the font swaps a word whether it receives the original or the decoy. Setting the family by hand therefore renders the decoy with no error; use the unprotected component or the neutral cut of the family instead.
Can ShieldFont protect a client-only single-page app?
Not through the React component. Encoding must run in Node or a build step, so a client-only app needs the build-step package to pre-encode the string and import it, as the documentation explains.
Is ShieldFont accessible to screen readers?
The real words are reachable through a notice control that works with mouse, keyboard and screen reader, and the scrambled text is hidden from assistive technology on purpose. The path is tested against one real screen reader in continuous integration and by hand with another, and a third is untested.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/isaqueseneda-shieldfont)