Skip to content

Scanning with cora scan

cora scan opens each target in Google Chrome and waits for the page to settle. Then it checks the page with axe-core, including its iframes and open shadow roots. Each run writes one results file. The CLI reference lists its arguments and options.

A target is a page to scan, or a crawl that finds pages.

  • On the command line, a target is an absolute http or https URL, or a path such as /pricing. A path resolves against baseUrl, which --base-url sets for one run. --crawl adds a crawl from a start page, and the other crawl options set its limits and patterns. Targets and crawls on the command line replace the config’s targets.
  • In the config, targets lists URLs, paths and crawls. See Targets.
  • A crawl starts at one page and follows the links in each page’s main document that stay on its host. It stops at maxPages pages, and at maxDepth links from the start, each with a default. It skips links that match an exclude pattern, and links a site’s robots.txt disallows: see Crawls and robots.txt.

A page that two targets name, such as a configured page that a crawl also finds, is scanned once.

Terminal window
npx cora scan /pricing https://example.com/docs/

To crawl a site with no config, give its start page to --crawl. Each --crawl adds one crawl, and every crawl on the command line takes the same --crawl-max-pages, --crawl-max-depth, --crawl-exclude and --crawl-ignore-robots-txt. Give crawls different limits in the config instead. Outside a project, run npx @vispero/cora, or pnpm dlx @vispero/cora, as Scanning without a project shows:

Terminal window
npx @vispero/cora scan --crawl https://example.com/ --crawl-max-pages 50 --crawl-exclude /logout

A crawl that stops at one of its limits with links to more pages left to follow is listed under Crawls stopped at a limit in the scan’s summary, by the setting to raise.

A crawl reads a site’s robots.txt, as the crawlers of other people’s sites do, and follows no link it disallows for CORA, by the rules of RFC 9309:

  • The base URL’s origin is exempt. A crawl reads no robots.txt on the scheme, host and port of baseUrl, so a crawl of your own app scans it as a person’s browser sees it. It reads the file of each other origin its links lead to, once a run.
  • CORA’s product token is VisperoCORA. A group whose user-agent line names it, in any case, sets CORA’s rules on that site in place of the group for every crawler, *, so a site owner can give CORA rules of its own.
  • Only links are held back. A crawl’s start page, a page a target names and a page a redirect lands on are scanned whatever robots.txt says. In a sharded run, a landing page robots.txt disallows that another shard owns is scanned by neither shard, and the run is incomplete.
  • An unreachable file holds back every link on its origin. A robots.txt that answers with a server error or HTTP 429, or cannot be fetched within the file’s own page.timeout and page.retries, disallows every page there. One that answers with another 4xx, such as a 404, allows every page.
  • A page held back is not scanned. The results list it under disallowed-by-robots, with the rule that disallows it, and it counts toward maxPages. Like any target that was not scanned, it leaves the gate without a verdict unless new violations fail it: see Targets that were not scanned.

To crawl a site you own outside baseUrl, such as staging on another origin, set the crawl’s ignoreRobotsTxt, or pass --crawl-ignore-robots-txt.

A crawl fetches robots.txt in the same browser as the pages it governs, through the same proxy, certificates and sign-in. It does not read Crawl-delay, which RFC 9309 does not define: a crawl visits one page at a time and waits for each to settle. A robots.txt served as a download reads as one that cannot be fetched.

cora waits for each page by the config’s page options, and those of each routes entry that matches it:

  1. It loads the page and waits for its load event.
  2. With page.waitFor, it waits until an element matches the selector, or the function resolves.
  3. It waits for the page to settle: the page and its same-origin iframes have loaded, its documents stop changing for a quiet window, its fonts are ready, and its animations that end have ended. Its requests for documents, styles, scripts, fonts, images and data must also go quiet. Each request counts for at most page.settle.requestTimeout, so a long poll does not hold the page.

When the page’s own script sends the browser to another page meanwhile, as a redirect in the browser does, cora follows it and waits for that page to load and settle the same way, so it is scanned with its styles.

When page.settle.timeout passes first, cora scans the page as it is. The results record that the page did not settle, and the log’s warning names what it still waited on. See Findings that differ between runs.

A visit, from opening the page to closing it, is bounded by page.timeout. A visit that fails to load the page or runs out of time is made again in a fresh tab, as often as page.retries allows. The progress line shows Retrying meanwhile.

The config’s page.exclude leaves elements out of a scan, each with everything inside it. Use it for a chat widget or an embedded player the project does not own. No rule evaluates them, and the rest of the page is scanned as usual.

On a page where a selector matches, the target’s coverage is partial, with the reason excluded-by-config. The reason names the selectors that matched there. So a page left partly unscanned by design reads apart from a scan that fell short.

A target can end without a scan of its own:

  • It redirects to another page, such as a sign-in page after a session expired. cora records the target as redirected, not scanned. When the landing page is on the target’s host, cora scans it as a target of its own, unless the crawl’s exclude matches it, and the gate counts the target as scanned there. So a missing sign-in shows in the results, but can pass the gate: see Targets that were not scanned.
  • It fails, such as a page that does not load, answers with an HTTP error, never gets ready or runs out of time. cora records it as not scanned, with a reason code and its detail. The coverage reasons reference describes each reason. A page that fails to load or times out is recorded only once its last retry fails.

Either way, the run completes and writes its results file, which gives each target’s coverage and reason. The report directory’s summary lists them too.

A dev server whose HTTPS certificate your machine does not trust, such as a self-signed one, fails every page’s load with navigation-failed. See Self-signed certificates.

A target that was not scanned, or that redirected to a page the run did not scan, leaves the gate without a verdict, unless new violations already fail it: see Targets that were not scanned.

cora scan runs Google Chrome without a window. To watch it scan, as when a page does not settle or a sign-in fails, show the browser with --headed:

Terminal window
npx cora scan /pricing --headed
  • browser.headed in your user config, or CORA_BROWSER_HEADED=1, shows it in every scan on your machine. --headed wins over both.
  • Where no one may watch, in CI or under a coding agent, and where no display can show a window, the scan runs without one. It warns setting ignored on stderr, naming browser.headed and why: ci, coding-agent or no-display.
  • A display is an X11 or Wayland one on Linux, which WSL has only with WSLg. On macOS and Windows, a scan in an SSH session shows no window.
  • It is your own setting, so it never changes the config’s hash. cora-mcp’s browser follows mcp.headless instead.

Each run writes its results to .cora/runs/<run-id>.json in the project, or outside a project in the folder you run cora in (Scanning without a project). .cora/ ignores itself, so it never shows in git status, and your stateDirectory setting moves it.

  • --out <path> writes the results file to that path instead: the stable name to use in CI.
  • --out - writes it to stdout.

The file holds:

  • the browser the run launched, with its version;
  • what the run covered: each target with its browser and viewport, the rules that ran on it, or why it was not scanned;
  • whether each target’s issues keep its host in their identity, which one issue on two sites explains;
  • every finding: its rule, its element’s selector and markup, its WCAG success criteria, its severity, and a fingerprint that identifies the issue across runs;
  • each finding’s place in the source, when the page was built with source stamps.

The cora-results reference lists each of its fields.

While it runs, cora scan shows the target it is on. Then it prints a summary on stderr:

Not scanned:
http://localhost:4173/reports: the page did not load (HTTP 500)
Gate failed: 2 new issues at or above minor, and 1 target was not scanned
Targets: 11 (1 not scanned, 1 scanned before it settled)
Issues: 11 (2 new, 1 needs review, 8 accepted)
Duration: 14.2 s
Results: .cora/runs/20261006T170501123Z-k3j9x2ab.json
To accept them, run:
npx cora baseline update .cora/runs/20261006T170501123Z-k3j9x2ab.json
  • Not scanned: lists each target the run did not scan, with why in words and, in parentheses, its detail, such as the HTTP status or the browser’s error. The coverage reasons reference gives the code the results file records for each.
  • scanned before it settled counts the targets scanned before their page settled, whose findings may differ between runs. Like not scanned, it shows only above zero.
  • Issues: counts issues: the findings that share a fingerprint count once. One element’s violation at two viewports is one issue. With source stamps, so is one component’s violation on every page it renders on. One element’s violation on one path of two sites is two issues (One issue on two sites).
  • In parentheses, Issues: counts each kind of issue above zero: new, needs review, accepted by the baseline, report-only for a rule set to report, and found by an agent in a page state a coding agent reached. An issue with findings of a report rule beside its other findings counts under both.
  • Results that cover source files add a Source files: line, and list the files an engine could not check under Source files not scanned:, as Not scanned: lists targets.
  • Counts group their thousands, such as 1,204.
  • When a run covers more than one browser or viewport, Not scanned:, the report directory’s summary and the SARIF log name each target’s and each issue’s, such as in Google Chrome at 1280 by 720. A run that covers one names none.

A fingerprint names a page by its path when the page is on the origin of baseUrl, its scheme, host and port, whatever path baseUrl holds. So the app’s issues are the same on localhost and on a preview deployment, as long as each run’s baseUrl names the origin it scans, from the config, --base-url or CORA_BASE_URL.

A page on any other origin, and every page of a run with no baseUrl, keeps its host in its fingerprint. So cora scan https://a.example/ https://b.example/, outside a project, reports a violation on the same element of both home pages as two issues, and a baseline accepts each on its own site. www.example.com and example.com are two hosts, and a port is part of the host.

The rules reference lists every rule, and whether it runs by default. To change which rules run, see Rules in the configuration guide.

--shard i/n scans the ith of n equal shares of the targets, so n CI jobs can scan in parallel. Each shard writes its own results file:

Terminal window
npx cora scan --shard 2/4 --out results-2.json
  • Every shard plans the same target list, so each target lands in exactly one shard.
  • In a crawl, every shard visits the crawled pages to find their links, but scans only its own share.
  • A sharded run is compared with the baseline only once cora report merge merges every shard. See Sharding in the CI guide.

After writing the results file, cora scan compares the run with the project’s baseline. Its exit code says whether the gate passed or failed, or had no verdict. The CLI reference lists each code.

  • --no-gate and --shard skip the comparison, for a run that cora report merge compares later.
  • An error prints its code, a hint, and a link to its entry in the errors reference.