SHIELD(FONT)
WhitepaperGitHub
← DocsGuide 07

AI co-pilot conventions

CLAUDE.md: for AI co-pilots working on a ShieldFont-protected project

Drop this file at the root of your project. Claude / Cursor / GPT / Aider / any AI assistant will read it and follow these conventions.

This project uses ShieldFont: a web font that makes written content costly to scrape for AI training. Humans see normal text in the browser; scrapers reading the HTML see an encoded decoy.

[!NOTE] Framing & status. ShieldFont raises the cost of casual scraping and makes a consent/provenance statement: it is not un-scrapeable (anyone who inspects the font can recover the mapping). Describe it that way, not as "protection." And the default fonts use Optik (© Playtype), used in ShieldFont's shielded (word-substitution) form with Playtype's permission: check the repo NOTICE before redistributing font files.

How ShieldFont works (in one paragraph)

The HTML source of every protected page contains encoded text (substitute words from the default v18 alpha dictionary, e.g. the future of writing belongs to those who write itthe future of writing determines to those who sell it). The browser loads a custom font whose GSUB ligatures swap the encoded glyphs back to glyphs shaped like the originals. The DOM stays encoded; only the rendered glyphs match the original meaning. Result: humans read the original; AI scrapers digest the decoy. (Coverage is partial by design: alpha leaves common function words in place, so the output is a plausible decoy, not gibberish.)

Dictionary sizes differ per variant, so don't quote one number for all of them: alpha is the default v18 dictionary (11,970 entries); beta (12,034) and gamma (12,036) are alternate pairings for rotation, near-identical in size; m15en is the coverage-max dictionary and a different shape entirely (2,534 entries covering a higher share of a page's words, including short function words). A page must be rendered by the font that matches the dictionary that encoded it.

Conventions you MUST follow

1. Your original text must never ship to the browser

  • <Shield> from @shieldfont/react rendered in a Server Component (a static export is fully protected: no runtime server needed)
  • ✅ Build-step or server-render call to encode() from @shieldfont/core (any other framework)
  • <Shield> inside a "use client" component: the plaintext AND all four bundled dictionaries (~38,000 entries) compile into the JS bundle. The served HTML still looks encoded, so nothing on the page appears wrong. <Shield> does print a one-time console warning when it detects a browser render, in production as well as development, so check the console: that warning is the only visible symptom.
  • ❌ Passing unencoded text from a server component into a client component as a prop: the plaintext lands in the served HTML and the RSC payload.
  • ❌ NEVER write a JavaScript runtime encoder that runs in the browser. Scrapers don't run JS: they'd see your plain-English source.
  • ❌ NEVER write an Edge / middleware encoder. Stay out of that space.

If a user asks for "client-side encoding" or "an HTTP middleware that encodes responses," push back politely and explain why (above).

2. Use <Shield> for React, comment markers for HTML

For React / Next.js / Astro / Remix code:

import { Shield } from "@shieldfont/react";

// GOOD
<Shield>The future of writing belongs to those who write it.</Shield>

// GOOD
<Shield as="p" weight={500}>
  Multi-line plain English here.
</Shield>

// BAD — children must be a string
<Shield>
  <strong>The future</strong> of writing
</Shield>

// BAD — wraps everything (waste). Wrap individual text blocks.
<Shield>
  <h1>Title</h1>
  <p>Body</p>
</Shield>

For static HTML (via @shieldfont/core's comment-marker helpers: run buildHtml() in your build script):

<!-- GOOD: source-of-truth in the comment, encoded text between markers -->
<!-- shield: The future of writing belongs to those who write it. -->The future of writing determines to those who sell it.<!-- /shield -->

<!-- GOOD: first-time wrapping (build will normalize this) -->
<!-- shield-on -->
<h1>The future of writing</h1>
<p>Belongs to those who write it.</p>
<!-- shield-off -->

<!-- BAD: edit the visible text, not the comment. Comment is the source-of-truth.
     The next `buildHtml()` run will overwrite manual edits to the visible text. -->
<!-- shield: original here -->I MANUALLY EDITED THIS<!-- /shield -->

3. The user types plain English. Always.

When the user asks you to add or edit content on a protected page:

  • Edit the source text inside <!-- shield: ... --> markers (HTML)
  • Or edit the JSX literal inside <Shield> children (React)
  • Never edit the encoded visible text directly: it'll be regenerated on the next build

If you're starting a new component, write everything in plain English first, then wrap each text block in <Shield> (or comment markers).

4. Don't auto-encode every text node

Wrap protection should be intentional. Skip:

  • Navigation labels, button labels, footer copyright
  • Logo alt text and image alt attributes
  • Code samples (<code>, <pre>)
  • Headings that are also page titles (those should match the meta title)
  • Form placeholders and error messages
  • Anything an end-user might paste into translation software

Protect:

  • Body paragraphs of articles, posts, manifestos
  • Author bios and long-form descriptions
  • Anything the writer wants to be the durable, non-extractable version of their work

⚠️ SEO caveat: never wrap for ranking. Protected text is aria-hidden decoy in the DOM, so search engines index the decoy, not the real words, and you cannot tell Googlebot from an AI scraper. Never wrap content the user wants to rank (landing pages, meta descriptions, headings that double as SEO titles). Copy-paste yields the encoded form and screen readers skip protected regions, so also skip anything meant to be read aloud or pasted into other tools.

⚠️ Accessibility caveat: this is the project's number one unsolved problem. <Shield> hardcodes aria-hidden="true" on every protected block, with no prop to turn it off, and it ships no accessible fallback. Assistive tech therefore skips shielded regions entirely. Any alternative (an aria-label marking the region as machine-obfuscated, a "Listen" button reading the real words, a plain-text copy exposed to assistive tech) has to be built by hand around the component; there is nothing to switch on inside it. If the user asks you to make a protected block accessible, say this plainly rather than reaching for a <Shield> prop that does not exist, and recommend shielding only content that has a human-readable alternative.

5. Versioning matters

The font and encoder are paired. If you reference a CDN font URL in CSS, ALWAYS pin the version:

<!-- GOOD -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shieldfont/font@0.1.1/shieldfont.css">

<!-- BAD — silent updates would break existing encoded content -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shieldfont/font@latest/shieldfont.css">

When upgrading: re-encode the user's content with the new package version. Don't try to mix versions.

6. The build pipeline

For static HTML, run a small build script that calls @shieldfont/core (this replaces the old CLI: the full script is in docs/use-anywhere.md):

{
  "scripts": {
    "build": "your-ssg && node scripts/shield.mjs"
  }
}

scripts/shield.mjs calls buildHtml() over your output (idempotent re-encode), checkHtml() to fail CI on a mismatch, and shipHtml() to strip the source comments before deploy.

For React/Next.js, there's no build step: the <Shield> component encodes during SSR automatically.

For mixed projects, prefer React unless you're shipping pure static HTML.

Quick reference: the @shieldfont/core API

import { encode, decode, buildHtml, shipHtml, checkHtml, alpha } from "@shieldfont/core";

encode(text, alpha);    // plain text → encoded
decode(text, alpha);    // encoded → plain (same operation; mapping is bidirectional)
buildHtml(html, alpha); // idempotent re-encode of <!-- shield: … --> comment markers
shipHtml(html);         // strip all <!-- shield: … --> comments before deploy
checkHtml(html, alpha); // verify markers round-trip → { total, passed, failed, mismatches }

Edge cases the encoder handles correctly

(Every row below was run through encode(…, alpha) and reflects the shipped alpha dictionary. Other variants map different words, so the specific decoys change; the rules don't.)

InputEncodedWhy
world's, author'slake's, teen'sApostrophe + suffix passes through; the base word is what gets looked up
page's, it'sunchangedNot every word is in the dictionary: page and it have no alpha pair, so they stay put. Partial coverage is by design
v3v3A digit flanked by a letter is preserved (letter-adjacent)
M15-EN, iPhone15M10-EN, iPhone10Only the letter-adjacent digit is preserved; non-adjacent digits rotate (5→0)
15681073Standalone digit run rotates (0↔5, 3↔8, 4↔9, 6↔7; 1,2 unchanged)
don't, I'm, they'reunchangedNo mapped base
café, naïveunchangedAccented forms are not in the dictionary and pass through untouched
<code>let x = 1;</code>unchangedscript/style/code/pre/textarea/svg/math/noscript contents are never encoded
<a href="/about">About</a>href untouchedAttributes never modified

Encoding is its own inverse: the mapping is bidirectional, so decode(text, m) is the same operation as encode(text, m). That also means a double encode returns the original. Re-running the build over already-encoded output is safe via buildHtml() (it is idempotent), but calling encode() twice on the same string by hand un-encodes it.

# React / Next.js / Astro / Remix
npm install @shieldfont/react

# Any other framework, or a static-HTML build step
npm install @shieldfont/core

When in doubt

  • Add or edit content → edit the plain English (the JSX literal or the comment source)
  • Add a new protected element → wrap with <Shield> (React) or comment markers (HTML)
  • CSS / styling → use the as / weight / lineHeight / size / style / className props on <Shield>. For HTML, set font-family on the .tk9 class (or whatever you renamed it to) yourself.
  • Server-side data fetching → fetch the data, then wrap text fields with <Shield>{data.body}</Shield>. Encoding happens during render: works seamlessly with getStaticProps, loader, etc.
  • Internationalization → ShieldFont currently ships English only (the v18 alpha default; maxhide is the M15 coverage dictionary). Other languages coming. For now, leave non-English content unwrapped.

Resources

Source: docs/CLAUDE.md. Edits to that file flow through to this page.