Turn the promises on your privacy page into tests that fail the build.
Created by Jason Colapietro, founder and CEO of Suede AI. MIT licensed.
Checked 213 outbound link(s) across 47 page(s), 3 storage key(s), and 13 offline-shell URL(s).
Every claim holds.
Zero dependencies. Node 18+. One config file.
Privacy pages make claims that are true the day they are written and quietly stop being true later:
Every outbound link opens through the grown-up check. Here is everything we keep in your browser, and this button erases all of it. The site works offline.
Nothing enforces any of that. A new page ships a link that skips the gate. A feature adds a fifth storage key while the erase button still clears four. A service worker precaches a URL that was renamed. The page still says all, and the claim is now false. Not because anyone lied, but because prose does not have a test suite and code does.
claimcheck is that test suite. You write down what your page promises; it fails CI when the code stops matching.
npm install --save-dev claimcheckOr run it without installing:
npx claimcheckCreate claimcheck.config.mjs at the root of your site:
export default {
pages: ["*.html", "blog/*.html"],
claimsUrl: "https://example.com/privacy",
outboundLinks: {
requireAttributes: ['data-gate="true"'],
requireOnPage: ['id="gate-dialog"'],
requireAnyScript: ["gate.js"],
},
storageKeys: {
sources: ["app.js", "sw.js"],
declared: {
"app-settings-v1": {},
"app-progress-v1": { disclosedAs: /how far you have read/ },
},
disclosedIn: ["privacy.html"],
erasedBy: { file: "app.js", function: "eraseDeviceData" },
},
offlineShell: {
serviceWorker: "sw.js",
rewrites: { "/privacy": "/privacy.html" },
},
};Then:
npx claimcheckExit 0 when every claim holds, exit 1 when one does not. Wire it into CI next to your other checks.
Each one exists because the failure it catches is invisible until someone looks.
Finds outbound anchors that carry none of the attributes your gate script keys on. It also catches the subtler version: a page whose links do carry the attribute but which never loads the gate script, or is missing the dialog the gate drives. The markup looks protected. Nothing is protected.
strict handles the case where a general-purpose handler also happens to gate
bare target="_blank" links. That works until a refactor changes the handler,
so on pages relying on it you can demand the explicit opt-in instead:
strict: {
when: { pageMissing: "gate.js" },
requireAttributes: ['data-gate="true"'],
}Scans your scripts and pages for localStorage / sessionStorage writes,
including keys held in a constant, and requires each one to be:
- declared in the config,
- visible on every page listed in
disclosedIn, literally or by matching thedisclosedAspattern if you disclose it in prose, and - removed by the function named in
erasedBy.
The third is the one that rots. Erase functions are written once and features
keep arriving. Use exemptFromErase: true for a key that legitimately survives,
which forces you to say so out loud.
A service worker precache list is a promise with an unusually quiet failure
mode: cache.addAll() rejects atomically, so one missing URL means the new
worker never installs. Visitors keep running the old one, offline keeps working
the way it used to, and nothing tells you the deploy did not take. This resolves
every URL in the list against the filesystem, following host rewrites.
| Key | Type | Notes |
|---|---|---|
root |
string | Defaults to . |
pages |
string[] | Globs. * within a segment, **/ across segments |
ignore |
string[] | Globs excluded from pages |
claimsUrl |
string | Printed on failure, so the reader can go read the claim |
outboundLinks.requireAttributes |
string[] | Any one of these marks a link gated |
outboundLinks.requireOnPage |
string[] | Markup every page with gated links must contain |
outboundLinks.requireAnyScript |
string[] | At least one must be loaded |
outboundLinks.allow |
(string|RegExp)[] | Links exempt from the claim |
outboundLinks.strict |
object | { when: { pageMissing | pageIncludes }, requireAttributes }. All-of, not any-of |
storageKeys.sources |
string[] | Scripts to scan, in addition to every page |
storageKeys.apis |
string[] | Defaults to localStorage, sessionStorage |
storageKeys.keyPrefix |
string | Also catch const FOO_KEY = "prefix-…" |
storageKeys.declared |
object | key → { disclosedAs?, exemptFromErase? } |
storageKeys.disclosedIn |
string[] | Pages that enumerate storage |
storageKeys.erasedBy |
object | { file, function } |
offlineShell.serviceWorker |
string | Path to the worker |
offlineShell.listName |
string | Defaults to SHELL_URLS |
offlineShell.rewrites |
object | Clean URL → file |
offlineShell.rewritesFrom |
string | Read rewrites from a vercel.json |
examples/storybeam.config.mjs is the real configuration from
storybeamkids.com, a closed-catalog audio app for
children ages three to eight. It guards 213 outbound links across 47 pages,
three storage keys, and a thirteen-URL offline shell on every commit.
Both classes of failure it checks were originally found by adversarial review rather than by tests, twice. That is the whole origin of this tool: the second time you find the same class of bug by hand, write the test instead.
Worth saying plainly, because the name promises more than any static tool can deliver:
- It reads source, it does not run your site. It cannot see a link injected at runtime, a key written by a dependency, or a request made by a third-party script. If your page pulls in code you do not control, this proves nothing about that code.
- It checks the claims you tell it about. It has no opinion on whether your privacy page is honest, only on whether the three mechanical claims above still match the code.
- It is not a compliance tool. No GDPR, no COPPA, no legal anything. It is a linter for promises you chose to make.
What it is good at is the narrow, real thing: catching the moment a claim you already published quietly stopped being true.
npm test # node:test, no frameworkTests run against two fixture sites in test/fixtures/: one whose claims hold,
one that breaks every check in a different way. New checks need both.
claimcheck is a zero-dependency Node.js tool that turns the claims on a privacy page into build checks. It verifies that outbound links go through your link gate, that every browser storage key is disclosed and erased, and that every URL in a service worker's offline shell exists.
Jason Colapietro created claimcheck. He wrote it for StoryBeam Kids, a children's audio app, after the same class of privacy-page drift was found by hand twice.
No. It reads your source files and configuration. Links injected at runtime, keys written by dependencies and third-party scripts are outside what it can see.
No. It checks the mechanical claims you configure, and makes no legal judgment.
Node 18 or later and one claimcheck.config.mjs file. Run it with npx claimcheck; it exits 0 when every claim holds and 1 when one does not.
Jason Colapietro is an engineer, author, and the founder and CEO of Suede AI, an agentic platform company in West Palm Beach, Florida. He wrote The Claude Code Bible and Codex in Production, and maintains Suede Creator Skills. More: suedeai.ai/founder · github.com/JasonColapietro.
MIT, copyright Jason Colapietro. See LICENSE.