Skip to main content

v0.0.179

Released Nov 12, 2026


Release Notes

Audits that hit a slow or blocking site now end cleanly: the crawl phase has its own time budget (--crawl-timeout, [crawler] crawl_timeout), a crawl it stops reports a partial status with a crawl_timeout reason, sitemap URLs that answered with a rate limit or a bot wall are listed as unverified instead of as orphans, and the JSON report carries a crawlPhase block (schema 1.12). Rules now say what they checked: each rule can declare its check names by outcome class, rules that ran and found nothing record a clean pass instead of an empty list (scores can rise by about a point), and security/leaked-secrets reports the scripts it could not read so a secret is never read as removed. External link results are sharper (confirmed broken links fail, awaiting-confirmation has its own warning), perf/render-blocking flags slow blocking scripts, a11y/color-contrast measures inline colours, redirect chains name every hop, and content/dev-leakage stops flagging the site's own http:// links. Reports count pages per page type (schema 1.14) and carry the machine-readable reason for locked and not-checked rules (schema 1.13), the llm format labels its scores, and the local MCP server gains list_pages. a11y/color-contrast also reads the page background from same-site linked stylesheets and leaves text inside containers they paint as not measured, links/no-contextual-inbound only reports URLs that resolved to a page, perf/content-in-js-bundle gives one count per site whatever the crawl order, and security/leaked-secrets counts the scripts a store_scripts run held back. crawl/sitemap-coverage says where a shop's product URLs were found, [crawler] discovery_probe_paths approves discovery probes one path at a time, the TLS probe is shared with a plain message when the certificate was not checked, and the publish help and shell completions no longer name reports.squirrelscan.com.

This release also carries the second bundle that was merged before the release was cut: a URL that redirects is recorded as a redirect and no longer audited as a page (the page count and the page charge drop for sites with redirecting links), core/meta-title and core/meta-description report too short and too long as separate checks, folded messages state a range and the number of pages, rendered audits read the scripts hydration removes, and the new rule crawl/conflicting-index-signals finds pages that say index and do not index at once.

Added

  • crawl/sitemap-coverage reports where a shop's product URLs were found (#1651). For a site with commerce evidence (a top-level Product or ProductGroup JSON-LD node or og:type product on at least three crawled pages, or on one product-path page plus a cart or checkout link; a nested Product in a review, list or offer does not count) it adds an info check product-discovery: how many product URLs came from the sitemap, from crawl links or from both, and whether the sitemaps were read complete, partial or unreadable. A crawl stopped at its page limit is reported as bounded coverage. It never claims a product is missing, because neither a sampled crawl nor a sitemap is a product inventory. Sites without commerce evidence see no change.
  • squirrel audit --crawl-timeout and [crawler] crawl_timeout set the crawl-phase backstop, bounded at 6 hours. Unset, it is derived from the page budget and the effective per-host rate instead of a fixed 30 minutes, capped at 60 minutes (an explicit value can go to 6 hours, which weakens the wedge backstop). A crawl stopped by it reports status: "partial", reason code crawl_timeout and a crawlPhase coverage object with linkGraphComplete: false (#1639).
  • Approve discovery probes one path at a time. [crawler] discovery_probe_paths (and --discovery-probe-path, repeatable) lists the probe paths an audit may request, so /llms.txt can go out without /llms-full.txt, /.well-known/llms.txt or /docs/llms.txt, and /AGENTS.md without its variants. A probe left out is not sent and its rule reports not checked (path not approved), never missing; a redirect from a listed path may not change the path, and the per-host limits, robots.txt and sitemap discovery are unchanged. A 429 or a bot challenge from the audited host (or its www or apex twin) now also stops the optional probes still waiting, which report not checked; a 429 from another host, such as a third-party sitemap host, does not. Unset keeps today's behaviour. (#1655)
  • third-party-scan is a known cloud service id and third_party_scan a known credit feature, both free (cost 0). Plumbing for the cloud third-party script scan: no CLI behaviour changes.
  • Locked and not-checked rules can carry a machine-readable reason (plan, provider_error or unsupported_environment, with a short detail), and every report format says what it means: a plan exclusion names the plan, a provider failure says a re-run may help, an unsupported environment says where the check can run. The JSON report gains lockedRules and cause/causeDetail on notChecked entries (JSON schema 1.13). Reports with no reason keep today's wording (#1734)
  • content/keyword-stuffing no longer counts a price or rating line that repeats unchanged across five or more cards ("Shipping costs from $4.90"): it is card microcopy like one whose number varies. The 20% deduction cap, the raw-density limit and the floor are unchanged, so a stuffed phrase dressed in a fixed price is still flagged; the cap on large card grids is a separate decision. The rule page states what is and is not counted, and the short contact page with a plain menu is pinned (#1441)
  • The smart-audit coverage docs now say what unrenderedFindings counts (findings on pages no audit has rendered, including a page with no record in the store, which widens no page count) and state the invariant carriedFindings > 0 implies knownPages > auditedPages, with a test over a capped run that rotates its pages. Pages outside the crawl that hold a finding but have no record are still not counted as pages: that count needs the hosted store (#1031)
  • crawl/schema-noindex-conflict and security/csp now emit structured evidence: the conflict rule lists its pages (a capped sample with the true count in details) and each page's blocking source and schema types; the CSP rule names the page the header was read from, the header, and the directive carrying 'unsafe-inline' with its value. Cloud issue views and affectedPages() show them instead of only prose (#1735)
  • Rules can declare the check names they emit by outcome class (meta.checks: problem, clean or skipped, with the problem checks a clean check clears), and the declaration travels in the report's rule meta. security/leaked-secrets, security/hsts, security/x-frame-options, security/csp and content/dev-leakage declare theirs, so a consumer can tell that "the clean check ran" supersedes a problem check without guessing from names. Nine rules no longer return an empty checks list: integrity/brand-impersonation, fake-auth-overlay, obfuscated-script, seo-doorway and kit-signature emit a pass when they looked and found nothing; integrity/cloaking and known-malicious-url emit a skipped check when the probe or threat intelligence did not run, instead of saying nothing; content/date-agreement and schema/rating-scope emit a pass or a skipped check. No check is renamed. Scores can rise by about a point: those rules now record a clean pass where they used to record nothing, and a pass counts toward the score. A test pins the rules that have not adopted the declaration yet, so the list only shrinks and a new rule must declare its checks (#1741)
  • security/leaked-secrets reports the scripts it could not read: a new info check leaked-secrets-unfetched lists each referenced script with no content and a status other than 404 or 410 (a timeout, a 5xx, a refused request, a WAF challenge, a throttled request, a fetch skipped for its budget) and counts scripts the asset fetch cap never requested. While any script is unread the rule no longer passes, so a secret in a script that could not be fetched is never read as removed; a script that is really gone (404 or 410) is not listed. The report's size cap never drops a row that says part of the site was not read (unread-page, scan-truncated, and *-unscanned, *-omitted and *-unfetched checks), whatever its status (#1749)
  • The report counts, per URL-based page type, the pages the crawl knows of and the pages it audited (scanScope.pageTypes, JSON meta.pageTypes as schema 1.14, <page-type> in the llm and XML reports, and a Page types audited: line in the text, markdown and console reports), so a capped or Surface audit can say "1 of 36 product pages". A complete audit reports equal counts (#1744).
  • The local MCP server has a list_pages tool: the page map of a stored audit (the rows of squirrel pages -f json) with the missing, duplicate, too_long, orphan, section and page filters and limit, offset and hasMore paging. It reads the project store, so it needs no login and uses no credits. #701
  • New rule crawl/conflicting-index-signals finds pages that say "index me" and "do not index me" at once: a page listed in a sitemap that also carries at least two signals keeping it out of the index (noindex in the robots meta tag or the X-Robots-Tag header, a canonical to another URL, a robots.txt block). It lists each page once, with the signals found and the related rules, and asks whether the page should be indexed. A site that is closed as a whole (80% or more of at least three pages noindexed or blocked) gets a single site-wide-noindex info check instead. crawl/noindex-in-sitemap keeps reporting and now names this rule on the pages both find (#1771).

Changed

  • The TLS certificate handshake behind security/tls-certificate now lives in a shared package (@squirrelscan/fetchers/tls-probe) with the same egress checks and proxy handling, and accepts an explicit CONNECT proxy URL. CLI behaviour is unchanged. (#3922)
  • When a hosted audit cannot check the TLS certificate, security/tls-certificate now says so in plain words, with no advice about networks, proxies or --offline. The local wording is unchanged. (#3922)
  • The --publish help text and the shell completions no longer name reports.squirrelscan.com: published reports now live at your organization's address on squirrel.report, and links on the old host redirect there.
  • The TLS certificate handshake (security/tls-certificate) moves back from packages/fetchers into the CLI (apps/cli/src/audit/tls-probe.ts), its only user, so the shared packages hold no raw socket code. CLI behaviour is unchanged.

Fixed

  • perf/lcp-hints: a grouped lcp-preload finding over several pages now lists each page's own candidate images in details.pageEvidence (capped at 25 pages, with pageEvidenceOmittedPages counting the rest) instead of keeping one page's candidates (#1656).
  • crawl/sitemap-coverage no longer reports a sitemap URL that answered 429 or a bot-wall challenge as a sitemap-orphans warning. It is listed in an informational sitemap-unverified check with its status, and sitemap URLs a stopped crawl never reached are informational too (#1639).
  • A link to a reserved suffix (.invalid, .test, .example) is reported as broken (no such domain suffix), not as "not requested": it can never resolve for a visitor. refused-external-links now covers only private, loopback, link-local and metadata addresses and its message no longer mentions DNS. A non-resolving name on a real TLD stays broken (domain does not exist), and a resolver that times out or fails never marks a host dead (a definitive NXDOMAIN is confirmed on a second lookup). The main check no longer says "All N external link(s) are working" when some links were unverified or never requested: it says how many were verified working out of how many were found. The rule's advice says a lapsed domain can be registered by someone else (#1740)
  • Behaviour change: links/broken-external-links now has severity error. An external link confirmed broken (a 404, a 410, a domain that does not exist or a reserved suffix) fails; links that may be broken (a 5xx, a timeout, a TLS or connection error, another 4xx) warn under a new check possibly-broken-external-links; a link that failed a first check and awaits its confirming one has its own warning check, awaiting-confirmation-external-links ("likely broken, being confirmed"), and is no longer listed as blocked; links that cannot be verified (a bot wall, rate limiting, a refused address, over the limit) inform as before. Confirmed-broken external links now count as failures in the score (#1739)
  • Check rename: security/hsts and security/x-frame-options now give each outcome its own check name, as security/csp does, so a consumer can tell a missing header from a present-but-weak one. HSTS: hsts-no-pages, hsts-not-applicable, hsts-missing, hsts-malformed, hsts-disabled, hsts-max-age-short, and hsts (adequate, unchanged). X-Frame-Options: x-frame-options-no-pages, x-frame-options-missing, x-frame-options-allow-from, x-frame-options-invalid, x-frame-options-csp-frame-ancestors, and x-frame-options (a valid header, unchanged). Findings that carried the old single name re-key once on the next audit (a missing, short or invalid header shows as a new finding and the old one resolves), and anything keyed on security/hsts|hsts or security/x-frame-options|x-frame-options for a failing outcome must use the new names (#1737)
  • The Quick level summary says what Quick does not check. The line shown on the level picker now says Quick does not follow links (a linked page outside the sitemap gets a status check only and is not audited) and skips the cloud checks, so a Quick audit with no broken links reads as what it is. (#2729)
  • Behaviour change: squirrel report --publish now publishes as unlisted by default; it was public. squirrel audit, the docs and the agent skill already said unlisted, and the help now matches. Pass --visibility public to publish publicly (#1730)
  • squirrel skills status reads the version of a skill symlinked from a checkout, marks it (linked) and flags it behind the latest, instead of showing -; the checkout is never written (#1730)
  • Docs: any command can trigger the CLI auto-update and squirrel --version may differ after one; offline and cloud scores are not comparable; the schema-noindex-conflict page says a header:noindex source lives outside the page source (#1730)
  • links/anchor-mismatch no longer reports a "page -> target" pair as the affected page: each item names its source page alone, and the target and anchor text stay in the label, so the llm, markdown, JSON and cloud views agree (#1729).
  • Behaviour change: the llm report format labels its two kinds of score (scope="group" on each <group>, scope="category" on each <cat>) and prints the version recorded in the report as the <audit version>, so a stored report no longer reads 0.0.0. core/canonical is now declared severity error, matching the fail results it emits (relative or invalid canonical, canonical to the home page); its warn results stay warnings, and a failing canonical now reports as an error instead of a warning (#1746).
  • perf/render-blocking now measures how long each script fetched during the audit took to answer and warns, in a new slow-blocking-script check, when a synchronous <head> script took more than 2000 ms, or more than 1000 ms when it was not the first script fetched from its host (that one pays for DNS and TLS), so a script near the limit does not open and close an issue between audits, with the measured time in the message and on the item. The rule now runs per page (it was copied across a template cluster) because the measurement is looked up by each page's resolved script URLs. Synchronous head scripts were already listed with the stylesheets (#1751).
  • a11y/color-contrast now measures text with an inline color against the background the markup determines: the nearest inline ancestor background, a plain html/body/:root rule in a <style> block, or the browser's white when no stylesheet can set one. Low-contrast text such as #b9b9b9 on white is reported with its colors and ratio. Where a stylesheet the audit does not read may set the background, the text is not reported but counted in a new info check, color-contrast-unresolved, which names why. A page with no inline text color now passes, with a note that there was nothing to measure, instead of returning the info message Limited contrast analysis available (#1752).
  • images/image-file-size now names the pages that embed an oversized image in each item's meta (pages, up to 10, with pageCount) and its label (<url> (1.1 MB, embedded on 2 pages)), as well as in sourcePages, and says so when no crawled page is recorded. perf/total-byte-weight now names the largest resource on the heaviest page, with its size, in the message (..., largest resource <url> <size>KB (...)) and in the details (heaviestResource, heaviestResourceKb, heaviestResourceKind) (#1753).
  • links/redirect-chains now labels a chain of two or more redirects with its hop count and every hop (3 hops: r1 (301) → r2 (301) → r3 (301) → final (200)) and reports it in its own redirect-chain-multi-hop warning, so it no longer reads like a single redirect (#1750).
  • perf/bad-caching no longer blames Cloudflare for a missing ETag or Last-Modified on pages that send Cache-Control: no-store: the validators message names no-store, and the freshness check tells no-store apart from no caching policy (#1754).
  • security/leaked-secrets findings now name the pages they sit on: a key in an external script lists the pages that load it in sourcePages, and an inline copy on a page is listed with that page in meta.occurrences, so affectedPagesCount is no longer 0. The rule is renamed from "Leaked Environment Variables" to "Leaked Secrets and API Keys", since a hard-coded key is not an environment variable (#1755).
  • content/dev-leakage no longer reports an http:// link to the audited site's own host (for example its logo) as a development host; that is mixed content, covered by security/mixed-content and links/https-downgrade. #1758
  • security/leaked-secrets now lists the scripts a store_scripts run fetched but the script rule limit kept from the rule: they are counted in leaked-secrets-unfetched and up to 20 are named (not scanned (script limit)), and the rule withholds its pass while any exist. The check's message now says "the script limit or the asset fetch cap" where it said only the asset fetch cap (#1757)
  • a11y/color-contrast now reads the page background from same-site linked stylesheets, so text whose background comes from styles.css is measured instead of listed as not measured: the asset phase fetches up to 20 same-site stylesheets (1 MB each, same egress rules and fetch budget as scripts) and keeps only the html, body and :root background color. A light background reports the measured ratio, a dark theme does not warn for light inline text, and a stylesheet that fails to load, is on another site, uses an @import or overrides the body background in an at-rule such as prefers-color-scheme keeps the pair unresolved, never assumed white. A background on any other selector (.hero { background: #111 }) is not read, and text inside an element that selector matches is counted as not measured instead of measured against the page background (#1756)
  • links/no-contextual-inbound only reports URLs that resolved to a page in the crawl. A footer link to a URL that answers 4xx or 5xx is left to links/broken-links, a redirecting URL is judged at its destination, and chrome-only targets the crawl never fetched are counted in a skipped check with the not-fetched reason instead of being reported (#1760)
  • perf/content-in-js-bundle gives the same count and verdict for the same site whatever order the crawl delivered pages, scripts and loading pages in: pages are sampled, chunks ranked and loaders listed in URL order, the single-page message states the measured page count like the sitewide one, and a count that is a floor (pages sampled out, search stopped early) now says "at least N pages (M checked)" (#1761)
  • crawl/soft-404 states one source of evidence: the server's HTML (and that a client-rendered page could differ) or the rendered page content, never both, and no longer says it cannot verify without rendering when it read a render (#1766).
  • An image with no width and height is reported once: images/dimensions owns it, and perf/cls-hints cls-images is now an info row pointing there. Both key the image by its absolute URL, so the cls-images finding you were tracking (which keyed the image by its raw src) is resolved on your next audit, and the same image stays open under images/dimensions. The score is the same as with images/dimensions alone (#1768).
  • A URL that redirects is recorded as a redirect and no longer audited as a page with the destination's content (#1762). Before, /old redirecting to /new was counted as a page and listed in the page list of content, metadata, accessibility, performance and legal rules, describing /new, which was audited as itself too. Now the crawler stores the redirect (URL, hops, destination) with no body and queues the destination, so it is audited once under its own URL, whether the redirect came from a link, the sitemap or a known URL. crawl/canonical-chain, crawl/redirect-chain and links/redirect-chains still report the redirecting URL and its hops, and the pages-audited count and the "on N pages" counts leave it out. Findings stored against a redirecting URL resolve on your next audit. A redirect that ends in an error or loops, and one to a destination the crawl will not visit (an excluded or not-included path), stay pages as before. A redirect row takes no maxPages slot, a slash redirect either way (/a to /a/) is a redirect while a redirect that only adds a tracking or stripped parameter is the same page, and links/redirect-chains reports a page whose canonical names a URL that redirected to it (canonical-redirects). Billing: pages audited and the page charge no longer include redirecting URLs, so they can be lower than before for a site with redirecting links.
  • Behaviour change: with --include-pages, a URL that redirected is no longer a row of pages (it is a redirect, not a page: the destination is listed once under its own URL). Redirecting URLs moved from pages to a new redirects list, { url, finalUrl, status, hops } each, with redirectsTotal (JSON schema 1.15), so a consumer that read finalUrl from a redirecting page row still finds it (#1762).
  • Rendered audits read the scripts the server HTML sent even when client code removes them after hydration (#1769). The audited script set is the union of the server HTML's and the rendered DOM's (external src and inline bodies, deduped, at most 50 added per page), so security/leaked-secrets finds a key such as one in a head script a router drops once hydrated, and perf/critical-request-chains, perf/unminified-js and perf/render-blocking list the script as a server-sent resource. The result no longer depends on render timing. Applies when a plain-HTTP copy of the page is available beside the render (--render-mode all and auto).
  • core/meta-title, core/meta-description and links/internal-links no longer report both directions under one check name, which made a 93-character title read "Title too short" once pages were aggregated (#1765). The checks are now meta-title-too-short / meta-title-too-long, meta-description-too-short / meta-description-too-long and internal-links-too-few / internal-links-too-many; each message carries the length and the limit and each item says "under" or "over". meta-title and meta-description remain the names of the missing-title and missing-description failures. Rules can declare supersededChecks (old check name to its replacements): a finding stored under the old name is resolved when every replacement passed, stays failing when one failed (continuing under the new name) and stays carried when one was not checked, in the cloud and CLI merge and in squirrel report --diff. Health scores can rise by about 2 points, because a page inside both limits now records two passes where it recorded one; pages that fail are unchanged.
  • A check folded across many pages no longer states one page's problem for all of them (#1767). One image missing alt text on 16 pages reads 1 image missing alt attribute, shown on 16 pages instead of 1 image(s) ... (+15 more pages), different items read 14 images ... across 9 pages, differing counts become a range, and pages that disagree are listed with their page counts. The aggregate carries details.distinctItems, pageMessage and, when pages disagree, messages. The message merge moved to @squirrelscan/core-contracts/message-merge and the report re-exports it.
  • schema/coverage-outlier no longer reports a noindex page (robots meta tag or X-Robots-Tag header) for missing the structured data its siblings have. Noindex pages are also left out of the group that sets the norm, and a site where every page is noindex is reported as not applicable instead of passing. (#1770)
  • local/nap-consistency and social/asset-divergence leave noindex pages (robots meta tag or X-Robots-Tag) out of their cross-page comparison on an indexed site, on both engine paths, so a staging copy or thank-you page with different details no longer makes the site look inconsistent. A site whose compared pages are all noindex is reported as not applicable instead of a pass (#1773).
  • A check folded across pages whose messages differ by a per-page measurement in parentheses states the range and the pages, for example "Slow server response (850 to 1200ms) on 2 pages", and a list of distinct problems reads "Multiple H1 tags found (2 to 5) on 3 pages; H1 tag is empty on 2 pages" instead of repeating the sentence per value (#1767).

Update to this version

squirrelscan will auto-update, or run this command to update now:

$

Downloads

macOS

Detected
Intel
squirrel-0.0.179-darwin-x64 · 110.9 MB
Download
SHA-256
Apple Silicon (M-series)
squirrel-0.0.179-darwin-arm64 · 102.3 MB
Download
SHA-256

Linux

x64 (musl/Alpine)
squirrel-0.0.179-linux-x64-musl · 115.1 MB
Download
SHA-256
ARM64
squirrel-0.0.179-linux-arm64 · 121.2 MB
Download
SHA-256
ARM64 (musl/Alpine)
squirrel-0.0.179-linux-arm64-musl · 114.0 MB
Download
SHA-256
x64
squirrel-0.0.179-linux-x64 · 121.2 MB
Download
SHA-256

Windows

x64 (Intel/AMD)
squirrel-0.0.179-windows-x64.exe · 127.0 MB
Download
SHA-256

Audit your site in one command

SEO, performance, security, accessibility and agent experience issues, with exact fixes for your coding agent.

Install
$

No account needed for the CLI. Cloud audits include free monthly credits.