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-coveragereports where a shop's product URLs were found (#1651). For a site with commerce evidence (a top-levelProductorProductGroupJSON-LD node orog:typeproduct on at least three crawled pages, or on one product-path page plus a cart or checkout link; a nestedProductin a review, list or offer does not count) it adds an info checkproduct-discovery: how many product URLs came from the sitemap, from crawl links or from both, and whether the sitemaps were readcomplete,partialorunreadable. 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-timeoutand[crawler] crawl_timeoutset 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 reportsstatus: "partial", reason codecrawl_timeoutand acrawlPhasecoverage object withlinkGraphComplete: 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.txtcan go out without/llms-full.txt,/.well-known/llms.txtor/docs/llms.txt, and/AGENTS.mdwithout 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.txtand sitemap discovery are unchanged. A 429 or a bot challenge from the audited host (or itswwwor 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-scanis a known cloud service id andthird_party_scana 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_errororunsupported_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 gainslockedRulesandcause/causeDetailonnotCheckedentries (JSON schema 1.13). Reports with no reason keep today's wording (#1734) content/keyword-stuffingno 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
unrenderedFindingscounts (findings on pages no audit has rendered, including a page with no record in the store, which widens no page count) and state the invariantcarriedFindings > 0impliesknownPages > 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-conflictandsecurity/cspnow emit structured evidence: the conflict rule lists its pages (a capped sample with the true count indetails) 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 andaffectedPages()show them instead of only prose (#1735)- Rules can declare the check names they emit by outcome class (
meta.checks:problem,cleanorskipped, with the problem checks a clean checkclears), and the declaration travels in the report's rule meta.security/leaked-secrets,security/hsts,security/x-frame-options,security/cspandcontent/dev-leakagedeclare 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-doorwayandkit-signatureemit a pass when they looked and found nothing;integrity/cloakingandknown-malicious-urlemit a skipped check when the probe or threat intelligence did not run, instead of saying nothing;content/date-agreementandschema/rating-scopeemit 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-secretsreports the scripts it could not read: a new info checkleaked-secrets-unfetchedlists 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,*-omittedand*-unfetchedchecks), 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, JSONmeta.pageTypesas schema 1.14,<page-type>in the llm and XML reports, and aPage 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_pagestool: the page map of a stored audit (the rows ofsquirrel pages -f json) with themissing,duplicate,too_long,orphan,sectionandpagefilters andlimit,offsetandhasMorepaging. It reads the project store, so it needs no login and uses no credits. #701 - New rule
crawl/conflicting-index-signalsfinds 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 theX-Robots-Tagheader, 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 singlesite-wide-noindexinfo check instead.crawl/noindex-in-sitemapkeeps reporting and now names this rule on the pages both find (#1771).
Changed
- The TLS certificate handshake behind
security/tls-certificatenow 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-certificatenow says so in plain words, with no advice about networks, proxies or--offline. The local wording is unchanged. (#3922) - The
--publishhelp text and the shell completions no longer namereports.squirrelscan.com: published reports now live at your organization's address onsquirrel.report, and links on the old host redirect there. - The TLS certificate handshake (
security/tls-certificate) moves back frompackages/fetchersinto 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 groupedlcp-preloadfinding over several pages now lists each page's own candidate images indetails.pageEvidence(capped at 25 pages, withpageEvidenceOmittedPagescounting the rest) instead of keeping one page's candidates (#1656).crawl/sitemap-coverageno longer reports a sitemap URL that answered 429 or a bot-wall challenge as asitemap-orphanswarning. It is listed in an informationalsitemap-unverifiedcheck 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-linksnow 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-linksnow has severityerror. 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 checkpossibly-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/hstsandsecurity/x-frame-optionsnow give each outcome its own check name, assecurity/cspdoes, 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, andhsts(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, andx-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 onsecurity/hsts|hstsorsecurity/x-frame-options|x-frame-optionsfor 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 --publishnow publishes asunlistedby default; it waspublic.squirrel audit, the docs and the agent skill already saidunlisted, and the help now matches. Pass--visibility publicto publish publicly (#1730) squirrel skills statusreads 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 --versionmay differ after one; offline and cloud scores are not comparable; the schema-noindex-conflict page says aheader:noindexsource lives outside the page source (#1730) links/anchor-mismatchno 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 reads0.0.0.core/canonicalis now declared severityerror, matching thefailresults it emits (relative or invalid canonical, canonical to the home page); itswarnresults stay warnings, and a failing canonical now reports as an error instead of a warning (#1746). perf/render-blockingnow measures how long each script fetched during the audit took to answer and warns, in a newslow-blocking-scriptcheck, 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-contrastnow measures text with an inlinecoloragainst the background the markup determines: the nearest inline ancestor background, a plainhtml/body/:rootrule in a<style>block, or the browser's white when no stylesheet can set one. Low-contrast text such as#b9b9b9on 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 messageLimited contrast analysis available(#1752).images/image-file-sizenow names the pages that embed an oversized image in each item'smeta(pages, up to 10, withpageCount) and its label (<url> (1.1 MB, embedded on 2 pages)), as well as insourcePages, and says so when no crawled page is recorded.perf/total-byte-weightnow 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-chainsnow 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 ownredirect-chain-multi-hopwarning, so it no longer reads like a single redirect (#1750).perf/bad-cachingno longer blames Cloudflare for a missing ETag or Last-Modified on pages that sendCache-Control: no-store: the validators message namesno-store, and the freshness check tellsno-storeapart from no caching policy (#1754).security/leaked-secretsfindings now name the pages they sit on: a key in an external script lists the pages that load it insourcePages, and an inline copy on a page is listed with that page inmeta.occurrences, soaffectedPagesCountis 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-leakageno longer reports anhttp://link to the audited site's own host (for example its logo) as a development host; that is mixed content, covered bysecurity/mixed-contentandlinks/https-downgrade. #1758security/leaked-secretsnow lists the scripts astore_scriptsrun fetched but the script rule limit kept from the rule: they are counted inleaked-secrets-unfetchedand 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-contrastnow reads the page background from same-site linked stylesheets, so text whose background comes fromstyles.cssis 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 thehtml,bodyand:rootbackground 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@importor overrides the body background in an at-rule such asprefers-color-schemekeeps 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-inboundonly reports URLs that resolved to a page in the crawl. A footer link to a URL that answers 4xx or 5xx is left tolinks/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-bundlegives 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-404states 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
widthandheightis reported once:images/dimensionsowns it, andperf/cls-hintscls-imagesis now an info row pointing there. Both key the image by its absolute URL, so thecls-imagesfinding you were tracking (which keyed the image by its rawsrc) is resolved on your next audit, and the same image stays open underimages/dimensions. The score is the same as withimages/dimensionsalone (#1768). - A URL that redirects is recorded as a redirect and no longer audited as a page with the destination's content (#1762). Before,
/oldredirecting to/newwas 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-chainandlinks/redirect-chainsstill 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 nomaxPagesslot, a slash redirect either way (/ato/a/) is a redirect while a redirect that only adds a tracking or stripped parameter is the same page, andlinks/redirect-chainsreports 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 ofpages(it is a redirect, not a page: the destination is listed once under its own URL). Redirecting URLs moved frompagesto a newredirectslist,{ url, finalUrl, status, hops }each, withredirectsTotal(JSON schema 1.15), so a consumer that readfinalUrlfrom 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
srcand inline bodies, deduped, at most 50 added per page), sosecurity/leaked-secretsfinds a key such as one in a head script a router drops once hydrated, andperf/critical-request-chains,perf/unminified-jsandperf/render-blockinglist 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 allandauto). core/meta-title,core/meta-descriptionandlinks/internal-linksno 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 nowmeta-title-too-short/meta-title-too-long,meta-description-too-short/meta-description-too-longandinternal-links-too-few/internal-links-too-many; each message carries the length and the limit and each item says "under" or "over".meta-titleandmeta-descriptionremain the names of the missing-title and missing-description failures. Rules can declaresupersededChecks(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 insquirrel 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 pagesinstead of1 image(s) ... (+15 more pages), different items read14 images ... across 9 pages, differing counts become a range, and pages that disagree are listed with their page counts. The aggregate carriesdetails.distinctItems,pageMessageand, when pages disagree,messages. The message merge moved to@squirrelscan/core-contracts/message-mergeand the report re-exports it. schema/coverage-outlierno longer reports a noindex page (robots meta tag orX-Robots-Tagheader) 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-consistencyandsocial/asset-divergenceleave noindex pages (robots meta tag orX-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: