PrivaLint checks websites in a real browser
Privacy checks as a CLI, GitHub Action and Node.js API. PrivaLint 0.10.0 with 33 rules, Impressum and privacy policy checks, an HTML report and a changelog of every version.A consent banner can look right while analytics keeps sending requests. That is why I built PrivaLint. As a CLI and GitHub Action it loads websites in a real Chromium browser and checks for likely privacy problems: what loads on the first visit and which trackers stay active after a click on “Reject”.
PrivaLint uses Playwright and is MIT-licensed. The current version, 0.10.0, is published on npm. Next to the CLI and the GitHub Action there is a Node.js API. Source code and issue tracker live on GitHub. What changed since the first version is in the changelog at the end.
Without installing anything, you can check your website with the privacy check. The web scanner runs PrivaLint 0.10.0 and shows findings and suggested fixes in the browser. It does not run the extra passes for banner settings, closing, accepting and Global Privacy Control, or the HTTPS rule; those need the CLI. Why it exists is covered in the post about the privacy check. For larger crawls, your own rule settings and CI, use the CLI.
You need Node.js 20.12 or newer and Chromium. Replace the example address with a site you operate. The scan checks up to ten pages, loads three at a time, writes German and uses the profile for Germany.
Each finding carries evidence and a suggested fix. Severities are the tool's defaults, not a legal assessment of every third-party request. The rule documentation describes each check. For every rule and page the report states whether it was checked without a finding, found something or could not be checked, and why. That keeps “nothing found” apart from “not looked at”.
The privacy check on this site runs 28 of the rules, grouped by the three stations of a test visit: arriving without clicking; saying no to the banner; required information and forms. trackers-after-settings, consent-preselected, trackers-after-close, gpc-signal and https-transport only run in the CLI. Every rule appears in the report with one of four states: with a finding, without a finding, not checkable (with the reason, for example no supported banner was recognised) or not needed. Not needed means there was no banner and nothing that needs consent loaded before a choice. Without a finding is not a pass, only: nothing was detected in what loaded.
The two shop rules only run on the home page of sites that look like a shop: a cart or checkout link, or scripts of a shop system. Other sites get no findings from them. If you don't need one of the new rules, switch it off with --disable or under "rules" in the config.
--lang de writes findings, fixes and reports in German; --lang en stays the default. --region de|at|ch|hu|eu picks the profile for Germany, Austria, Switzerland, Hungary or the EU. Every finding lists the legal references of the chosen profile.
The Hungarian profile cites Eht. 155. § (4) for cookies and Ekertv. 4. § for the imprint. PrivaLint recognises Hungarian banner buttons such as “Elfogadom”, “Elutasítom” and “Csak a szükségesek”, settings links such as “Süti beállítások”, and “Impresszum” and “Adatkezelési tájékoztató”. Since 0.10.0, --lang hu writes Hungarian reports too.
The Swiss profile rates trackers before consent, third-party embeds and missing Impressum links one level lower. Trackers that keep running after an explicit rejection stay an error. For a Swiss site with EU visitors, run a second scan with --region eu.
The profile controls references and severities in the tool. It does not determine which law applies to a website, nor where the browser is. --locale sets the browser locale separately.
Every page starts in a fresh browser context with no cookies and no stored data. PrivaLint waits for the network to go idle plus a settle delay and records requests, cookies, links and resource hints. On the home page it looks for a consent banner, clicks Reject, reloads, records a second state and checks that the settings can be reopened. If a banner shows up late, PrivaLint keeps looking for one more settle period; consent.bannerWaitMs records how long.
Before clicking Reject, PrivaLint presses the Tab key up to 60 times and measures whether Reject is as visible as Accept. It loads the Impressum and privacy policy the home page links once more and reads their text, plus a cookie policy the privacy policy links, also as a PDF on the same site. Trackers routed through the site's own domain are recognised by path and parameters, such as GA4 under /g/collect. Native scans also resolve the CNAME records of subdomains to find trackers hidden behind them. When the site has more hosts such as blog. or docs., the first crawled page of up to five of them gets the same start-page checks; --host-checks main turns that off.
Detection uses the selectors of 20 consent managers, inside iframes too: Axeptio, Borlabs Cookie, Complianz, consentmanager, Cookie Information, Cookie-Script, CookieConsent, Cookiebot, CookieFirst, CookieHub, CookieYes, Didomi, iubenda, Klaro, OneTrust, Osano, Sourcepoint, Termly, TrustArc and Usercentrics. Other banners are found by button labels in seven languages: German, English, French, Spanish, Italian, Dutch and Hungarian. If a site needs no banner because nothing requiring consent loads before a choice, PrivaLint skips the banner rules with the reason no-consent-needed.
With --check-settings PrivaLint goes one step further: it loads the home page again in a fresh browser, opens the banner settings, switches off every optional category, saves and reloads. If a TCF banner has a “Legitimate interest” tab, PrivaLint objects there too. It then reads __tcfapi('getTCData') to see which purposes and vendors the banner still reports as on. Trackers that still load are reported by trackers-after-settings. Without the flag the rule is skipped, and the report does not count as incomplete.
Trackers are recognised by hosts, cookies and the names of localStorage and sessionStorage entries from a maintained list, which since 0.9.0 includes Adobe Analytics, etracker, INFOnline, Piwik PRO, Heap, Pendo and Optimizely. Values are never read. Cookies missing there are looked up in about 1,000 analytics and marketing cookies from the Open Cookie Database, by cookie name only.
The crawler follows internal links breadth-first, drops query strings and merges duplicates after redirects. Pages selected by a query parameter, such as /?page_id=2, are crawled as separate pages. The public suffix list decides what counts as one site: blog.example.de and shop.example.de belong together, two sites under github.io do not. With --concurrency it loads several pages at once. The report stays the same as with one page, because pages are still committed in breadth-first order. Pages that fail are listed without stopping the crawl.
- --format picks text, JSON, Markdown or SARIF. Several --output options write several reports; .json, .md and .sarif set the format.
- --fail-on error|warning|never sets the threshold. Exit code 0 means below it, 1 means findings and 2 invalid arguments or a failed scan.
- --fail-on-incomplete exits with 3 when required observations are incomplete. Findings (exit 1) take precedence.
- privalint.config.json or --config sets severities per rule and a vendor allowlist. Allowed vendors stay visible as info with a note; the allowlist does not replace consent.
- --disable takes comma-separated rule ids. --skip-consent skips the click on Reject, --check-settings also tests the banner's settings layer.
- --check-accept accepts in a fresh browser and records what loads then, --check-close closes the banner with its close button, --gpc sends Global Privacy Control. Each loads the home page once more.
- --format html writes a self-contained HTML report for light and dark. --baseline report.json marks each finding new or existing, and --fail-on then counts new ones only.
- --device "iPhone 15" checks the banner in its mobile layout, --sitemap picks the pages from the sitemap, pages with forms first.
- --settle waits 2000 ms by default, --timeout is 30000 ms per page and --locale is de-DE.
- --headed opens a visible window, --channel chrome or --channel msedge uses an installed browser. --sandbox runs Chromium with its sandbox, recommended for sites you don't trust.
The Action writes a Markdown report to the job summary and produces SARIF. Uploading it shows the findings in GitHub code scanning where available. With pr-comment: true and the pull-requests: write permission it creates a comment on the pull request and updates it on every run. Forks and read-only tokens skip the comment without failing the scan.
Further inputs are url, config, max-pages, concurrency, fail-on, fail-on-incomplete, disable, lang, region, check-settings, sarif-file and job-summary, since 0.10.0 also baseline, check-accept, check-close, gpc, device and sitemap. Since 0.9.0, max-pages, concurrency, lang, region and fail-on-incomplete default to empty, so the values in privalint.config.json apply. The Action returns exit-code and, when a report was written, sarif-file. The runner needs Node.js 20.12 or newer. The example scans a fixed URL; pass your host's preview URL to test a pull request's changes. Live pages map to synthetic paths such as privalint/your-site.de/kontakt, so an alert has no source file to open.
The API takes the same options as the CLI. It never loads a config file on its own; you pass severities and the allowlist explicitly.
To put PrivaLint behind a public form, as the privacy check does, the repository has a guide to the controlled scanner: every request passes your own network adapter with limits on count, size, redirects and time.
- Reject is tested and, with --check-settings and --check-close, switching everything off in the settings and closing the banner. --check-accept records what loads after accepting but does not judge it as a finding.
- PrivaLint checks the Impressum and privacy policy for single details: address, email, register entry, the controller's contact and the services named. Whether the texts are correct and complete is not judged.
- Tracker detection relies on a maintained vendor list and the Open Cookie Database. Unknown vendors and server-side tracking can go unnoticed.
- Sites can hide banners from headless browsers or serve block pages. Check the scanned URL and page count, and use a visible browser if needed.
- Consent behaviour depends on location. GitHub-hosted runners often have US addresses. For EU behaviour you need an EU runner or a matching proxy.
- Without --sandbox, Chromium runs without its sandbox, as in Playwright's default. Scans of unknown sites belong in an isolated environment without secrets.
With 0.10.0 PrivaLint reads the Impressum and privacy policy itself for the first time, and --baseline compares two scans. More region profiles and report languages are planned next.
Test your own site, report a false positive or contribute a rule through the privalint repository. The contribution guide explains the workflow.
Every version with its main changes. The full list is in CHANGELOG.md, and each version under Releases.
- 16 new rules, among them banner-keyboard, reject-less-prominent, fingerprinting, legal-notice-content, privacy-policy-vendors, privacy-policy-content, third-party-personal-data, form-preticked-consent and mixed-content. They run by default and may add findings to existing scans; mixed-content reports a form over HTTP as an error.
- The Impressum and privacy policy are loaded once more and read, a linked cookie policy also as a PDF.
- Trackers routed through the site's own domain are recognised, and in native scans trackers behind CNAME records.
- Opt-in checks --check-accept, --check-close and --gpc; also --device, --sitemap and start-page checks on up to five more hosts of the site.
- HTML report with --format html, comparison with an earlier report through --baseline, every third-party host in thirdParties and a per-rule summary in ruleSummary.
- Fix guides for Cookiebot, Usercentrics, OneTrust and Complianz; when PrivaLint recognises the consent manager, the finding links the matching section.
- The Hungarian profile now comes with Hungarian reports (--lang hu).
- Report schema 1.5.0; 1.0.0 to 1.4.0 stay supported.
- Six new rules: consent-banner-missing, banner-legal-links, form-privacy-notice, https-transport, withdrawal-button and accessibility-statement. They may add findings to existing scans.
- --check-settings and trackers-after-reject read the IAB TCF API. On TCF banners --check-settings also objects to legitimate interest.
- Late banners are found, and trackers are also recognised by the names of localStorage and sessionStorage entries.
- New trackers: Adobe Analytics, Target, Audience Manager and Experience Platform Tags, etracker, INFOnline, Piwik PRO, Heap, Pendo, Optimizely, Chartbeat and Comscore. The Facebook SDK is its own social vendor instead of the Meta Pixel.
- New embeds: SoundCloud, Mapbox, Trusted Shops, Pinterest and Google Calendar. Error reports to Bugsnag, Rollbar and Datadog no longer make a capture incomplete.
- Tracker, embed and font rules count requests with an HTTP error too: a pixel that answers 403 still received the IP address and referrer.
- Action: new inputs config and check-settings; empty defaults let privalint.config.json apply.
- Fixed: credentials in the target URL no longer appear in any report. “Confirm all” no longer counts as the save button, and offer buttons in a footer no longer count as a cookie banner.
- Report schema 1.4.0 with timing, TCF data and the banner wait; 1.0.0 to 1.3.0 stay supported.
- Skipped error reports to Sentry ingest hosts no longer make a page incomplete. The page never uses their response, and their URL stays recorded.
- The coverage check follows capture.complete. Load diagnostics that hide nothing no longer mark the page as incomplete.
- trackers-after-reject and trackers-after-settings cite Art. 6(1)(a) GDPR instead of Art. 7(3) GDPR. reject-option no longer cites Art. 7(3) GDPR.
- Usercentrics v3 banners are recognised by their buttons.
- Intercom is recognised on all documented hosts, regional ones included.
- Contentsquare's opt-out cookie _cs_optout no longer counts as a tracker.
- Region hu for Hungary citing Eht. 155. § (4) and Ekertv. 4. §; Hungarian banner buttons, settings links, imprint and privacy notice are recognised.
- Optional check of the settings layer with --check-settings and the new rule trackers-after-settings.
- About 1,000 analytics and marketing cookies from the Open Cookie Database extend the tracker list.
- Chat and review widgets in third-party-embeds: Intercom, Tawk.to, Zendesk Web Widget, Trustpilot and Typeform.
- First-party detection through the public suffix list, including github.io, netlify.app and vercel.app.
- Report schema 1.3.0; 1.0.0 to 1.2.0 stay supported.
- When a page cancels a request itself, such as a prefetch on a slow network, the page no longer counts as incomplete. The request shows as skipped with cancelled-by-page, and its URL still counts for the tracker rules.
- Real failures such as adapter errors, timeouts, connection, TLS and DNS problems still limit the result.
- Report schema 1.2.0 adds the new reason; 1.0.0 and 1.1.0 stay supported.
- Action input concurrency for parallel crawls in CI.
- Axeptio, TrustArc, CookieHub, Cookie Information and Cookie-Script are recognised by their buttons.
- Session-replay services FullStory, Mouseflow, Smartlook, Lucky Orange and Contentsquare in the tracker list.
- On sites without a banner where nothing requiring consent loads, the banner rules are skipped with no-consent-needed instead of being reported as not checkable.
- New rule adobe-fonts for fonts from Adobe Fonts.
- New rule consent-settings-link: can consent be reopened after rejecting?
- Criteo, Taboola, Outbrain, Adform and Quantcast in the tracker list.
- --sandbox runs Chromium with its sandbox, --concurrency loads several pages at once; on a six-page site it halved the scan time.
- Fixed: the Osano reject button is found again.
- A beacon the network adapter deliberately does not send no longer makes the page incomplete. Its URL is still checked by the tracker rules.
- JSON configuration with per-rule severities and a vendor allowlist.
- Optional pull request comment from the Action.
- Coverage gate with exit code 3 (--fail-on-incomplete) and JSON schemas for the report format.
- hCaptcha, Turnstile, OpenStreetMap, TikTok and Calendly as embeds; CookieFirst, Osano, Termly and Sourcepoint as consent managers, inside iframes too.
- Controlled capture through a network adapter per request, with limits on count, bytes, redirects and time. This is the basis of the web scanner.
- Load diagnostics and coverage status per page and rule.
- Failed or blocked requests no longer count as evidence of a loaded vendor.
- --lang de for German findings and reports.
- --region de|at|ch|eu with legal references per country and softer Swiss severities.
- lang and region inputs for the Action, plus a German README.
- --headed for sites that hide banners from headless browsers, and --channel chrome|msedge for installed browsers.
- First release: scanner in a real Chromium, Reject test on the home page, eight rules, a CLI with text, JSON, Markdown and SARIF output, and the GitHub Action.