Skip to main content

v0.0.109

Released Oct 12, 2026


Release Notes

Published reports went on a diet. Whatever the size of the crawl, the report squirrel publishes is now a summary capped at 1 MiB: issues carry a sample of their pages and items plus the true counts, the score still comes from every check the audit ran, and the full detail stays on your machine. Heads up if a script or CI job runs it: squirrel config validate now checks [rule_options] and exits 1 on mistakes it used to wave through. Also in this release: squirrel mcp hands your agent the squirrelscan skills directly, Cloudflare pages with JavaScript Detections are no longer mistaken for bot challenges, gzip sitemaps get read, and a pile of rule fixes.

Thanks to @deniskern, whose reports are behind the gzip sitemap, core/meta-title, core/charset, links/tel-mailto, a11y/aria-hidden-focus and a11y/button-name fixes below.

Added

  • squirrel mcp serves the squirrelscan agent skills (squirrelscan 1.13 and audit-website 2.5) over MCP through the Skills extension (io.modelcontextprotocol/skills): skills/list, skills/get and every skill file through resources/read, each listed with its sha256 digest and size. A client that supports the extension gets the skills that match the server it connected to, with no separate install. See Skills.

Changed

  • Behaviour change: squirrel config validate now checks [rule_options] and exits 1 when it finds a problem. It reports a table whose rule ID matches no rule, an option the rule does not declare, a value of the wrong type and an enabled that is not true or false, one Invalid config: line each, naming the table and the option. It used to print Config valid and exit 0 for all of these, so a squirrel config validate && ... step can now fail on a config that used to pass. squirrel audit runs the same check before it crawls: a wrong-typed option on a rule the audit runs stops it with Error: [rule_options."core/meta-title"] min_length: ... instead of failing after the crawl with a bare validation error, and the problems that change nothing are printed as warnings. The config subcommands now read and write the file --config-file names, the file an audit uses, where they always used the nearest squirrel.toml; config set still never creates a missing file. See rule options.
  • A published report is now a summary capped at 1 MiB, however many pages the audit crawled. Each issue lists a sample of its affected pages and items (usually 10 pages and 5 items) with the true counts, and the score still comes from every check the audit ran, not just the sampled ones. On the largest reports the samples shrink, and lower-priority issues can be cut down to their counts or left out. The report says it is a summary and where the full list is: the full per-page detail stays in the local project, and squirrel report --format json exports it. Large audits used to publish bodies of several MB. squirrel audit prints a warning when a report needs more than the usual sampling to fit, and a report that cannot fit at all is refused with PAYLOAD_TOO_LARGE before it is sent. squirrel report --input <file> --publish now refuses a file that has page checks but no crawled page list (the --format json export, or a published report) with PUBLISH_PREPARE_FAILED, where it used to publish a score built from its failures alone: publish the audit from its project with squirrel report <audit-id> --publish. See What a published report holds.
  • squirrel channel now runs on the same MCP SDK (v2) as squirrel mcp, and the CLI no longer ships the v1 SDK or the HTTP server packages it pulled in. The channel still connects with the 2025-era initialize handshake, which Claude Code needs to register it as a channel, and the docs now say so.
  • core/charset reports a UTF-8 page whose charset meta is not first in <head> as placement advice at info status instead of the warning it shared with real encoding problems. Info checks do not count toward the score and are not listed in the report yet. On a page fetched without browser rendering, a charset <meta> that does not end within the first 1,024 bytes of the HTML is now a warning. The rule also warns when the Content-Type header and the page disagree about UTF-8, and reads a quoted header value such as charset="utf-8" without its quotes, but audits do not pass the header's charset to rules yet, so those two take effect once they do. See core/charset.

Fixed

  • Gzip-compressed sitemap files, such as a sitemap.xml.gz child served as application/gzip without Content-Encoding, are now decompressed and parsed. crawl/sitemap-valid used to report each one as Unknown sitemap format and the crawl got none of its URLs. Sitemap requests now ask for gzip (Accept-Encoding: gzip, unless custom headers set another value) and squirrel decodes the response itself, because the runtime's own decoding inflated a whole response before it could be stopped: in our test, a 1 MB gzip bomb sent as Content-Encoding: gzip cost about 1.4 GB of memory. Reading and decompressing now stop at the sitemap protocol's 50 MB uncompressed limit, so a gzip bomb, or a response that keeps sending past 50 MB, is reported as Sitemap exceeds the 50 MB uncompressed size limit, and the same bomb now costs under 70 MB. The limit applies to every sitemap body, so an uncompressed sitemap over 50 MB, which search engines also reject, is now reported the same way instead of parsed. A body that does not decompress is reported as Corrupt gzip body: <reason> (Corrupt br body or Corrupt deflate body for those codings), and a coding squirrel does not decode as Unsupported Content-Encoding: <coding>.
  • On a server that answers every path with a 200 page, the common sitemap locations squirrel guesses (/sitemap.xml, /sitemap_index.xml and six more) are no longer reported as eight invalid sitemaps. A guessed location that answers with an HTML page, or with a body that is not XML, now means no sitemap is there, so crawl/sitemap-exists reports No XML sitemap found where it used to pass on those pages. A sitemap that robots.txt or a sitemap index names and that answers with a page is still a crawl/sitemap-valid failure, now labelled Not a sitemap: the URL returned an HTML page.
  • An ordinary page on a Cloudflare site with JavaScript Detections (which Bot Fight Mode turns on) is no longer classified as a bot-challenge page and set aside from scoring, and ax/agent-blocking no longer reports GPTBot and Claude-User as blocked on such a homepage. Cloudflare's JavaScript Detections script loads from /cdn-cgi/challenge-platform/, which was read as a challenge whenever it fell in the first 10 KB of the page, as it does on a small one. A challenge is now recognised by the challenge's own script (/cdn-cgi/challenge-platform/h/…/orchestrate/), _cf_chl_opt or __cf_chl_, and challenge wording such as "Just a moment" counts only on a 401, 403, 429 or 503, or, for ax/agent-blocking, in the page <title>. A Cloudflare challenge page is still detected and named; a 200 page whose only sign is challenge wording is now scored as a page. Scores on affected sites change, because their pages are now scored.
  • A page that mentions Imperva, or any other bot-protection provider, is no longer attributed to it. Body signatures are now the provider's own markup, script hosts and reference formats, not its name: Imperva's Incapsula incident ID line and /_Incapsula_Resource, DataDome's tag and captcha hosts, PerimeterX's _pxAppId, script host and captcha, Kasada's window.KPSDK and script path, Sucuri's block-page title and heading, Akamai's reference id and error host, and Cloudflare's ray id with its value. The ips.js, cd.js and dd.js attribution patterns, which matched any script whose name ends that way (tips.js, abcd.js, add.js), are gone or anchored to Kasada's own script path. A block page that carries one of these signatures, or the provider's headers, is still detected with its provider. One that carried only the provider's name no longer is, so a headerless 403 like that is reported as an ordinary refusal.
  • A site behind Imperva that answers with x-cdn: Imperva is now recognised as Imperva bot protection, the same as x-cdn: Incapsula, so a blocked crawl names the right provider even when the response carries no x-iinfo header.
  • a11y/aria-hidden-focus and a11y/button-name no longer report controls the markup shows are hidden. Both rules now share one check. It reads the hidden attribute, a closed <dialog> or unopened popover, and an inline style that plainly sets display: none or visibility: hidden. a11y/button-name also skips buttons under aria-hidden="true". An inline style that sets display or visibility in a form the check cannot read exactly (a comment, var(), an unlisted keyword and the like) is reported as an unconfirmed warning (aria-hidden-focus-unconfirmed, button-name-unconfirmed), not a failure. <style> blocks and stylesheets are not read, so a control hidden only by a class or selector rule is still reported. Off-screen and transparent controls are still checked.
  • core/meta-title measures the title as a browser shows it: runs of spaces, tabs and line breaks count as one space and the ends are trimmed. Themes that indent <title> across several lines (Shopware, Shopify) were reported Title too long because of their indentation. The finding's value and item now show the collapsed title, and a title holding only whitespace is reported missing, like an empty one.
  • links/tel-mailto no longer reports a mismatch for a link such as <a href="tel:+4960616309800">+49 (0)6061 630 980 0</a>. The (0) is the optional trunk prefix, which is dropped after the country code, so the link dials the number shown. That display form is now formatting advice in a new tel-trunk-prefix check at info status, which does not count toward the score and is not listed in the report yet. The reading applies only to the link text and only to calling codes whose trunk prefix is 0 (+31, +32, +33, +41, +43, +44, +49, +61, +64 and +353); a display number that really differs from the href goes through the mismatch check as before. See links/tel-mailto.
  • perf/total-byte-weight no longer counts a page's inline CSS and JavaScript twice. The HTML size already includes them, and the rule added them again, so a page with a 1.8 MB inline stylesheet read as about 3,600 KB. The breakdown's HTML total holds the inline code, and its CSS and JavaScript totals now hold external files only. See perf/total-byte-weight.
  • content/hidden-text reads <style> sheets and inline styles the way a browser tokenizes them. A {, } or ; inside an unquoted url(...), inside a ( or [ block, or after a backslash no longer ends a rule or a declaration, so a sheet such as .a{background:url(}.b{display:none}.c{)}, which a browser reads as one rule, is no longer reported as hiding .b. See content/hidden-text.
  • Template fan-out no longer shares a verdict between pages that differ in the <html> language or aria-hidden, the <body> aria-hidden, or a favicon link. Those are inputs three template rules read.
  • squirrel audit no longer seems to hang after printing the report url on a machine with many local projects. Marking the report as published opened every project database under ~/.squirrel/projects until it found the audit (5.5 s for 2,000 projects, and far longer on a large store); it now opens only the audited project's database. squirrel report --publish marks the database the report was loaded from.
  • A release download that stalls no longer hangs squirrel self update or the background updater. The download used to be bounded only until the server sent its headers, so a body that stopped arriving left the detached updater waiting forever, holding the update lock until the lock went stale after 30 minutes. A download now fails when no data arrives for 60 seconds or it has not finished after 20 minutes, the lock is released, and a later run tries again. The new bounds apply to updates made from this version on: the update to this version still runs on the previous release's downloader.
  • Docs: custom request headers are on every plan, Free included, where several pages said Pro only, and the pages that still called local audits free in general now say a local audit costs nothing only when it runs signed out or with --offline. The firewall page now covers letting an audit through Cloudflare Access, Vercel deployment protection and HTTP basic auth, the failed audits guide walks through each failure cause, and the hosted MCP page lists every tool, get_website_settings and update_website_settings included. The docs, README and npm package now give one rule count: 300+.

Update to this version

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

$

Downloads

macOS

Detected
Intel
squirrel-0.0.109-darwin-x64 · 76.1 MB
Download
SHA-256
Apple Silicon (M-series)
squirrel-0.0.109-darwin-arm64 · 69.7 MB
Download
SHA-256

Linux

x64 (musl/Alpine)
squirrel-0.0.109-linux-x64-musl · 81.4 MB
Download
SHA-256
x64
squirrel-0.0.109-linux-x64 · 87.4 MB
Download
SHA-256
ARM64
squirrel-0.0.109-linux-arm64 · 87.3 MB
Download
SHA-256
ARM64 (musl/Alpine)
squirrel-0.0.109-linux-arm64-musl · 80.6 MB
Download
SHA-256

Windows

x64 (Intel/AMD)
squirrel-0.0.109-windows-x64.exe · 93.4 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.