Kjeks Scanner
A standalone Node + Playwright scanner. Crawls your pages under every consent state and reports the trackers that actually fire.
101 First scan
The scanner runs outside WordPress. It drives a real Chromium browser through each consent state and records the network requests, cookies, and storage that appear — catching trackers injected by themes, embeds, or other plugins that are easy to miss by hand.
# Fastest — run straight from npm, no clone (Node 20+)
npx kjeks-scanner --url https://example.com/ --blog-id 1 --out scan
# Or install it globally, then use the kjeks-scanner / kjeks-scan command
npm install -g kjeks-scanner
kjeks-scanner --url https://example.com/ --blog-id 1 --out scan
# From a clone (for development)
npm install
npx playwright install chromium
node src/cli.js --url https://example.com/ --blog-id 1 --out scankjeks-scanner, so npx kjeks-scanner needs no clone. The first run downloads a pinned Chromium (~100 MB) via Playwright; later runs reuse it. After a global install, both kjeks-scanner and kjeks-scan work.The six consent states
Every path is loaded once per state, each in a fresh browser context:
| State | Consent injected |
|---|---|
before-choice | None — the banner hasn't been answered. |
reject-all | Everything optional denied. |
only-preferences | Only preferences granted. |
only-analytics | Only analytics granted. |
only-marketing | Only marketing granted. |
accept-all | All optional categories granted. |
kjeks_consent cookie and localStorage value before page scripts run — so blocking is evaluated exactly as a returning visitor would experience it.201 Config & authentication
Config file
For anything beyond a single URL, describe your sites in a config file and pass--config. Each site lists the paths to crawl and optionalscenarios (scripted click/wait steps for gated content like video embeds):
{
"sites": [
{
"url": "https://example.com/",
"blog_id": 1,
"policy_version": 1,
"paths": [ "/", "/about" ],
"scenarios": [
{
"name": "open a gated video",
"steps": [
{ "action": "click", "selector": ".kjeks-embed__load" },
{ "action": "wait", "ms": 1000 }
]
}
]
}
]
}Generate it from WordPress
Don't hand-write the site list — the core plugin can emit it. Use the CLI, or pull it from the REST route with credentials:
# Generate the config from WordPress, then scan it
wp kjeks scan-config --output=config.json
npx kjeks-scanner --config config.json --out scan
# Pull config straight from a live site (HTTP Basic auth)
KJEKS_USER=admin KJEKS_APP_PASSWORD='xxxx xxxx …' \
npx kjeks-scanner --config-url https://example.com/wp-json/kjeks/v1/scan-config --out scanInvocation forms
| Flag | Purpose |
|---|---|
--url + --blog-id | Scan one URL without a config file. |
--config <file> | Scan sites from a local config file. |
--config-url <url> | Fetch the config from a live site (needs KJEKS_USER + KJEKS_APP_PASSWORD). |
--overlay <file> | Merge extra paths/scenarios by blog_id. |
--concurrency <n> | Sites scanned in parallel (default 3). |
--per-host <n> | Parallel scans sharing one hostname (default 2) — politeness for subdirectory multisites. |
--full | Scan the server selection as-is; skip re-scanning pages that previously produced a tracker. |
--import [<url>] | After scanning, POST observations to the import endpoint in the same run. |
--out <dir> | Output directory (default scan). |
--endpoint <CDP> | Connect to an existing browser over the DevTools protocol. |
301 Output & CI
What it writes
One stable JSON file per site is written to <out>/<host>[_<path>].json. Each file records every state plus a flattened list of observations:
{
"host": "example.com",
"url": "https://example.com/",
"blog_id": 1,
"states": {
"before-choice": { "cookies": [], "scripts": [], "iframes": [], /* … */ },
"reject-all": { /* … */ },
"accept-all": { /* … */ }
},
"observations": [
{ "name": "_ga", "storage_type": "cookie", "party": "third",
"domain": ".example.com", "retention": "…",
"triggered_by": ["accept-all", "only-analytics"],
"source_urls": ["/", "/blog/hello-world/"] }
]
}Per state, the scanner captures:
cookies, localStorage, sessionStorage,indexedDB, thirdPartyHosts, beacons,scripts, iframes, and redirects. Each observation also records triggered_by — the consent state that caused it — and source_urls, the page(s) it actually loaded on, so you can see exactly which choice and which page produced a tracker. (The kjeks_consentcookie itself is excluded from results.)
source_urls), so sampling never drops a known-tracker page. Pass--full to scan the server selection as-is.Diffing & exit codes
On each run the scanner compares the new file with the previous one andexits with status 1 when anything changed. That makes it a natural CI gate: a non-zero exit means a new tracker appeared and your inventory needs review.
Close the loop
Once you've reviewed the results (optionally with theAI Reviewer), import them back into WordPress — either as a separate step, or in the same run with --import:
# Feed reviewed observations back into the inventory
wp kjeks import scan/example.com.json --blog_id=1scan-config and import surfaces. See thecore 301 guide for those.