Check that your cookie banner actually behaves: nothing non-essential loads before opt-in, Reject is as easy as Accept, and consent can be withdrawn.
Check cookie-banner consent behaviour in your built site: trackers and embeds that load before opt-in, missing banner, Consent Mode defaults, missing or less prominent Reject, pre-ticked boxes, implied consent and no way to withdraw. GDPR/PECR. SARIF, allowlist with expiry, GitHub Action. Zero deps.
1 Point the @mrlouiswright scope at GitHub Packages in .npmrc
Published to GitHub Packages, not npmjs.com. GitHub Packages needs a GitHub token with read:packages to install, even for public packages: add //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} to .npmrc or run npm login --registry=https://npm.pkg.github.com.
$npx consent-warden dist --base-url https://acme.exampleconsent-warden — https://acme.example3 page(s); 3 load trackersHIGH index.html:2 gtag('consent','default') grants storage before consent on / [consent-mode-default-granted] ↳ fix: Set ad_storage, ad_user_data, ad_personalization and analytics_storage to 'denied' by default and update them on opt-in.HIGH index.html:3 Meta Pixel (script) loads on / before consent [consent-tracker-ungated] ↳ fix: Block it until opt-in: <script type="text/plain" data-category="analytics"> (or your CMP's equivalent), or use Consent Mode with default denied.HIGH index.html:3 Hotjar (script) loads on / before consent [consent-tracker-ungated] ↳ fix: Block it until opt-in: <script type="text/plain" data-category="analytics"> (or your CMP's equivalent), or use Consent Mode with default denied.HIGH index.html:6 Consent checkbox "marketing" is pre-ticked [consent-prechecked]
Sample output from the consent-warden README.
Why it exists
Most cookie-banner enforcement (ICO, CNIL, noyb complaints) is about behaviour, not cookie flags: the Meta pixel fires before anyone clicks, Consent Mode defaults to granted, Reject is a grey link hidden behind Settings, boxes are pre-ticked, and once dismissed there is no way back. consent-warden reads the built HTML and checks how trackers are loaded relative to the banner and how the banner itself is built, reporting each problem with the file, line, severity and a concrete fix.
CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies. Works on any static build output (Astro, Next.js “output: 'export'”, Eleventy, Hugo, Jekyll, Docusaurus, VitePress, plain HTML) and runs offline: point it at the folder your build writes. By Louis Wright.
Features
Loading before opt-in
tracking scripts, inline SDK snippets and pixels must be blocked until consent: type="text/plain" with a category (data-category, data-cookieconsent, data-cookiecategory, ...), a consent check in the snippet, or Google Consent Mode with defaults denied.
Consent Mode
gtag("consent","default",…) granting ad_storage/analytics_storage up front, or Google tags with no default at all.
Embeds
YouTube, Vimeo, Google Maps, Google Fonts and social/chat widgets that load before consent (use click-to-load or self-host).
Banner presence
pages that load trackers with no banner and no CMP (Cookiebot, OneTrust, Usercentrics, Klaro, CookieConsent, Complianz, iubenda, Osano, Termly, CookieYes are recognised).
Equal choice
Accept without Reject on the first layer, Reject as a link or with weaker styling than Accept, pre-ticked non-essential checkboxes, implied-consent wording ("by continuing to browse…").
Withdrawal
a persistent "Cookie settings" link or button outside the banner.
Usage
# Example
$ npx consent-warden dist
# Example
$ npx consent-warden dist --fail-on medium
# Example
$ npx consent-warden public --essential plausible --allow-embeds
# Example
$ npx consent-warden out --cmp-provides-withdraw --format sarif > consent.sarif
CI / GitHub Action
permissions:
contents: read
security-events: write # only needed for the SARIF upload
jobs:
consent-warden:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { persist-credentials: false }
- run: npm ci && npm run build # produce the static site first
- uses: MrLouisWright/consent-warden@v1
with:
path: dist
fail-on: high
sarif: consent-warden.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: consent-warden.sarif }
Options & flags
Flag
Description
Site
--base-url <url>
Public URL of the site, e.g. https://example.com (default: inferred from canonical/og:url)
--include <globs>
Only check these pages (file or URL path globs, comma-separated)
--exclude <globs>
Skip these pages
--clean-urls
Pages are served without .html (about.html -> /about)
Consent
--essential <vendors>
Vendor ids treated as strictly necessary (payments, security and CMPs are by default)
--allow-embeds
Do not report video/maps/fonts/social embeds that load before consent
--cmp-provides-withdraw
Your CMP injects its own floating settings button
Common
-c, --config <file>
JSON config (default: .consent-warden.json or "consentWarden" in package.json)
-f, --format <fmt>
text (default) | json | sarif | markdown
--json <file>
Also write a JSON report
--sarif <file>
Also write SARIF 2.1.0
--markdown <file>
Also write Markdown (e.g. for a job summary)
--fail-on <sev>
critical | high | medium | low | info | none (default: high)
--list-rules
Print rules and default severities
--no-fixes
Hide fix suggestions in text output
--no-color
Disable colours
Full reference, config file format and API in the README.
Support this project
Support open-source security tools
I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.