Written from packages/cli/src/cli/*.ts (void 0.1.0, 2026-09-16). If void --help disagrees with this file, trust void --help and report the drift.
Install and run: the plugin ships the CLI in packages/cli (Bun ≥ 1.2). Run bunx void … or bun packages/cli/src/index.ts … from a checkout, or void … when it's linked or installed globally. It needs a Chromium: resolved from VOID_CHROMIUM → CHROME_PATH → system paths (/usr/bin/chromium, Chrome.app…) → the Playwright cache. void doctor shows what it found.
Commands
| Command | Purpose | Exit |
|---|---|---|
void audit [url] |
Run every page category: perf, smooth, a11y, seo, geo, design (+ lint when src is set) |
0/1/2/3/4 |
void perf [url] |
Only performance (web-vitals + resources [+ Lighthouse]) | same |
void smooth [url] |
Only smoothness (scripted scroll, interactions, animations, reduced motion, rAF, WebGL) | same |
void a11y [url] |
Only accessibility (axe WCAG 2.2 AA, focus visibility, target size, reflow) | same |
void seo [url] |
Only SEO (status, head, canonical, links, OG, icons, robots, sitemap) | same |
void geo [url] |
Only AI search (raw vs rendered content, JSON-LD, AI robots, llms.txt, Markdown mirrors) | same |
void design [url] |
Only design heuristics (type, color, spacing, overflow, slop tells) | same |
void lint [dir] |
Static source checks, no browser. dir defaults to config src, else . |
0/1/2 |
void rules |
List all rules | 0 |
void rules |
Show one rule: title, why, fix, example, refs | 0 / 2 if unknown (suggests near matches) |
void rules |
List a category (void rules perf) or prefix (void rules seo/canonical) |
0 |
void report |
Re-render a saved JSON report (--format md|pretty|json) |
0 if report passed, else 1 |
void extract |
Extract a site's design DNA (tokens, type, color) to dna.json, brief.md, direction.css. (Module in progress: may print "not available in this build") |
0/2/4 |
void doctor |
Check Bun, Chromium launch, playwright-core, axe-core, web-vitals, optional Lighthouse, config | 0/2/4 |
url may be omitted when --start or config url/start provides it. Bare hosts get a scheme: localhost:3000 → http://, example.com → https://.
Audit flags (audit and the category shorthands)
| Flag | Values / default | Notes |
|---|---|---|
--only |
perf,smooth,a11y,seo,geo,design,lint |
Comma list. The shorthands set this for you |
--profile |
mobile | desktop | both (default both) |
mobile = 412×823 @2.625x, 4× CPU, 150 ms RTT, 1.6/0.75 Mbps; desktop = 1350×940, unthrottled |
--routes |
/,/pricing |
Paths resolved against the URL. Default: the URL's own path |
--runs |
1–20 (default 3) | Perf runs per profile; the median is reported. Use 5 near budgets, 1 for quick iteration |
--lighthouse |
boolean | Also run Lighthouse 13 (optional peer: bun add -d lighthouse) |
--start |
"bun run start -p 3100" |
Spawns the command (with PORT and NODE_ENV=production set), waits for 2xx on readyPath, audits, then kills the process group. Requires --port |
--port |
1–65535 | Port the started server listens on. The URL becomes http://localhost: |
--src |
./src |
Also lint this directory as part of the audit |
--kind |
marketing | app |
Selects default budgets (JS 170 vs 300 KB, etc.) |
--screenshots |
dir | Save fold + full-page screenshots (design audit) |
--format, -f |
pretty (TTY default) | md | json |
md = compact for agents, grouped by rule |
--json |
boolean | Same as --format json; only JSON on stdout, progress on stderr |
--out, -o |
report.json | report.md |
Also write the report to a file (format from extension) |
--max-per-rule |
number (md default 5) | Occurrences listed per rule |
--fail-on |
error (default) | warn | info | never |
Exit 1 threshold. Budgets also fail unless never |
--config, -c |
path | Default: void.config.ts / .mts / .js / .mjs in cwd |
--quiet, -q |
boolean | No progress output |
--no-color |
boolean | Plain output |
--help, -h / --version, -v |
Lint uses --config, --fail-on, --format/--json, --out, --no-color.
Extract uses --out , --screenshots , --pages /,/pricing, --max-pages N, --format md|css|json, --quiet.
Environment: VOID_CHROMIUM / CHROME_PATH (browser binary), VOID_DEBUG=1 (stack traces), NO_COLOR, CI (plain progress lines).
Exit codes
| Code | Meaning | What to do |
|---|---|---|
| 0 | Pass: no findings at or above --fail-on, all budgets pass |
Done (check stopping criteria) |
| 1 | Findings ≥ --fail-on, or a budget exceeded |
Fix (see the audit SKILL loop) |
| 2 | Usage or config error (bad flag, invalid void.config.ts, unknown rule id) |
Read the message and hint |
| 3 | Target unreachable, or --start server exited / timed out |
Check the build, port and readyPath; the last 10 server log lines are printed |
| 4 | Browser launch failure | void doctor; install Chromium or set VOID_CHROMIUM |
void.config.ts schema (VoidConfig)
| Key | Type | Default | Meaning |
|---|---|---|---|
url |
string (absolute) | — | Base URL when none is passed |
routes |
string[] | URL path | Paths to audit |
start |
{ command, port, cwd?, readyPath?, timeoutMs? } |
— | Server to spawn (readyPath "/", timeout 120 s) |
src |
string | — | Source dir for lint (used by void lint and added to void audit) |
profiles |
("mobile" | "desktop")[] |
both | |
runs |
1–20 | 3 | Perf runs per profile |
kind |
"marketing" | "app" |
marketing | Default budget set |
budgets |
{ mobile?: Budgets, desktop?: Budgets } |
see below | Merged over the defaults per key |
rules |
Record |
— | Severity overrides |
failOn |
"error" | "warn" | "info" | "never" |
error | |
lighthouse |
boolean | false | |
screenshots |
string | — | Directory |
only |
Category[] | all page categories |
Budgets keys: lcp, inp, tbt, ttfb (ms) · cls · jsKb, cssKb, fontKb, imageKb, htmlKb (KB transferred at load) · requests · droppedFramePct (%).
Default budgets:
| lcp | cls | inp | tbt | ttfb | jsKb | cssKb | fontKb | imageKb | htmlKb | droppedFramePct | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| marketing mobile | 2500 | 0.1 | 200 | 200 | 800 | 170 | 25 | 120 | 1000 | 30 | 5 |
| marketing desktop | 1500 | 0.1 | 200 | 200 | 800 | 170 | 25 | 120 | 1500 | 30 | 5 |
| app mobile | 2500 | 0.1 | 200 | 300 | 800 | 300 | 40 | 120 | — | 60 | 5 |
| app desktop | 1500 | 0.1 | 200 | 200 | 800 | 300 | 40 | 120 | — | 60 | 5 |
The config file may export default a plain object (as the template does, with a JSDoc @type {import('@void/cli/config').VoidConfig}) or use defineConfig from @void/cli/config when the package is installed.
Report JSON (--json), abridged
{
schemaVersion: 1,
tool: { name: "void", version: "0.1.0" },
targets: [{ url, profile, status }],
scores: { perf, smooth, a11y, seo, geo, design, lint }, // 0–100; gates cap a category at 49
overall: number, // weighted: perf .25 a11y .2 smooth .15 seo .15 design .15 geo .1
metrics: [{ name: "lcp", value: 1830, unit: "ms", profile: "mobile", url, runs: [..], budget: 2500 }],
budgets: [{ name, limit, actual, pass, profile, url }],
findings: [{
id: "perf/lcp-image-lazy", category: "perf", severity: "error",
message: "LCP image /hero.avif has loading=lazy …",
source: "web-vitals" | "axe" | "lighthouse" | "loaf" | "trace" | "cdp" | "dom" | "ast" | "css" | "http" | "heuristic",
target: { url, profile, selector, html, file, line, column, snippet },
evidence: { metric: { name, value, unit, threshold }, data: {…} },
fix: { hint, example }, docs: "https://void-design.vercel.app/rules/perf/lcp-image-lazy", fingerprint
}],
errors: [{ category, message, url, profile }], // audits that could not complete
failOn: "error",
summary: { errors, warnings, infos, pass }
}Useful jq:
void audit --json --start "bun run start -p 3100" --port 3100 > report.json
jq -r '.findings | group_by(.id)[] | "\(.[0].severity)\t\(length)\t\(.[0].id)"' report.json | sort -r # counts per rule
jq '.budgets[] | select(.pass == false)' report.json
jq '.scores' report.jsonRule id prefixes
| Prefix | Examples |
|---|---|
perf/ |
lcp-slow, cls-high, tbt-high, ttfb-slow, js-budget, lcp-image-lazy, lcp-image-no-priority, lcp-background-image, images-missing-dimensions, third-party-heavy, bfcache-blocked, cache-headers, no-text-compression |
smooth/ |
scroll-jank, long-frames-during-scroll, inp-slow, layout-shift-after-input, animate-layout-property, reduced-motion-ignored, raf-loop-idle, multiple-webgl-contexts |
a11y/ |
axe ids (color-contrast, button-name, label, link-name, image-alt, heading-order, landmark-one-main, meta-viewport, bypass…), plus target-size, focus-not-visible, reflow-overflow, axe-other |
seo/ |
http-status, soft-404, metadata-in-body, canonical-missing, canonical-not-self, title-length, meta-description-missing, h1-count, og-image-invalid, favicon, robots-blocks-all, sitemap-lastmod, broken-links |
geo/ |
js-dependency, hidden-streamed-content, jsonld-parse-error, jsonld-required, jsonld-not-visible, faq-deprecated, search-bots-blocked, llms-txt-missing, markdown-mirror-indexable, answer-first, no-citations |
design/ |
too-many-font-families, no-type-scale, line-length-long, palette-sprawl, multiple-accents, gradient-text, purple-gradient, emoji-icons, spacing-off-grid, horizontal-overflow |
lint/ |
tw-unknown-class, tw-v3-arbitrary-var, tw-v3-renamed, page-level-use-client, image-priority-deprecated, heavy-import, browser-global-in-render, timer-in-render, outline-none-no-replacement, canonical-in-root-layout, og-merge-drops-parent, not-found-in-suspense, dynamic-metadata-streaming, sitemap-lastmod-now, route-js-budget |
void rules --json prints the full registry (the source of truth; the website renders /rules/ from it).