Skip to main content

Releases

Release history and changelogs

All releases up to v0.1 will be in the stable channel.

Release channel

Release History

v0.0.103Oct 5, 2026Latest

Release Notes

Two rule fixes from community bug reports: fewer false accessibility warnings on honeypot and hidden fields, and noindex set by a response header now shows up in the report.

Fixed

  • a11y/aria-hidden-focus no longer reports elements a keyboard user cannot reach. Inside an aria-hidden region, an element with tabindex="-1", a disabled form control, a hidden input, and anything under an inert or hidden element now count as not focusable, matching axe-core. Honeypot spam fields from form builders were the usual false positive, on every page that had the form. A link or button you can still Tab to inside aria-hidden is reported as before. Thanks to @deniskern for the report (#456).
  • core/robots-meta now reports noindex and nofollow sent in the X-Robots-Tag response header, including the crawler-scoped form (X-Robots-Tag: googlebot: noindex). Before, a page kept out of search only by the header appeared nowhere in the report. Each message now names its source, so a directive set by a CDN or server config is easy to tell from one in the page source; existing robots meta tag messages end with "via robots meta tag". Thanks to @deniskern for the report (#457).

Downloads

Windows
Linux
v0.0.102Oct 3, 2026

Release Notes

The spend line after a signed-in audit now counts the same pages your report shows as audited.

Fixed

  • The cloud credits line after a signed-in audit counts the pages the report shows as audited: its coverage line ("Coverage: audited 12 of 12 known pages"), or its page count when there is no coverage line. Before, it used the crawl count, which also includes error pages such as a 404 kept for its broken-link finding, so the line could show more pages than the report did. A cloud render paid for a page that turned out to be an error is refunded when the audit completes, and pages a smart audit carries forward from an earlier audit are not charged again. The server settles the charge for every CLI version, so this changes what the CLI prints, not what you pay. The credits page explains the count.
v0.0.101Oct 2, 2026

Release Notes

The CLI's credit estimates and spend line now match how signed-in audits are billed, and a balance that can't cover the page limit lowers the limit instead of running out part-way.

Changed

  • Signed-in audits are estimated and billed at 50 credits plus 2 per audited page, however the page was fetched: plain HTTP, a cloud browser render, the render cache, a 304 Not Modified, or a plain fetch after a render failed. Each page is charged once per audit, and rendering does not change the price, so choose --http or rendering for accuracy, not to save credits. A 19-page audit costs 88 credits.
  • A cloud audit pays 1 credit per distinct external link it checks, and the same link on twenty pages is one check. Link checks the CLI runs for squirrel audit, and all internal links, stay free.
  • A balance that can't cover the page limit now lowers the limit, with a notice saying how many pages it covers, instead of running out part-way. [cloud] max_credits_per_audit works the same way: its default of 1,000 covers 475 pages. Below 52 credits (the base plus one page) the audit runs local-only.
  • squirrel credits, the out-of-credits messages and the consent prompt state the same price, and the cloud spend line after an audit matches what your balance was charged, for example Cloud credits used: 88 (audit-base 50, 19 audited pages 38). Earlier versions are billed the same way, but their estimate and spend line can show less than was charged.
  • The failed audits guide has a section for each cause a cloud audit can fail with, saying what it means, how to check it and what to do.
v0.0.100Oct 2, 2026

Release Notes

Large audits use about half the memory and keep every page they crawl, an audit that cannot write its local store says so before it starts, and a round of rule fixes stops flagging module scripts, <noscript> fallbacks and CSP host wildcards that were never a problem.

Changed

  • Large audits use about half the memory. The rules phase no longer holds every page's findings until the report, and the report no longer reads every page body back out of the local store. A 4,000-page audit of 1.5 MB pages peaked at about 3 GB, down from about 5.8 GB, and the report step is several times faster. Findings and scores are unchanged.
  • The local content store's 1 GB cap is configurable: [storage] content_store_max_bytes in squirrel.toml takes a byte count or a size such as "4GB", and SQUIRREL_CONTENT_STORE_MAX_BYTES sets it for one run. The default is still 1 GB. squirrel self doctor shows the store's size, the cap and the last eviction, and warns when the store is over the cap.

Fixed

  • An audit never evicts its own pages from the local content store. Once a crawl outgrew the cap, the store pruned the audit's earliest pages before the rules read them back, and those pages quietly dropped out of the audit. An audit now keeps every page it stored until it finishes, even past the cap, and the next one prunes the store back down. When pages were evicted earlier, a re-audit requests them again in full and says how many, and squirrel report and a resumed audit say how many are missing.
  • An audit whose local store it cannot write now stops before it starts, with one line naming the file, why (owned by root after a sudo run, blocked by a coding agent's sandbox, read-only, a directory, damaged, disk full) and the command that fixes it. Before, it failed partway with attempt to write a readonly database, or fetched every page, saved none and ended with a bare "No pages were crawled". Nothing is charged when the check fails (#403).
  • A signed-in audit that fails within its first second, or whose terminal is closed, now shows as failed or cancelled in your dashboard. Before, it could be flipped back to running and an hour later marked "stopped reporting progress". Closing the terminal now cancels the run, the same as Ctrl-C.
  • Pages that embed fonts as base64 in a <style> block no longer stall the audit. a11y/color-contrast read that CSS in quadratic time, so a chat widget's 428KB of @font-face data held one page for over a minute; the same page now takes milliseconds, with the same findings. links/tel-mailto had the same flaw on a mailto link wrapped around a long unbroken run of text.
  • perf/inp-hints, perf/render-blocking and perf/critical-request-chains no longer count <script type="module"> as render-blocking. Module scripts are deferred by default, nomodule scripts never run in a modern browser, and scripts with a non-JavaScript type (consent-gated text/plain, Partytown) never block either. The three rules now agree on which scripts block. Thanks to @deniskern for the report (#424).
  • Rules no longer report content inside <noscript> as if it loads or renders. Tracking pixels, GTM iframes, and fallback images and stylesheets placed there for visitors without JavaScript stopped showing up as missing alt text, untitled frames, missing SRI and the like, on roughly 30 rules. Thanks to @deniskern for the report (#434).
  • a11y/aria-required-children accepts the implicit roles of native elements, so <input type="radio"> inside role="radiogroup" counts as a radio, and the same goes for options, list items, rows and cells. Thanks to @deniskern for the report (#435).
  • security/csp no longer calls a host wildcard like https://*.example.com a bare *; a bare * host still warns (#427).
  • perf/source-maps reports a source map from a shared bundle once, listing every page that loads it, with a separate finding per section bundle, instead of one finding per page.
  • crawl/sitemap-domain and the sitemap-orphans checks list a 50-URL sample with the exact count. A 100,000-URL sitemap used to make a 12 MB JSON report on its own; it is now about 50 KB, and the orphan count no longer stops at 10,000.
v0.0.99Sep 30, 2026

Release Notes

squirrel setup gets you from install to your first audit in about a minute, the CLI installs and updates the agent skills itself, --no-publish really does skip the publish, and --disable-discovery-probes turns off the pre-crawl requests that got some audits banned by server firewalls.

Added

  • squirrel setup: one guided first run. It signs you in (optional, local audits never need an account), installs the agent skills for Claude Code, Codex, Cursor and more (default yes), and turns on auto-update, then hands you the command for your first audit. Run it again any time: each step says when it is already done. --yes takes every default, --dry-run shows what it would do. The installers now end with one next step, squirrel setup.
  • Running squirrel on its own shows a quick start and every command grouped by what it is for (audits, agents, cloud, settings), in place of a flat list that ended in ERROR No command specified. squirrel --help, group help such as squirrel self, and each command's --help use the same layout, with examples.
  • The CLI, install.sh and install.ps1 share one header: the pixel squirrel from squirrelscan.com, in the site's colours. It uses truecolor or 256 colours depending on what the terminal supports, a plain silhouette with NO_COLOR or piped output, and text alone without UTF-8.
  • squirrel feedback works without a terminal, so an agent or a script can send it. Pass the text with -m/--message, as plain words, or piped on stdin and it sends without a single prompt. --json prints one result object and, on any failure, exits 1 with an error code and the web form's URL. --run-id and --website-id say which audit or site it is about, and --email sets the reply address, which otherwise falls back to the one you last gave, then your signed-in account's. At a terminal with no text it asks, as before.
  • send_feedback on squirrel mcp: the local server gets the feedback tool the hosted one already had, sent through the same path as squirrel feedback.
  • squirrel audit --disable-discovery-probes, or [crawler] disable_discovery_probes = true in squirrel.toml, turns off the requests an audit sends before its first page for the files agents look for: llms.txt, /.well-known/*, /openapi.json, /swagger.json, AGENTS.md, and the homepage fetched as GPTBot and Claude-User. Some firewalls ban a client for asking for /swagger.json. A common fail2ban "sensitive files" jail does exactly that, every request after it gets connection refused, and the audit fails right after it starts. robots.txt, sitemap discovery and the page you asked for still run. The flag wins over the config for one run, so --disable-discovery-probes=false sends the probes when the config turns them off. The crawler docs list every request, and how to let the audit through your firewall instead.

Changed

  • Input piped to squirrel feedback is now the feedback itself, not answers to its prompts. A pipe left open and silent for 5 seconds counts as finished, so the command can't hang waiting on it.
  • squirrel skills installs and updates the agent skills itself: no Node, no npx, no other tool. It reads the published manifest.json in squirrelscan/skills, downloads only the files that changed, checks every one against its sha256, and only then writes the skills to ~/.claude/skills/ ($CLAUDE_CONFIG_DIR/skills/ when set) and ~/.agents/skills/ (Codex, Cursor, Gemini CLI and other Agent Skills tools) as real copies, so it works the same on macOS, Linux and Windows. A failed or mismatched download writes nothing and leaves the previous install as it was.
  • The commands: squirrel skills (the same as squirrel skills status: what is installed where against the latest version), install [skill...] [--agent claude|agents|all] [--project] [--dry-run], update [skill...] [--check] [--force] [--dry-run] [--json] and uninstall [skill...] [--agent ...] [--project], which removes only what squirrel wrote. update --check exits 1 when an update is available, and 2 when the check itself fails, for scripts and agents. --project means the repo you run it in (the nearest folder with a .git), and each repo's copies are updated from inside that repo.
  • A skill file you edited is never overwritten silently. squirrel records the hash of every file it writes; update keeps an edited file and says so, and --force replaces it after backing your copy up to ~/.squirrel/skills/backups/. Your own files in a skill folder stay put.
  • Skills installed earlier with npx skills are taken over: squirrel updates them in place, replaces the symlinks npx skills made with copies, and removes them from its lock file after copying it to .skill-lock.json.bak, so the two tools stop updating the same folders. A skill folder that links elsewhere, such as your own checkout, or that holds another author's skill of the same name, is left alone.
  • Auto-update keeps the installed skills current. On the first run after the CLI updates itself, a background job updates the skills you already have (never one you don't, and never inside a repo): no prompts, edits kept, once per version, a 60 second limit, and no network at all when none are installed. It follows auto_update, is off in CI and with SQUIRREL_NO_UPDATE, and squirrel self settings set skills_auto_update false turns off just the skills. It runs after the update has landed, so it can never fail or delay it.
  • The agent skills and agent plugins have one home: squirrelscan/skills. This repo no longer carries copies: its skills/ directory and the Claude Code, Cursor and Agent Plugins manifests are gone. The Claude Code plugin (/plugin marketplace add squirrelscan/skills), the Cursor plugin and the Agent Plugins package each bundle both skills with the hosted MCP server there. None carries a version, so Claude Code versions its plugin by commit and every change to the skills reaches plugin users without a CLI release.
  • If you added the squirrelscan/squirrelscan Claude Code marketplace, it keeps working: its plugin entry now points at squirrelscan/skills, so /plugin marketplace update squirrelscan then /plugin update squirrelscan@squirrelscan moves you over. The Cursor plugin can't redirect that way; install it from squirrelscan/skills.
  • The discovery probes follow your rule selection. A probe goes out only when a rule that reads it is going to run, so --rule-exclude ax sends none of them and --rule-exclude ax/api-discovery leaves out the OpenAPI, Swagger and OAuth paths.
  • The discovery probes queue behind per_host_concurrency and per_host_delay_ms like every page does. They used to go out 20 at a time, whatever those were set to, so a polite crawl was polite everywhere except in its first second. They are plain requests, so they keep those settings when cloud rendering loads pages one at a time.
  • An agent experience check whose probe was not sent, or whose probe for a file like /llms.txt or /.well-known/mcp.json got no answer (a refused connection from a firewall that just banned the audit, or a timeout), says "not checked" (value not-checked) instead of reporting the file missing, and is not scored.

Fixed

  • squirrel skills install works again. In v0.0.98 it stopped with "No valid skills found" once the skills moved to their own repository; it now installs them from squirrelscan/skills directly.
  • A mistyped command now says what you probably meant (squirrel audti suggests squirrel audit, squirrel skills instal suggests squirrel skills install) and exits 1 without a stack trace. A missing argument shows the command's usage and an example, and a command group run on its own (squirrel self) shows its help and exits 0.
  • squirrel -c <path> <command> works: the config path given before the command was read as the command name ("Unknown command").
  • squirrel skills update updates the skills. It used to pass a repository to npx skills update, which reads its arguments as skill names, so it matched nothing installed and changed nothing.
  • squirrel audit --no-publish skips the publish. The flag never reached the command, so a signed-in run published anyway, or ended in "Could not auto-publish" when the connection failed. --no-publish together with --publish (or -p) now stops with an error before the audit starts, whatever the order. Before, the pair was accepted and --no-publish ignored.
v0.0.98Sep 22, 2026

Release Notes

A crawl no longer slows down as your local page cache grows, and the entity map that travels with a report now carries a representative sample of a large site instead of an alphabetical slice of it.

Fixed

  • A crawl's speed no longer tracks the size of your lifetime page cache. Storing a page ended in a count over the whole content store, an aggregate no index can answer without walking every row, so a long crawl got slower as it went and the same audit took longer against a large cache than against an empty one. The store now answers from a running total, and pruning drains to its target in batches instead of stopping short and pruning again on the very next page. A 400-page audit against a roughly 1 GB store now matches the same audit against an empty one, 17.4s against 17.5s; before it was 28.2s against 17.8s on a cold store.
  • The entity map published with a report keeps a sample of the site rather than its alphabetical head. Entities declared on several pages come first, then one-off entities stratified by type so a catalogue of thousands of products cannot crowd out the organisation, website and brand entities the map exists to show, and the per-page entities a graph hides by default are held to a small share. Every type present in the map keeps at least one entry.
  • The copies of the entity map that travel are bounded and say what they dropped. The published copy and the copy inlined into an HTML report each have their own node, edge, string and total-size limits, and both now carry the number of entities, references and pages left out. A clipped map is no longer indistinguishable from a small site. The summary still describes the whole site, not the sample, so the totals you read are the real ones. Your local copy on disk is unchanged and stays complete.
v0.0.97Sep 19, 2026

Release Notes

The macOS binaries carry a signature macOS accepts again, the installer stops blaming memory for a binary the system killed, and a Sitemap: line in robots.txt written without a scheme is read as the host it names.

Fixed

  • The macOS binaries are signed again. bun build --compile writes its bundle into the executable but leaves the linker's ad-hoc signature describing the file as it was before, so codesign --verify --strict failed on every darwin asset we have published, and a Mac that enforces signatures killed the binary on sight. That is what the installs failing on Apple Silicon with exit 137 were hitting. Both darwin binaries are now re-signed after the build and before anything hashes them, and a macOS step verifies the signature and the recorded hashes before a release can publish.
  • The installer no longer tells a Mac user that a killed binary is out of memory. On macOS it names the two causes that are actually possible, a rejected code signature or security software, gives the command that tells them apart, and puts the macOS version in the report it sends.
  • A Sitemap: line in robots.txt that leaves out the scheme is read as the host it names instead of as a path on the audited site. Sitemap: example.com/sitemap.xml is now fetched at https://example.com/sitemap.xml, where it used to be fetched at https://example.com/example.com/sitemap.xml and 404. A declared sitemap whose path is not one of the common locations is reached again, so its URLs count towards sitemap coverage, orphan pages and missing pages, and a sitemap declared on another host (a CDN) is fetched there, still without the audit's own request headers. The report no longer lists a robots.txt sitemap failure that only our own resolution created, stores the resolved URL in every field that carries one, and reports the missing scheme as a robots.txt problem rather than repairing it silently.
v0.0.96Sep 17, 2026

Release Notes

Every audit now builds a map of the entities your site declares, a new command and five MCP tools read it, thirteen rules check it, and the leaked-secrets scanner went from a thousand false alarms on real sites to a few hundred correct public keys.

Added

  • Entity map on every audit. Structured data across the whole crawl is resolved into one graph (organisations, people, products, articles, and the pages that declare them), stored with the audit and embedded in every report format. Report-only: no score changes.
  • squirrel entities queries that map: a summary of what your site declares, a lookup by @id, key or name, and --list over stored audits. --type, --page and --problem narrow the result in every output format, not just on screen.
  • Export the graph as csv, dot, graphml or mermaid, alongside the existing json, jsonld, html and markdown.
  • squirrel entities --diff compares two audits of a site and reports entities added and removed, @id changes, and new and resolved conflicts and dangling references. An entity is only called removed when every page that declared it was crawled again; anything else is reported as not crawled, so a smaller crawl never reads as a site that deleted its structured data.
  • Five entity tools on squirrel mcp: list_entities, get_entity, get_entity_graph, compare_entities (with occurrence_threshold) and get_entity_findings. They read the local project store: no auth, no credits, no round trip.
  • Thirteen schema/entity-* rules over the map: missing organisation and website entities, split identities, conflicting declarations, dangling references, orphans, publisher mismatches, type drift, @id format, missing sameAs, authors, and one local business per page. Each has a rule doc page.
  • After a local audit, a signed-in user whose reports have never reached the cloud gets a one-line nudge to publish (with an explicit unlisted visibility). It shows once; the first publish switches it off for good.
  • When an audit is refused for lack of credits, the CLI shows the cost, your balance, the reset date and an upgrade link scoped to the organisation that hit the wall, on the preflight warning and on squirrel credits. The wall also prints the balance again (it never did: the number was read from the wrong place in the response).
  • After a published audit of a site on a recurring schedule, the CLI prints one line naming the cadence and the link that turns it off. When the site wanted a schedule but the plan's scheduled-site slots are spent, the line says so instead, with the plan's limit and an upgrade link. squirrel report --publish keeps a bare URL on stdout and prints the line on stderr. The line renders what the cloud sends, never a guess of its own.
  • Leaked-secrets decodes what it scans: HTML character references, \u and \x escapes in scripts, and base64 runs that decode to text (data URIs and SRI hashes are never decoded). AWS access key ids, GitHub tokens, JWTs (Supabase service_role is high, anon is public, an expired token is a new leaked-secrets-expired info check), Algolia secured keys, Stripe, Slack and Sentry tokens are decoded offline so the finding carries what the token is, and a GitHub token with a bad checksum is dropped.

Changed

  • Leaked-secrets precision. Prefix patterns respect a left boundary, brand keywords must sit within 40 characters before the value, minified member keys no longer count as assignments, and public-by-design keys (PostHog, Shopify Storefront, Mixpanel, Raygun, and generic API keys under twelve public brands) report as public instead of leaks. On a private corpus of 383 real pages: 1,235 findings before, 352 after, none high. Scans are also faster.
  • Leaked-secrets checked against a 197-site launch corpus. Twelve false-positive classes fixed, each paired with the real shape it must still catch: a run of A in a base64 payload is not a Twitter bearer token, a decoder table is not a Telegram token, a font CDN path is not a DigitalOcean key, a credential word in a ternary branch is not a key, a connection string with no password has nothing to leak, and Amplitude and Supabase publishable keys report as public while sb_secret_ keys are now caught. On the 56 sites that fired at the previous release: 172 findings before, 0 after, with no true positive lost.
  • The Discord bot token pattern no longer goes quadratic on long base64 runs (1.6 s to 2.5 ms on a 200 KB data URI page).

Fixed

  • Reports print the page limit you asked for alongside the one the run used, so a run clamped from 10,000 to 4,320 pages no longer tells you to raise a limit that was already higher.
  • Merged check messages carry their counts instead of a bare N. When pages disagree on a count the message shows the range (3 to 6 image(s) missing alt); pages that disagree on any other number (a date, a size, a code) keep their own message instead of being merged. Digits inside words are left alone, so H1 no longer reads HN.
  • perf/bad-caching no longer fails a page that revalidates correctly. A document served with max-age=0 or no-cache plus an ETag or Last-Modified declares a caching policy and passes; no-store, and a validator with no Cache-Control and no Expires, still do not. On the launch corpus this check failed 163 of 196 sites, most of them doing the right thing.
  • integrity/template-discontinuity no longer calls a site's own signed-out or empty-state page "likely injected". A page that loads the site's own stylesheets or scripts is reported for review, never escalated; shared public hosts (font and script CDNs) do not count as the site's own. Escalation is now a warning rather than a failure, so one off-template page cannot zero Site Integrity, and the escalation is withheld when the crawl hit its page limit with fewer than minBaselinePagesWhenCapped pages (default 20).

Docs

  • New pages for the entity map (CLI, formats, cloud), cloud crawl scope, include/exclude patterns and authorised crawls; the rule catalogue counts refreshed.
  • Scheduled audits: the docs now name every place a schedule is disclosed (report page, completion email, digest, CLI line), the resolved findings the digest lists, and what happens when a run is skipped for credits.
  • Scheduled audits: a table of the five schedule states (off, active, capped, unschedulable, paused), what the dashboard shows for a site whose plan slots are spent, and how a freed slot is filled.
  • Adding a website in the dashboard now asks for a scan depth (default surface) and a schedule (default weekly, on from creation); the docs page for adding a website describes both and what happens when the plan's scheduled-site slots are spent.
  • The hosted MCP server's reference page lists its five entity tools, and the published API spec documents the entity export endpoint's eight formats and its new exclude_type parameter.
  • The cloud entity map docs describe the dashboard's history view: metric series across audits and the change list between two audits, with the eight change kinds it can show and the one gap it cannot yet.
v0.0.95Sep 12, 2026

Release Notes

Two fixes for scores that moved when nothing on the site had, one for local audits that reached out to the cloud, and an installer that works where GitHub does not.

Changed

  • Audits of localhost, loopback, and private-network addresses no longer hand anything to a hosted runner: no publish, no run registration, no cloud rendering, and no site added to your dashboard. This covers squirrel report --publish as well as squirrel audit, and internal names such as box.local and metadata.google.internal as well as private IP ranges. The audit, its report, and the content-based cloud analysis are unchanged, and the CLI prints one line saying the report stayed local. Hosted services cannot reach those addresses, so a dashboard site for one could never be screenshotted, re-rendered, or re-audited.

Fixed

  • Published cloud reports no longer count findings the CLI carried from its local store as pages crawled this run. A machine with an old local audit database could publish thousands of stale findings, which the cloud stored as first seen today and scored as if every one of those pages had just been crawled, swinging the health score by tens of points between identical runs. Carried findings keep the date the cloud last saw them, and the audited-page count is now the pages the crawl actually visited.
  • Two page URLs that differ only by their query string are two pages again. Findings on /p?id=1 and /p?id=2 were stored under one URL, piling every page's issues onto a single entry in the report.
  • A signed-in audit of a local or private-network address no longer submits its pages to the cloud render service, which charged on submit for renders the hosted browser then refused.
  • The installer now downloads from install.squirrelscan.com when GitHub is unreachable, so an install no longer dies on networks that cannot reach GitHub's release hosts. Both sources are checksum-verified against the release manifest. Set SQUIRREL_FORCE_MIRROR=1 to skip GitHub from the start, and if both hosts fail the installer now prints the two URLs it tried.
  • A download host that answers with a sign-in or error page instead of the release manifest is now treated as a failed source, so the installer moves on to the other host rather than stopping on a parse error. This is what a captive portal or an intercepting proxy looks like.
v0.0.94Sep 9, 2026

Release Notes

A one-line follow-up to v0.0.93 for the cloud audits that still ended before their first page: the quick crawl phase now waits for the entry page's retry.

Fixed

  • A quick cloud audit's crawl phase now holds the entry page's worst case. The 130 s phase ended before the entry retry added in v0.0.93 could report, so a slow origin still failed with "no pages collected". Quick runs get a 210 s crawl phase and a 330 s runtime (the post-crawl slice is unchanged); surface and full are untouched. squirrelscan/repo#1699 squirrelscan/repo#2026
v0.0.93Sep 9, 2026

Release Notes

A release for the audits that never started. Two cloud fixes stop a slow or oddly-shaped origin from ending an audit before its first page (one site had failed every attempt since August), the CLI starts on a third of the memory so install.sh no longer dies at the last step on capped machines, and a re-audit of an unchanged site replays its rule results instead of re-running them. Plus three contributed fixes from the community. We are convinced this is the fastest way to crawl and audit a site, and it is now also the most patient one.

Changed

  • A re-audit of an unchanged site replays its rule results. A page whose inputs its rules read are all unchanged is neither parsed nor rule-checked again; its stored results are replayed. On a 2,500-page site the warm rules phase falls from 110 s to 12 s and the warm audit from 143 s to 44 s, with the report byte-identical to a cold run. Site-scope rules always run. When the cache is on, template fan-out (#279) is off for that run, which costs the cold rules phase about 7%; set SQUIRREL_RULE_CACHE=0 to disable the cache. Figures are in benchmarks/2026-09-perf-program.md. squirrelscan/repo#1990

Fixed

  • Cloud rendering now reserves time for its plain-HTTP fallback inside each page's fetch deadline: the render has the deadline minus 12 seconds to itself, then the plain fetch starts and races it, and whichever lands first serves the page. A render that finishes late but inside the deadline is still used, and a crawl stop mid-submit no longer leaves the server's render debit unrecorded. The cloud runner's per-page deadline rises from 12 to 30 seconds (the CLI's default) so a far-away origin no longer decides the audit; the preamble budget, sitemap walk window and entry-page retry that derive from it are documented next to the constant. squirrelscan/repo#2026

  • A cloud audit of a site whose sitemap points at another domain no longer fails with "No pages were crawled from <site>: The operation was aborted." when the entry page's first fetch hits its deadline. The entry URL now gets one more plain fetch with a doubled deadline before the audit is given up (a warning event says so), an abort from a document fetcher classifies as a timeout that names the fetcher and the deadline instead of unknown with the runtime's text, and a crawl whose preamble budget runs out records a warning naming the budget. squirrelscan/repo#1699

  • squirrel starts light. Every invocation used to evaluate the whole CLI (the audit engine and every rule package included) before parsing a single argument, which put squirrel self install at about 140 MB resident and got it killed (exit 137) at the last step of install.sh on memory-capped machines. Subcommands and the startup extras now load on demand, and the standalone binary is built with --splitting, so self install and --version peak at about 43 MB. Commands that need the engine load it when they run. #2023

  • install.sh finishes the install itself when self install is killed by a signal (the file work needs no memory), then checks that the binary runs. A binary that will not run is reported under its own step with the binary and link paths, a memory figure, the swap recipe, and the cloud dashboard as the no-binary alternative, instead of "retry". #2023

  • squirrel self update now checks that the binary your PATH resolves is the one it just installed, and says so when it isn't. It used to flip the symlink recorded at install time and report success on that alone, so a stale install_bin_dir (or a second squirrel earlier on PATH) left you running the old version after every "Updated to vX". A recorded bin directory that no longer exists is now dropped, and the update falls back to the default one. #293

  • squirrel self doctor gained an Install location check: the recorded install_bin_dir, the link and the release version it points at, and the squirrel your PATH actually resolves, with a warning when they disagree. #293

  • The script fetch cache is reachable again. It stored a script under the hash of its content but looked it up by the hash of its URL, so every audit re-downloaded every script. Both sides now key by URL, a cached script older than a day is fetched again, and a refresh replaces the stale row instead of keeping the first body forever. Thanks to @WilliamK112 for the fix. #182 #206 #303

  • The CLI's end-of-run report to the cloud is retried on transport errors and 5xx responses, three attempts with a short backoff, so a completed audit is no longer left "running" on the dashboard until the reaper finds it. Thanks to @WilliamK112. #178 #208

  • install.ps1 enables TLS 1.2 before its first network call, so Windows PowerShell 5.1 with legacy protocol defaults can download the release. Thanks to @WilliamK112. #166 #207

v0.0.92Sep 8, 2026

Release Notes

A release about running again. v0.0.91 made a big audit fit in memory; this one makes the second audit of the same site cheaper than the first, lets a local audit crawl 10,000 pages, and cuts the rules pass on script-heavy pages by 40%. It also adds the disk tooling a project database needs once audits accumulate, and fixes the CLI minting API keys against the wrong organization for accounts with more than one.

By the numbers

Every figure is from the checked-in record in benchmarks/2026-09-perf-program.md. Laptop rows are a cold squirrel audit --coverage full, heap sampled after a forced collection at exit.

v0.0.91v0.0.92
2,500-page audit, wall time502 s177 s
2,500-page audit, retained heap3,902 MB996 MB
1,000-page audit, wall time194 s128 s
1,000-page audit, retained heap1,662 MB565 MB
Largest local audit5,000 pages10,000 pages in 12 min, 5.4 GB peak RSS
SQL statements compiled on a warm 120-page re-crawl1,48248
Rules pass on a 1 MB script-heavy page274 ms165 ms
Page-rule CPU on a templated site (25 template-scoped rules)13% less
Report assembly at 1,000 pages, peak memoryabout 150 MB less
Hosted publish merge carrying 60,000 prior findings330 MBunder 100 MB
Hosted re-audit of an unchanged 150-page site115 pages rendered17 rendered, report byte-identical
Project database after six audits of a 40-page site14.7 MB11.9 MB

Added

  • squirrel self disk shows where ~/.squirrel goes, and --prune gets it back. Every audit keeps its full history in the project database, so a re-audited site grows by roughly one audit per run (about 95 MB per audit of a 1,000-page site). squirrel self disk lists per-project and total usage. squirrel self disk --prune --keep N retires the audits beyond the newest N, prints the plan, asks, and then rebuilds the database so the space returns to the filesystem. --keep is required: a retired audit can no longer be rendered, and report --list, --diff and --regression-since reach into that history, so the window is your call. Retiring keeps everything the next audit reads (the newest page record per URL, sub-resources, links, images), so an incremental re-crawl still gets its ETags. A retired audit stays listed, marked with the date its data was reclaimed, and report, --diff, --regression-since and analyze refuse it instead of rendering an empty report.

  • squirrel keys takes --org, and auth whoami says which org you are. On an account with more than one organization, keys create minted against the newest one rather than the active one, silently. It now takes --org <slug|id>, lists the organizations and refuses when there is more than one and no --org, and keys list and keys revoke are scoped the same way. auth status and auth whoami print the active organization.

  • The audit says when --max-pages was clamped. Asking for more pages than the cap allows used to run at the cap and say nothing. The audit now prints the requested and effective limits, and the JSON and LLM reports carry both, so a clamped audit is distinguishable from a complete one.

  • Template-aware rules. Pages of one site share their chrome (header, navigation, footer, script stack) far more than their structure: a real storefront's 247 pages fall into 13 chrome clusters. Every page now carries a template cluster key, and page rules whose verdict depends only on the chrome (viewport, doctype, favicon, consent mode, tag manager, font and script delivery, subresource integrity, and others) run once per cluster with the verdict fanned out to the cluster's other members, with findings byte-identical to running them on every page. About 13% less rule CPU on that storefront. SQUIRREL_TEMPLATE_FANOUT=0 turns it off.

  • The crawl says when its base is the wrong host of a pair. When the seed redirects between the apex and www and the probe is refused, the crawl used to pin itself to the wrong host and drop every link as cross-domain, producing a one-page audit. The probe now sends a real user agent, recovers the base from the links it sees, and warns when the two disagree.

  • A project keeps its last 3 audits. Re-auditing wrote a whole new crawl and retired nothing, so project.db grew by about one audit every time: roughly 95 MB per audit of a 1,000-page site, forever, with nothing saying so. A successful audit now retires the audits older than the newest three, which turns that growth into a ceiling. Set the window with [storage] keep_audits, or turn it off with 0 or false. A retired audit stays listed and says when its data went; it can no longer be opened, diffed, or used as a --regression-since baseline, so raise the window if you keep an old audit as a reference. A run that ends failed or blocked neither retires anything nor takes a place in the window, and the page cache the next audit reads is never part of what goes: a re-audit after retirement still serves every unchanged page from its conditional GET. squirrel self disk --prune now offers to rebuild a project whose audits were already retired, which is what returns the freed space to the filesystem.

Changed

  • The rules pass on script-heavy pages is 40% cheaper. On a page carrying 800 KB of inline script the rules phase cost 274 ms per page; it is now 165. The secret scan and keyword scans skip any pattern whose mandatory literals are provably absent from the page, the skip-link rule stops serializing the whole body once per heading, and two script rules stop counting with regular expressions that built arrays to read their length. Findings are unchanged.

  • The report reads a crawl's checks once, not twice. Assembling the report loaded every rule result twice; it now loads them once and reuses the rows, which is about 150 MB less peak memory at 1,000 pages. The report also stops building per-page fields (response headers, image lists, structured data) that no output format or renderer ever read.

  • A warm re-crawl compiles seven statements, not thousands. The storage layer prepared its per-page statements on every call; they are cached now, which takes a re-crawl of an unchanged site from about twelve compilations per page to well under one.

  • Hosted audits stream the carried side of the publish merge. Finalizing a hosted re-audit held every previously open finding in memory at once, about 5 KB each, so a site carrying 60,000 open findings needed 330 MB inside a 128 MB worker. Prior findings now stream from a cursor and the report's carried side is a bounded per-rule sample with exact counts, which brings that case under 100 MB with the same scores.

  • A page fingerprint survives a Shopify cache regeneration. Shopify rewrites request ids and shuffles app-block order between two fetches of the same page, which made every page look changed. The fingerprint now ignores those.

  • A local audit crawls up to 10,000 pages. The hard cap on --max-pages and [crawler] max_pages was 5,000; it is now 10,000, on every plan, local audits being free either way. A 10,000-page audit peaks at about 5.4 GB of memory and takes about twelve minutes on a laptop, so the cap is a real ceiling rather than a formality: past it, split the audit by section with include patterns. Cloud audits follow their plan instead, and Team's ceiling rises to 10,000 with this release.

    Publishing a report from a crawl this size needs an API that accepts it, so update the CLI only after the hosted side has: an older server rejects a publish carrying more than 2,000 page statuses or 5,000 crawled URLs.

  • A local audit no longer holds the whole site in memory. The CLI's post-crawl phases ran the resident pipeline: one parsed page plus its DOM per crawled page, held from the end of the crawl through the entire rules pass, and a second full read of every page to assemble the report. Retained heap grew about 1.5 MB per crawled page, so a 2,500-page audit ended holding 3.9 GB while the operating system's "resident" figure read a reassuring 1.4 GB. Every phase after the crawl now walks the pages table in batches sized to the site and drops each batch before reading the next, which is what the hosted runtime has done since v0.0.91. Reports are unchanged, page for page.

    Batch size follows the site's own average page against a byte budget. SQUIRREL_STREAM_BATCH_BYTES sets that budget (48 MB of raw HTML by default) and SQUIRREL_STREAM_BATCH_PAGES pins an exact page count instead. Turning the budget below roughly a dozen pages' worth is counterproductive: the same crawl becomes several times as many read-parse-collect cycles and peaks higher, not lower.

  • A 429 is reported as a rate limit, not a broken link. The link rules used to count a rate-limited response as broken; it now needs the status lead-in like every other status.

  • Crawl bookkeeping fixes. The frontier's page-exists check always answered yes; the content-store prune check answers from a covering index instead of a full scan on every stored page; the container's cloud-prefetch payloads no longer hold their pages alive after use.

v0.0.91Sep 7, 2026

Release Notes

A release about big sites. A 500-page audit of a store whose pages weigh a megabyte each used to hold every parsed page in memory at once, run out of room, and fail three times in a row after the crawl had already finished. The audit engine now streams: pages are parsed in batches sized to the site, the rules pass runs over one batch at a time, and the report is assembled from per-rule tallies instead of from every finding at once. The same pass makes a finding mean one defect instead of one defect per page, adds three content rules for things that should never have shipped, and tightens the installer.

Added

  • Three content rules for leftovers. content/placeholder-text flags lorem ipsum, unrendered template tags such as {{ user.name }}, [object Object], undefined in a table cell and TODO markers in visible copy. content/unrendered-markup catches literal markdown, escaped HTML and double-encoded entities that reached the page as text, and stays quiet inside code blocks and syntax-highlighted samples. content/dev-leakage finds localhost, private IP ranges, staging. and dev. hosts, preview deploys (vercel.app, netlify.app, pages.dev, ngrok) and plain http:// links back to the site's own origin, on a production page.

  • A zero-page audit says why. When the crawl fetches nothing, the report, the CLI output and the failure notice now name the cause: the host did not resolve, the certificate was rejected, the connection closed before a response, the origin timed out, or it answered with a 4xx or 5xx. Before, every one of those read "No pages were crawled".

Changed

  • The audit engine streams instead of holding the whole site. Link checks, asset fetches, the rules pass and the report are all built from batches of parsed pages rather than one array of every page. The batch is sized from a byte budget (48 MB of raw HTML by default, SQUIRREL_STREAM_BATCH_BYTES) divided by the site's own average page size, so a docs site with 20 KB pages and a store with 1 MB pages both fit. Peak memory now tracks the batch, not the page count. Progress output reports the sub-phases of the pass (universe, site fetch, page rules, site rules, assemble) as they happen.

  • A finding is one defect, not one defect per page. An item-level finding such as a cross-origin script without Subresource Integrity now carries its own message and locator instead of inheriting the page's count ("26 resources without SRI" on one page, "24" on the next). The same script missing on 400 pages is one finding with 400 affected pages. This also fixes the reverse defect, where unrelated items on the same page shared a fingerprint because the page count matched.

  • Longer budget for large full audits. The per-page time allowance for a full-coverage audit rose from 4.8 to 7.2 seconds, so a 500-page audit of script-heavy pages reaches the one-hour ceiling instead of being cut off partway through the rules pass.

  • The installer pins its transport. Every curl in install.sh now refuses anything but HTTPS (including across redirects), requires TLS 1.2 or newer and follows at most three redirects.

  • The CLI is built on Bun 1.4. The runtime the squirrel binary is compiled with moved from 1.3.14 to 1.4.0, the release Bun rewrote in Rust. The visible effect is size: the Linux binaries that the install script and containers pull are 11 to 14 percent smaller (about 90 MB instead of 101 MB for glibc, 83 MB instead of 97 MB for musl and Alpine), the Windows binary is 8 percent smaller, and the CLI starts about 10 ms faster. Audit results are byte-identical across the two runtimes: the same sites produce the same scores, the same issue counts and the same reports. Every install target was built and run on the new runtime, including the Alpine and musl path.

  • A DNS failure is reported as a DNS failure. Bun 1.4 is the first runtime that distinguishes a host that does not resolve from one that refuses the connection, and the CLI now passes that distinction through, so an audit of a mistyped domain says the name did not resolve rather than that the server was unreachable.

v0.0.90Sep 3, 2026

Release Notes

A release about sites that do not answer the way you expect. A store that throttles, an origin that sends headers and then goes quiet, a seed URL that bounces somewhere else: each of these used to produce a report that was either wrong or silent about what it did not see. This pass teaches the crawler to wait when it is told to wait, to give up when nothing is coming, and to say in the report which of those happened. It also adds a compression check for assets, closes three gaps in the leaked-secrets scan, and lets the CLI apply an update it already knows about before it runs.

Added

  • Rate limiting is handled as rate limiting. A 429, a Shopify 430, or a 503 that carries Retry-After now pauses every worker aimed at that host, drops it to one request in flight, and waits out the backoff (honouring Retry-After when the server sends one) instead of retrying three times in a second while the other workers keep firing. A new [crawler] max_backoff_ms setting (default five minutes) caps how long one host can hold the crawl. Throttled URLs are no longer reported as broken links or 4xx sitemap entries: they move to an "unverifiable" bucket, the report carries a rateLimited count and host list, and a crawl that lost pages this way is marked partial with a reason that names the host. The progress line says which host is throttling and how long it is waiting.

  • Uncompressed text assets are reported. New perf/asset-compression rule flags stylesheets, scripts and other compressible text resources over 100 KB that were served with no content encoding, biggest first, with the pages that load them and the estimated saving. A server that gzips its HTML but not its CSS is a common misconfiguration and was invisible before.

  • The CLI applies a pending update before it runs. When auto-update is on and an earlier background check already found a newer version, the next command installs it first and re-runs itself on the new binary, so a run right after a release no longer reports results from the version before it. The wait is capped at two minutes and any failure falls back to running the command on the current binary.

  • An agent setup prompt after login. squirrel auth login now ends, in the terminal and on the browser success page, with the one-line prompt that points a coding agent at the setup docs, with a copy button.

  • Free plan schedules one website. Scheduled audits are no longer the paid line. The free plan can schedule one website weekly or monthly; daily schedules and more websites remain on paid plans.

Fixed

  • A stalled body no longer hangs the crawl. Every fetch deadline was cleared the moment response headers arrived, so an origin that answered 200 and then trickled or stopped its body could park the crawl forever before it had even started. The deadline now stays armed through the body read, in the crawler, the engine's script and external-link fetchers, and the CLI's own copies of them.

  • The crawl preamble has one budget. The root probes that run before page one (redirect resolution, robots, llms.txt, well-known files and friends) share a single wall-clock budget instead of seven sequential timeouts, so a slow origin can no longer spend the whole crawl phase before fetching a page. A probe that ran out of budget is recorded as unknown, never as a confirmed absence. The sitemap walk runs on a progress window instead, so a site with many legitimate sitemaps is slow but complete.

  • A refused off-site redirect is disclosed. When the seed URL redirects off its own domain the crawler refuses to follow it and audits the seed. The report now says so in every format, and says where the redirect pointed, so the scores cannot be read as being about a URL nobody visited.

  • Leaked-secrets finds three more shapes. A quoted JSON key ({"apiKey":"…"}), an environment fallback (process.env.KEY || "…"), and a standard-base64 bearer token were each slipping past the scan. All three are detected now.

  • Leaked-secrets no longer reports a spaced-out checksum. A sha256 key with a run of whitespace before its value was read as an unnamed assignment and reported as a key. The look-back now spends its budget on characters that carry meaning, and refuses to read a word out of a fragment it cut in half, so a checksum stays a checksum and a real credential cut mid-name still reports.

  • Leaked-secrets is fast on pages with thousands of matches. Overlap suppression compared every finding against every other, which was quadratic: an 875 KB page of 16,000 keys spent two seconds in that one loop. It now indexes by window and produces byte-identical results in a fraction of the time.

  • Markdown reports keep their metadata out of the warning. The partial-scan hint was emitted as a blockquote in the middle of the metadata lines, and CommonMark's lazy continuation pulled the recovery note and the version into the quote. It is a plain line now.

Changed

  • Domain stats are paused. The domain-level summary (backlinks, referring domains, organic keywords and traffic, domain rank) described a site's market position rather than anything you could fix in the code, and it sat awkwardly next to everything else a report tells you. cloud.domain_stats now defaults to false and the section is no longer rendered or charged for. Reports that already captured it keep their section. It may come back.
v0.0.89Sep 1, 2026

Release Notes

A release about not crying wolf. A rule that reports a problem that is not there costs more than a rule that stays quiet, because it teaches people to skim past the whole category, and a handful of checks had a shape that looked like a defect and was not: a redirect that no server ever sent, a checksum read as a leaked key, a decorative image read as a missing description. This pass fixes those, tightens how large sites and large reports are judged, and makes a few failures explain themselves instead of leaving a bare error.

Fixed

  • A redirect chain is only reported when a redirect actually happened. The crawler normalizes a URL before it fetches it, so a link written one way and fetched another looked like a hop even though no server sent a 3xx. Both the redirect-chain and canonical-chain checks now build a chain only from responses the crawl actually observed, so a tidy site that simply writes its links in a different form than it serves them is no longer told it redirects to itself.

  • Attribute names are matched the way a browser matches them. HTML attribute names are case-insensitive, but the parser was comparing them exactly, so markup that spells an attribute in camelCase (inputMode, or ALT in caps) slipped past the rules that read it. That was a quiet source of both missed findings and false warnings on framework-rendered pages, and it is fixed everywhere at once.

  • An empty alt on a decorative image is correct, not missing. A spacer or a purely decorative image that carries alt="" and aria-hidden is doing the right thing, and the alt-text check no longer counts it as a missing description.

  • Checksums are no longer mistaken for leaked secrets. A SHA-256 hash printed on a downloads or releases page is a 64-character hex string, which the leaked-secret scan was flagging as a possible key. It is not one, and it is no longer reported.

  • Mojibake detection leaves code alone. The garbled-text check now skips code, pre, samp and kbd, where byte sequences that look like mojibake are usually just the sample being shown on purpose.

  • A large sitemap is held to the right limit. A sitemap index is allowed 50,000 child sitemaps, and each of those up to 50,000 URLs, but the check was applying the per-file URL limit to the index as a whole, so a healthy sharded site with more than 50,000 URLs across its shards was told its index was too big. The index and the shards are now each measured against their own limit.

  • Sitemap freshness reads the page's own date. The lastmod-drift check now compares a sitemap entry against the page's own document-level date rather than the first date found anywhere in its structured data, so a sitewide organization or website node dated years ago no longer makes an up-to-date page look stale.

  • A first audit no longer implies a previous one. Findings on pages the audit has not rendered yet are now labelled "unrendered" instead of "carried", which used to suggest a prior audit existed when it did not.

  • A large report records what it left out. When a report reaches the cap on how much per-page detail it keeps for a single rule, it now records the true affected-page and check totals it clipped, so the coverage on a big crawl is never quietly understated.

  • An install killed for memory explains itself. A curl | sh install on a small machine can be killed by the out-of-memory killer, which leaves no output at all, so the installer reported nothing but a number. It now names the likely cause and the ways out: add swap, move to a machine with more memory, or download the binary and place it yourself.

  • A malformed cloud response no longer ends the audit. A success response from the hosted editor-summary that was missing its prose used to crash the run after the crawl had already finished. The CLI now takes what it can and completes the audit.

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.