Commands & flags
Every invocation is real-a11y <command> [url...] [flags]. Fifteen commands ship — install, audit, inspect, tree, outline, tabs, list, interact, click, type, focus, session, snapshot, diff, and login. Run real-a11y <command> --help for a command's own flags.
Findings and reports go to stdout; progress, warnings, and errors go to stderr — so -o / a pipe never mixes the two.
Exit codes are frozen:
| Code | Meaning |
|---|---|
0 | Clean — no findings at or above --fail-on. |
1 | Findings at or above the threshold (the CI gate). |
2 | Usage or navigation error. |
View commands (tree, outline, tabs, list) aren't gates — they exit 0 unless something actually failed. Neither are the act commands (interact, click, type, focus): they exit 0 when every step lands, and 2 when one can't be reached.
Environment variables:
A11Y_PAGES— a JSON[{ "name": …, "url": … }]array, the page set forauditandsnapshotwhen no URL is passed. Each entry also takes an optional"id"— the page's identity, which defaults to the URL's path. Set it only to separate two routes that collide (two sites both rooted at/) or to join two the path keeps apart; seeurls.A11Y_SNAPSHOT_OUT— default output path forsnapshotwhen-ois omitted.
Human (pretty) output is English-only and may be reworded between releases. The machine formats — json, sarif, junit, jsonl, and the v1: fingerprints — are a frozen contract: within 0.x, changes are additive-only. Never rely on the wording of a pretty line; key on the JSON.
All commands at a glance
Every browser-driving command reads Chromium's own accessibility tree over CDP, except tabs — the one view that tree cannot produce (see --root). Click a command for its flags.
Setup
| Command | Purpose |
|---|---|
install | Download Chrome for Testing into Real A11y's own cache — run once. |
login <url> --save <file> | Log in by hand and save the session for --storage-state. |
Audit — the gates: these exit 1 on findings at or above --fail-on.
| Command | Purpose |
|---|---|
audit <url...> | Every violation, grouped by rule with locator + severity — the flagship. |
inspect <url> | Findings plus tree + outline from one read. |
Views — never gates; they exit 0 unless something actually failed.
| Command | Purpose |
|---|---|
tree <url> | The semantic tree — role + accessible name, as a screen reader traverses it. |
outline <url> | Heading outline (h1–h6) in document order. |
tabs <url> | Focusable elements in keyboard Tab order — the one in-page read. |
list <category> <url> | One category — heading, link, button, form, landmark, image. |
Act — Chromium only. Exit 0 when every step lands, 2 when one can't be reached.
| Command | Purpose |
|---|---|
interact <url> --step | Run ordered steps, then print the tree diff they produced. |
click <url> --role | Real click at the element matched by role + accessible name. |
type <url> --role --text | Replace a text field's value; the value is never echoed back. |
focus <url> --role | Move real keyboard focus; the [focused] marker moves in the diff. |
Session — manage the background browser daemons that --session reuses.
| Command | Purpose |
|---|---|
session <subcommand> | List, stop, or stop-all named daemon sessions. |
Artifacts
| Command | Purpose |
|---|---|
snapshot [url...] | Audit a page set → one diffable JSON artifact (or --md). |
diff <base.json> <pr.json> | Findings-aware diff of two artifacts — new / changed / fixed. Pure: no browser. |
Commands
Each command below lists the flag groups it accepts. Shared flags are documented once, in Flags — the per-command list only links to them, plus any command-specific flags.
install
Download Chrome for Testing — Google's versioned, non-auto-updating Chrome build — into Real A11y's own cache and use it for every launched session from then on. A setup command: it takes no URL and doesn't drive a browser itself. Idempotent — a bare re-run does zero network work and exits 0 instantly when the cached build is still present. Prints the resolved executable path on stdout; progress goes to stderr.
real-a11y install # latest Stable, first time only
real-a11y install --channel beta # track a channel
real-a11y install --version 131.0.6778.87 # exact build — works even if the
# Chrome for Testing version
# endpoint is unreachable
real-a11y install --force # reinstall even if already presentPlaywright (the npm package) is still the driver — this replaces only the npx playwright install chromium browser download. Every browser-driving command then picks it up automatically; see --chrome-path for the full resolution order. Sessions launched with --cdp attach to your own Chrome and never use this binary.
Flags: --channel · --version · --force · Config · -q, --quiet · --verbose · -h, --help.
audit <url...>
The flagship gate. Audits one or more pages against the semantic-tree rules and prints violations grouped by rule — each with a CSS locator and severity. Exits 1 on errors with no extra flags. A failed page becomes an error entry (exit 2) while the others still report. Under GitHub Actions it also emits grouped ::error annotations and a job summary.
With no URL, it audits every entry in A11Y_PAGES or the config's urls.
real-a11y audit http://localhost:3000
real-a11y audit https://stage.example.com --device "iPhone 13" --fail-on warning
real-a11y audit ./dist/index.html --format json -o report.jsonFlags: Browser & page · Output · Config · --rules · --fail-on (default error) · --no-annotate · no --root.
inspect <url>
Findings plus the semantic tree and heading outline — all from one read of Chromium's own accessibility tree, so the views can never disagree, and the findings always agree with audit. Views print first; the gate outcome is the last thing on screen. Single URL.
That tree carries no tab order, so this command prints none — and prints no empty section either, which would read as nothing here is focusable. Run tabs for the sequence.
real-a11y inspect http://localhost:3000Flags: Browser & page · Output · Config · --rules · --fail-on (default error) · --no-annotate · --include-generic.
tree <url>
Print the semantic tree — what a screen reader perceives, role by role. Single URL; always exits 0.
real-a11y tree https://example.com
real-a11y tree https://example.com/player # reaches user-agent-shadow media controlsFlags: Browser & page · Output (pretty | json) · Config · --include-generic · no --root.
outline <url>
Print the heading outline (h1–h6) in document order. Single URL; always exits 0.
real-a11y outline https://example.comFlags: Browser & page · Output (pretty | json) · Config · no --root.
tabs <url>
Print every focusable element in keyboard Tab order. Single URL; always exits 0.
The one command still built from the in-page DOM walk, and the only one that takes --root. Chromium's accessibility tree knows whether a node is focusable, but not the sequence — tabindex never reaches a native node — so tab order is DOM work by nature, not a fallback.
real-a11y tabs https://example.com
real-a11y tabs https://example.com --root "#app main"Flags: Browser & page · Output (pretty | json) · Config · --root.
list <category> <url>
List every element in one category — as role, accessible name, and locator. The category is the first positional; the URL follows. Single URL; always exits 0.
Categories: heading, link, button, form, landmark, image.
real-a11y list image https://example.comAn empty category reports how much it scanned and which roles it looked for, so "none" is never confusable with a page that failed to load:
(none — filter "image" matched 0 of 412 nodes; it looks for role img)Worth knowing when a category surprises you: image looks for exactly img, so a page whose graphics are figures lists none, and landmark includes the form role while form does not — that one lists the fields.
Flags: Browser & page · Output (pretty | json) · Config.
interact <url> --step '<step>'
Drive a page through one or more steps, then print the accessibility-tree diff those steps produced — the answer to "what did that actually change for a screen reader?". Steps run in order and stop at the first failure. Single URL; exits 0 when every step lands, 2 on a usage error or an unreachable target. Chromium only.
A step is written the way the tree prints a node:
<verb> <role> ["<name>"] [nth=<n>] [= <text>]- verbs —
click,type,focus. "<name>"— the accessible name. Omit it to match any name; pass""to target an unlabeled control (the one an audit just flagged).nth=<n>— 1-based, document order. It's the spelling the ambiguity error prints, so the fix is copy-paste.= <text>—typeonly. Everything after the first=is the value, so query strings and base64 need no escaping.
real-a11y interact http://localhost:3000 --step 'click button "Open menu"'
real-a11y interact http://localhost:3000 \
--step 'type textbox "Email" = someone@example.com' \
--step 'click button "Sign in"'
real-a11y interact https://example.com --step 'click button "Save" nth=2'Targets resolve against Chromium's own accessibility tree by role + accessible name — never a CSS selector. If a control can't be reached that way, assistive technology can't reach it either, and that is an accessibility finding rather than a targeting inconvenience. Ambiguous matches list their nth= candidates; a disabled target is refused, because the click would be swallowed and the resulting empty diff would read as "that button does nothing".
The actions are real
They submit forms, toggle state, and can navigate. A step that loads a new document leaves the tree captured before it describing a page that no longer exists, so no diff is possible — the run still succeeds and reports where it landed.
Targeting, acting, and the diff all read the same tree — Chromium's own, over CDP. A node you aim at by one name therefore can't come back in the report under another. That tree is whole-document, which is why these commands take no --root.
A typed value is never echoed — not in progress output, not in --format json, where the step renders as = ‹hidden›. Don't use type to log in: a password on the command line is visible to other processes and lands in your shell history. Use login.
Under --format json the page object carries steps (the steps that ran, rendered and redacted), diff, and navigated — the last so a script can tell a new document was loaded (so there is no diff) without matching prose. url is the address the page landed on, which differs from the one you passed when a step navigated.
Flags: --step '<step>' (repeatable, required) · --step-settle · Browser & page (no --root) · Output (pretty | json) · Config.
click <url> --role <role>
Dispatch a real click at the element matched by role + accessible name, then print the tree diff. Shorthand for a one-step interact, and bound by the same contract.
real-a11y click http://localhost:3000 --role button --name "Open menu"
real-a11y click http://localhost:3000 --role button --name "Save" --nth 2
real-a11y click http://localhost:3000 --role button --name "" # unlabeledFlags: --role (required) · --name · --nth · --step-settle · Browser & page (no --root) · Output (pretty | json) · Config.
type <url> --role <role> --text <value>
Replace a text field's value (role is usually textbox, searchbox, or combobox), then print the tree diff — a combobox popping its options, an inline validation error appearing. The value is written through the element's own prototype setter plus input/change events, so framework-controlled inputs (React et al.) register it.
real-a11y type http://localhost:3000 --role textbox --name "Email" --text you@example.comThe value is never echoed back, in any format. Don't use it to log in — see login.
Flags: --role (required) · --text (required) · --name · --nth · --step-settle · Browser & page (no --root) · Output (pretty | json) · Config.
focus <url> --role <role>
Move real keyboard focus to the matched element, then print the tree diff — the focus move shows as the [focused] marker relocating. Pairs with tabs for focus-order work.
real-a11y focus http://localhost:3000 --role textbox --name "Email"Flags: --role (required) · --name · --nth · --step-settle · Browser & page (no --root) · Output (pretty | json) · Config.
snapshot [url...]
Audit a page set and write one JSON artifact — fingerprinted findings plus the tree and outline views per page, with meta.views recording which views the run measured. That artifact is the input to diff.
There is no tabs view: this reads Chromium's whole-document accessibility tree, which carries no tab order. The artifact omits the view rather than storing an empty one, so a diff reads it as not measured instead of every tab stop was removed. real-a11y tabs is the keyboard sequence.
Pages, in precedence order: positional URLs, else A11Y_PAGES, else the config's urls (--config or auto-discovered). Output goes to -o, else A11Y_SNAPSHOT_OUT, else stdout. Unlike the other gates, --fail-on defaults to never here — snapshot just writes the artifact unless you ask it to gate.
real-a11y snapshot https://example.com -o base.json
real-a11y snapshot --config a11y.config.json --md -o report.md
real-a11y snapshot https://example.com --md --only views -o views.md
real-a11y snapshot --config a11y.config.json --update-baseline
real-a11y snapshot --config a11y.config.json --baseline .a11y-baseline.json --fail-on errorFlags: Browser & page · Config · --rules · --fail-on (default never) · --include-generic · -f, --format (json | md | sarif | junit | jsonl) · --md · --only (findings | views, md-report-only) · --baseline · --update-baseline · -o, --output · -q, --quiet · --verbose · -h, --help.
diff <base.json> <pr.json>
Classify the findings in two snapshot artifacts as new / changed / fixed — robust to DOM churn (re-indentation, renumbered locators) that defeats a line diff. Pure: no browser. Takes exactly two positional files. Exits 1 only on NEW findings at or above --fail-on; fixes and drift never fail the build.
Default output is neutral — findings plus a real unified diff of the structure. Add --explain for a plain-language summary, or report a single axis with --only findings | views (an output filter — the exit gate is unchanged).
Pages are matched by their identity — the URL's path, not the display label and not the whole URL. Base and PR legitimately run on different hosts and ports, so the origin is ignored: localhost:3000/pricing and example.com/pricing are one page and compare normally, whatever either side calls them. Renaming a page therefore changes nothing about what it is.
A page whose identity is on only one side is reported as added or removed and is never compared, which is the right answer for genuinely different routes (/pricing vs /careers). If nothing matches at all, diff warns on stderr that it compared nothing — the report and exit code are unaffected. That now means the two runs really did capture different routes, so check each entry's url; it is no longer something a rename can cause.
To override the derived identity — to separate two sites that share a route, or join two the path keeps apart — set an explicit id on the entry (config urls, or A11Y_PAGES as [{ id, name, url }]).
real-a11y diff base.json pr.json
real-a11y diff base.json pr.json --explain
real-a11y diff base.json pr.json --only findings
real-a11y diff base.json pr.json --format md --explain --max-pages 5 --max-lines 20 -o comment.mdFlags: Config · --fail-on (default error) · --explain · --only (findings | views) · --max-lines · --max-pages · --baseline · --ignore-view-line · -f, --format (pretty | json | md) · -o, --output · -q, --quiet · --verbose · -h, --help.
login <url> --save <file>
Open a visible browser, log in by hand — MFA, SSO, and passkeys all work, because a human is driving — then press Enter to save the session to a file you can later pass to --storage-state. Interactive by construction: it needs a TTY and fails fast in CI. Forces headful; no device/viewport emulation, format, or output flags. The saved file holds live session tokens — keep it out of version control (the command warns if it isn't).
real-a11y login https://app.example.com --save auth.json
real-a11y audit https://app.example.com/dashboard --storage-state auth.jsonFlags: --save (required) · --wait-until · --settle · --timeout · Config · --verbose · -h, --help.
session <subcommand>
Manage the long-lived browser daemons that invocations with --session reuse. This command never launches a browser itself; it only talks to already-running daemons.
list— print every session, its pid, status, current URL, and whether it is busy. Add--format jsonfor machine-readable output.stop <name>— stop one session by name and remove its session directory.stop-all— stop every session in~/.real-a11y/sessions/. By default a session that is locked by a concurrent invocation only prints a warning and exits0; pass--strictto exit2so CI cleanup can retry.
Session names are sanitized for filesystem use: non-alphanumeric characters become underscores and the name is truncated to 64 characters. On Windows the daemon listens on a named pipe with a random per-session name (\\.\pipe\real-a11y-<id>; the id is stored in the session directory and is independent of the auth token); everywhere else it uses a Unix domain socket under ~/.real-a11y/sessions/<name>/.
Stale pidfiles/sockets are reported as stale instead of running, and stop/stop-all remove them.
real-a11y session list
real-a11y session list --format json
real-a11y session stop checkout
real-a11y session stop-all
real-a11y session stop-all --strictFlags: Config · -f, --format (list only) · --output (list only) · --strict (stop-all only) · -q, --quiet · --verbose · -h, --help.
A flag passed to a subcommand it doesn't apply to is rejected with an error naming the right one (e.g. session stop x --format json fails with "--format applies to session list") rather than being silently ignored.
Flags
Grouped and documented once. The Commands line above each entry names which commands accept it. An explicit flag always wins over an a11y.config.json default; see Configure once.
All flags at a glance
Throughout this table, browser commands is the eleven that drive a page: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot.
| Flag | Type / values | Default | Commands |
|---|---|---|---|
--root | CSS selector | body | tabs only † |
--device | Playwright device name | none | browser commands |
--viewport | WIDTHxHEIGHT | none | browser commands |
--wait-until | load | domcontentloaded | networkidle | commit | load | browser commands · login |
--settle | ms | 0 | browser commands · login |
--step-settle | ms | 200 | the act commands |
--timeout | ms | 30000 | browser commands · login |
--headful | boolean | false | browser commands |
--cdp | CDP endpoint URL | none | browser commands |
--chrome-path | path to a browser binary | none | browser commands |
--allow-file | boolean | false | browser commands |
--storage-state | path to a saved session | none | browser commands |
--audit-origin | origin (repeatable) | the target's own | browser commands |
--session | string | cwd hash | browser commands |
--session-idle-timeout | ms | 900000 | browser commands |
--include-generic | boolean | false | inspect, tree, outline, tabs, list, snapshot |
† Every other browser command reads Chromium's whole-document accessibility tree, so there is nothing for a selector to scope — they reject --root with that explanation rather than accepting and ignoring it. There is no producer flag either: each command has exactly one correct producer, so there is nothing to choose.
| Flag | Type / values | Default | Commands |
|---|---|---|---|
-f, --format | enum, per command | first value | browser commands · diff |
-o, --output | path | stdout | browser commands · diff |
-q, --quiet | boolean | false | browser commands · diff · install |
--verbose | boolean | false | all |
-h, --help | boolean | — | all |
| Flag | Type / values | Default | Commands |
|---|---|---|---|
--config | path to a11y.config.json | auto-discovered | all |
--no-config | boolean | false | all |
| Flag | Type / values | Default | Commands |
|---|---|---|---|
--rules | comma-separated rule ids | all rules | audit, inspect, snapshot |
--fail-on | error | warning | never | error (never for snapshot) | audit, inspect, snapshot, diff |
--no-annotate | boolean | false | audit, inspect |
Command-specific
| Flag | Type / values | Default | Commands |
|---|---|---|---|
--step | step string (repeatable, ordered) | — | interact (required) |
--role | ARIA role | — | click, type, focus (required) |
--name | accessible name; "" = unlabeled | any name | click, type, focus |
--nth | positive integer, 1-based | — | click, type, focus |
--text | the value to enter | — | type (required; click / focus reject it) |
--channel | stable | beta | dev | canary | stable | install |
--version | exact Chrome build id | — | install |
--force | boolean | false | install |
--save | path | — | login (required) |
--md | boolean | false | snapshot |
--only | findings | views | both | snapshot, diff |
--baseline | path | none | snapshot, diff |
--update-baseline | boolean | false | snapshot |
--explain | boolean | false | diff |
--ignore-view-line | regex (repeatable) | none | diff |
--max-lines | integer | full | diff |
--max-pages | integer | all | diff |
Browser & page
Control the browser that renders the page before extraction. Accepted by every browser-driving command — audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot. login takes only the settling flags (--wait-until, --settle, --timeout) and forces headful.
--root <selector>
- Type: CSS selector · Default:
body· Commands:tabsonly
Scope the tab-order walk to a region or component instead of the whole page.
tabs is the last command that runs in the page, and a selector only means something to an in-page walk. Every other browser command reads Chromium's own accessibility tree, which is whole-document: there is no subtree to narrow to, so they reject --root with that explanation rather than accepting it and silently doing nothing.
real-a11y tabs http://localhost:3000 --root "#app main"A project-wide defaults.root now reaches only tabs. On any other command it is warned about on stderr, not an error — the config loader is otherwise strict and fail-closed, and hard-erroring would red every CI that set the key, over config that was correct when it was written.
A route's urls[].rootSelector is likewise no longer a scope for audit or snapshot. Those warn once, naming the routes, and keep running: the entry is still how a route is identified, and findings from outside the old subtree are now included.
--device <name>
- Type: Playwright device name · Default: none · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Emulate a device — viewport, user agent, touch — to audit the mobile layout. Can't be combined with --cdp.
real-a11y audit http://localhost:3000 --device "iPhone 13"--viewport <WxH>
- Type:
WIDTHxHEIGHT· Default: none · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Explicit viewport size. Must match ^\d+x\d+$, e.g. 1280x800. Can't be combined with --cdp.
--wait-until <state>
- Type:
load | domcontentloaded | networkidle | commit· Default:load· Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot, login
The navigation lifecycle event to wait for before extracting. Use networkidle for JS-heavy pages that keep fetching after load.
--settle <ms>
- Type: integer ms · Default:
0· Max:30000(clamped) · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot, login
Extra wait after the --wait-until state, for animations or late hydration. Values above the max are clamped; a non-integer is an error.
real-a11y audit http://localhost:3000 --wait-until networkidle --settle 500--step-settle <ms>
- Type: integer ms · Default:
200· Max:30000(clamped) · Commands: interact, click, type, focus
How long to wait after each step before looking at the page again. A dispatch returning is not the same as its effect having landed: a React state update flushes on a later tick, a dialog mounts on the next frame. It gates the next step's targeting as much as the final diff — a step that opens a menu has to have opened it before the step that clicks an item can find that item.
real-a11y interact http://localhost:3000 --step-settle 600 \
--step 'click button "Open menu"' \
--step 'click menuitem "Settings"'0 opts out and reads immediately. Distinct from --settle, which waits once after the initial page load; this one applies per step.
A wait, not a guarantee
Nothing can tell you a page is about to navigate. A reaction that lands later than this still won't appear in the diff — raise it for a slow app rather than reading "no changes" as proof that nothing happened.
--timeout <ms>
- Type: integer ms · Default:
30000· Min:1· Max:120000(clamped) · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot, login
Navigation timeout. Unlike Playwright, 0 is not accepted (no "wait forever"); values above the max are clamped.
--headful
- Type: boolean · Default:
false(headless) · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Show the browser window. Can't be combined with --cdp. login is always headful.
--cdp <endpoint>
- Type: CDP endpoint URL · Default: none · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Attach to a running Chrome instead of launching one — the interactive way to audit a login. No emulation over CDP: can't be combined with --headful, --device, --viewport, --storage-state, or --chrome-path.
--chrome-path <file>
- Type: path to a browser executable · Default: none · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Launch this specific browser binary instead of Playwright's bundled Chromium. Can't be combined with --cdp — an already-running browser is the browser. Resolution order (shared with the MCP server's REAL_A11Y_CHROME_PATH): --chrome-path (error if missing) > REAL_A11Y_CHROME_PATH env (error if missing) > the install cache (silently skipped if absent) > Playwright's own bundled Chromium.
real-a11y audit http://localhost:3000 --chrome-path /usr/bin/google-chrome--allow-file
- Type: boolean · Default:
false· Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Unlock file: targets that come from a config file, A11Y_PAGES, or the snapshot command. A path or file: URL you type directly on the command line is already allowed (you are the authority); --allow-file is the gate for indirect sources so a PR or environment variable cannot silently point CI at a local file.
--storage-state <file>
- Type: path to a saved session · Default: none · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Audit as a logged-in user, using a session file written by login. Can't be combined with --cdp. See Authenticated pages.
real-a11y login https://app.example.com --save auth.json
real-a11y audit https://app.example.com/dashboard --storage-state auth.json--audit-origin <origin>
- Type: origin (repeatable) · Default: the target's own origin · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
An extra origin allowed under --storage-state. Origin pinning stops a redirect from routing extraction to an unintended, cookie-matching origin. Repeat for each additional origin; must parse as a URL.
real-a11y audit https://app.example.com --storage-state auth.json \
--audit-origin https://accounts.example.com--session <name>
- Type: string · Default: stable hash of the current working directory · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
Reuse a long-lived browser session across invocations. The first --session run spawns a daemon process that keeps the page open; later runs with the same name connect to it and act on the same live page. Omit --session to keep the one-shot default. See a11y.config.json session for a project-wide name.
The browser identity and working directory are frozen for the life of the session: --headful, --cdp, --chrome-path, --storage-state, the resolved proxy environment, and the directory the first run was started from must stay the same across runs. If you change any of them, session stop <name> first; otherwise the run is rejected with a "different browser flags" or "different working directory" error. --allow-file is a per-run gate for config-sourced file:// targets; direct paths typed on the command line are always allowed, so it does not need to stay the same across runs.
real-a11y tree https://app.example.com --session checkout
real-a11y click https://app.example.com --session checkout --role button --name "Apply"Commands that read the page (audit, inspect, tree, outline, snapshot, tabs, list) observe the live DOM at the moment they run. If you click or type first, a later snapshot/audit in the same session captures the post-interaction state. Capture baselines in a fresh session or before any act commands to avoid session-contaminated diffs.
--session-idle-timeout <ms>
- Type: ms · Default:
900000(15 minutes) · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot
How long a daemon for this session stays alive with no commands. The timer resets after every completed run; when it expires the daemon exits and its socket/pidfile are removed. The value must be a positive number of milliseconds, is clamped to a minimum of 1 s, and is capped at one hour to prevent a typo from leaving a daemon alive indefinitely. 0 is not accepted.
real-a11y tree https://app.example.com --session checkout --session-idle-timeout 600000--strict
- Type: boolean · Default:
false· Commands:session stop-all
When session stop-all cannot stop a session because another invocation is holding its lock, --strict makes the command exit 2 instead of 0. The default is forgiving so concurrent races do not fail an ad-hoc cleanup, but CI pipelines that must guarantee no background browsers remain can use --strict to detect the condition and retry. session list and session stop reject it.
real-a11y session stop-all --strictOutput
Format and destination. -o / a pipe never suppresses progress — that always stays on stderr.
-f, --format <fmt>
- Type: enum (per command) · Default: first value below · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot, diff
Never auto-switched — piping only drops color. Allowed values depend on the command:
| Command | Values | Default |
|---|---|---|
| audit, inspect, tree, outline, tabs, list | pretty | json | pretty |
| diff | pretty | json | md | pretty |
| snapshot | json | md | sarif | junit | jsonl | json |
sarif requires --config (results anchor to repo file paths).
-o, --output <file>
- Type: path · Default: stdout · Commands: audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot, diff
Write the report to a file (progress stays on stderr). A typo'd path fails before the browser launches, not after. For snapshot, A11Y_SNAPSHOT_OUT is the fallback when this is omitted.
-q, --quiet
- Type: boolean · Default:
false· Commands: install, audit, inspect, tree, outline, tabs, list, interact, click, type, focus, snapshot, diff
Suppress progress lines on stderr — auditing <url> …, per-page timings. install still prints the resolved executable path on stdout.
It does not silence --verbose diagnostics, which answer "what is this run using?" rather than "how far along is it". -q --verbose is a useful pair, not a contradiction: it drops the per-page noise and keeps the facts.
--verbose
- Type: boolean · Default:
false· Commands: all
Extra diagnostics on stderr: which config was found (below), which Chrome binary was resolved and where it came from, the browser cache directory, and per-page timings.
Only the timings are progress, so only they are suppressed by -q — the rest describe the run's inputs and survive it. That is the point: -q is for CI logs you don't want narrated, and a diagnostic you asked for by name shouldn't vanish because the same job also asked for quiet.
One line of it is worth knowing about before you need it — where the config came from, printed before anything depends on it:
config: /work/app/a11y.config.json (auto-discovered)
config: skipped (--no-config); built-in defaults only
config: none found — looked for /work/app/nested/a11y.config.json
auto-discovery checks the directory you run from and does not walk upward, so a
config in a parent directory is not picked up. Pass --config <file> to name one.The last one is the case that costs people time: the config is real, just not in the directory the command ran from, and every default silently reverts to its built-in. The path is absolute on purpose — a relative one tells you nothing you didn't already assume.
-h, --help
- Type: boolean · Commands: all
Print that command's usage and flags, then exit 0. real-a11y --help with no command prints the command list.
Config
An a11y.config.json seeds any flag you don't pass — the Jest/ESLint model. Accepted by every command.
--config <file>
- Type: path · Default: auto-discovered
a11y.config.jsonin cwd · Commands: all
Point at a config elsewhere. Its defaults block seeds unset flags; its urls list is the page set for audit and snapshot (diff reads two snapshot files, so it only takes defaults from the config). The config is strict and fail-closed — a typo'd key is a hard error, so a mistake can't silently un-gate CI. See Configure once.
--no-config
- Type: boolean · Default:
false· Commands: all
Ignore an auto-discovered config for this run.
Gate
Which rules run, and what fails the build.
--rules <ids>
- Type: comma-separated ids · Default: all five · Commands: audit, inspect, snapshot
Run only a subset of the rules. Valid ids: no-unlabeled-interactive, image-alt, heading-order, dialog-labeled, landmark-structure. An unknown id is a hard error.
real-a11y audit http://localhost:3000 --rules no-unlabeled-interactive,image-alt--fail-on <level>
- Type:
error | warning | never· Default:error(audit, inspect, diff) ·never(snapshot) · Commands: audit, inspect, snapshot, diff
The gate threshold. error exits 1 on any error; warning also fails on warnings; never never fails the build (it still reports). For diff, only NEW findings at or above the threshold gate — drift and fixes never fail.
--no-annotate
- Type: boolean · Default: annotate on · Commands: audit, inspect
Skip the GitHub Actions ::error annotations and job-summary report that audit and inspect emit when running under Actions.
Command-specific
Flags that belong to a single command (or a small set).
--channel <name>
- Type:
stable | beta | dev | canary· Default:stable· Commands: install
Which Chrome for Testing release channel to track. Re-resolves the channel's current build over the network; skips the download if it's unchanged from what's already installed. Mutually exclusive with --version.
--version <buildId>
- Type: an exact Chrome build, e.g.
131.0.6778.87· Default: none · Commands: install
Pin an exact Chrome for Testing build instead of tracking a channel. Skips the version-resolution network call entirely — the download URL is deterministic — so this is the escape hatch when the Chrome for Testing version endpoint is unreachable. Mutually exclusive with --channel.
--force
- Type: boolean · Default:
false· Commands: install
Reinstall even if the target build is already cached.
--include-generic
Include generic container nodes (untyped div/span wrappers) in the semantic tree, which are collapsed away by default. Only the commands that print a full tree honor it. snapshot, outline, tabs, and list accept the flag but ignore it — snapshot's artifact tree always collapses generics.
--md
- Type: boolean · Default:
false· Commands: snapshot
Shorthand for --format md — a human-readable report instead of the JSON artifact. Conflicts with an explicit non-md --format.
--baseline <file>
- Type: path · Default: none · Commands: snapshot, diff
Suppress findings this baseline accepts: they stay in the report (marked "suppressed": true) but drop out of the --fail-on count (and SARIF). Identity-matched, not string-matched. See Adopt the gate on existing debt.
--update-baseline
- Type: boolean · Default:
false· Commands: snapshot
Rewrite the baseline from the current findings, then stop — it writes the baseline file (.a11y-baseline.json, or --baseline's path) and never gates. Prints +N new, -N stale on stderr and carries forward note fields on entries that still match.
real-a11y snapshot --config a11y.config.json --update-baseline--explain
- Type: boolean · Default:
false· Commands: diff
Add a plain-language summary of structural changes ("Heading level changed: h2 → h3", "Keyboard tab stop removed …") to the neutral diff. Opt-in because the statements are an interpretive layer; the default never makes a claim the diff can't back up. Advisory only — it never affects the exit code.
This includes a Focused element changed: … → … statement when the element focused at capture time differs between the two snapshots (a moved autofocus target, or focus that appeared or vanished). Because focus isn't structure, it's excluded from the structural diff — a page where only focus moved shows no add/remove churn, just this one statement.
Under --only findings the statements are inert — the filter removes the structure axis they summarize (same as --max-lines), so a config default of explain: true never conflicts with an explicit filter.
--only <axis>
- Type:
findings | views· Default: the full two-axis report · Commands: diff, snapshot
Report just one axis: --only findings (the accessibility problems) or --only views (the tree/outline/tab-order structure). An output filter — the exit gate is computed from the full findings either way, so a filtered CI run can exit non-zero while showing only structure. What explains a gating exit: on diff, the always-present one-line findings summary; on snapshot, a stderr note (real-a11y: gate: …) — the views-only report itself stays a pure structure export with no findings content at all.
An enum on purpose: contradictory states are unrepresentable, and a config default ("defaults": { "only": "findings" }) is overridable from the command line by passing the other value.
- diff —
--only findingshides the view hunks; view-axis modifiers (--explain,--max-lines,--ignore-view-line) become inert.--only viewshides per-finding entries; composes with--explain. In--format jsonthe filtered axis's arrays are omitted (views/structuralvsnew/changed/removed); the summary and per-pagestructuralDiffboolean always ship. - snapshot — shapes the
--format mdreport, or writes a partial--format jsonartifact: the filtered axis is stripped andmeta.onlyrecords the capture mode. A partial artifact is a machine export (smaller payload, custom tooling), not a diffable snapshot —diffrejects it outright, because an empty-because-filtered axis is indistinguishable from empty-because-clean and would read as everything-new or all-removed.sarif/junit/jsonlare findings-shaped by construction and reject the flag.
real-a11y diff base.json pr.json --only findings
real-a11y diff base.json pr.json --only views --explain
real-a11y snapshot https://example.com --md --only views -o views.md
real-a11y snapshot https://example.com --only views -o views.json # partial artifact (meta.only: "views")--ignore-view-line <regex>
- Type: JS regex (repeatable) · Default: none · Commands: diff
Drop view lines matching the pattern before diffing — for generated content that differs on every build (a "last updated" timestamp, a build hash) and would otherwise read as drift on every page. Each value must be a valid RegExp.
real-a11y diff base.json pr.json --ignore-view-line '^time "'--max-lines <n>
- Type: positive integer · Default: full · Commands: diff
Cap the structural diff to n lines per page, then … N more. For CI comments — run once uncapped to a log and once capped to the comment.
--max-pages <n>
- Type: positive integer · Default: all · Commands: diff
Detail at most n changed pages, then list the rest by name. For CI comments.
real-a11y diff base.json pr.json --explain --max-pages 5 --max-lines 20 -o comment.md--save <file>
- Type: path · Default: none (required) · Commands: login
Where login writes the captured session. Written 0o600 (POSIX); the command warns if the path sits un-gitignored inside a repo. Session storage isn't captured — apps that keep auth there need --cdp instead.
real-a11y login https://app.example.com --save auth.jsonSecurity posture
real-a11y is designed to be run in CI and on shared workstations. The daemon feature makes the trust boundary explicit:
Socket and process isolation
- Unix domain sockets live under
~/.real-a11y/sessions/<name>/daemon.sock, in a per-session directory created0o700. The socket file is created with0o600permissions, so only the owning user can connect — the kernel refuses other users atconnect(), the same property a peer-credential (SO_PEERCRED) check would assert. - Windows uses a named pipe at
\\.\pipe\real-a11y-<id>, where the id is random, stored in the per-user session directory, and independent of the per-session 256-bit auth token — pipe names are enumerable by every local process, so a name derived from the token would publish a hash of the secret. The real protection is the token, which lives inside the per-user session directory and is required for every RPCauthmessage. Node does not expose a way to set a restrictive pipe DACL, andchmodhas no effect on Windows, so the CLI sets an explicit token-file ACL (viaicacls) granting only the current user full control. This is fail-closed: ificaclsis unavailable or the ACL cannot be applied, the session refuses to start — on the CLI side before spawning and again in the daemon before it reads the token — rather than leave the token relying on the profile directory's inherited ACL. Do not use--sessionwith--storage-stateon shared Windows machines, and keep~/.real-a11y/sessionsinside a profile directory that is not readable by other users. - A pidfile (
daemon.pid) records the daemon PID and version. A stale pidfile (daemon gone) or an incompatible daemon version is replaced on the next--sessionrun, so a CLI upgrade does not silently talk to an old protocol. - Daemons exit automatically after the
--session-idle-timeoutelapses (default 15 minutes).real-a11y session stopandreal-a11y session stop-allterminate them immediately and remove the session directory. - The RPC trust boundary is the token, not the request contents. Any process that can read the per-session token file is the same OS user, and a request is treated exactly like that user running the CLI one-shot: it carries the caller's working directory, from which
a11y.config.json, page lists, and relative--outputpaths resolve. The daemon grants nothing the caller could not already do directly; the working directory is pinned to the session's first run, so an existing session cannot be pointed at a different project directory.
Authenticated state
- The daemon keeps the same browser context for the life of the session. If you started it with
--storage-state, that context carries the saved cookies and local storage for every subsequent command with the same--sessionname. Stop the session when you're done. --storage-stateis not allowed together with--cdp— CDP attaches to a browser you already own, whose session lifecycle is outsidereal-a11y's control.
Origin pinning and the file:// gate
- By default a target is only allowed to redirect within its own origin.
--audit-originadds extra allowed origins; redirects outside the allowed set are refused. file://targets you type directly on the command line are always allowed (the shell user is the authority).--allow-filegatesfile://targets from indirect sources —a11y.config.json,A11Y_PAGES, andsnapshotpositionals — so a committed file or environment variable cannot silently point CI at a local file. It is a per-run flag: the gate does not persist across--sessionruns.- With
--session, the browser context is reused across runs. The allowed origins are pinned to the first run in that session; later runs can request a subset of those origins. To target a new origin, stop the session and start a new one. - The per-run origin pin is re-applied on every command, including the reused-page fast path, so a command cannot silently read from a page the previous run redirected to outside the allowed set.
Redaction
a11y.config.jsonredactpatterns are applied to URLs and text before anything is written or printed — snapshots, diffs, reports, and stdout.- This is especially important with
--session, because a long-lived page may contain tokens, PII, or signed URLs in query parameters. Redact them at the source so they never reach the daemon log or a committed artifact.
Daemon log
- Each session writes diagnostics to
~/.real-a11y/sessions/<name>/daemon.log. It may contain URLs and command names; it is owned by the same user and is removed when the session is stopped.