Releases
Release history and changelogs
All releases up to v0.1 will be in the stable channel.
Release History
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
Downloads
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=0to disable the cache. Figures are inbenchmarks/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
warningevent says so), an abort from a document fetcher classifies as a timeout that names the fetcher and the deadline instead ofunknownwith the runtime's text, and a crawl whose preamble budget runs out records a warning naming the budget. squirrelscan/repo#1699 -
squirrelstarts light. Every invocation used to evaluate the whole CLI (the audit engine and every rule package included) before parsing a single argument, which putsquirrel self installat about 140 MB resident and got it killed (exit 137) at the last step ofinstall.shon memory-capped machines. Subcommands and the startup extras now load on demand, and the standalone binary is built with--splitting, soself installand--versionpeak at about 43 MB. Commands that need the engine load it when they run. #2023 -
install.shfinishes the install itself whenself installis 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 updatenow 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 staleinstall_bin_dir(or a secondsquirrelearlier 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 doctorgained an Install location check: the recordedinstall_bin_dir, the link and the release version it points at, and thesquirrelyour 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.ps1enables 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
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.91 | v0.0.92 | |
|---|---|---|
| 2,500-page audit, wall time | 502 s | 177 s |
| 2,500-page audit, retained heap | 3,902 MB | 996 MB |
| 1,000-page audit, wall time | 194 s | 128 s |
| 1,000-page audit, retained heap | 1,662 MB | 565 MB |
| Largest local audit | 5,000 pages | 10,000 pages in 12 min, 5.4 GB peak RSS |
| SQL statements compiled on a warm 120-page re-crawl | 1,482 | 48 |
| Rules pass on a 1 MB script-heavy page | 274 ms | 165 ms |
| Page-rule CPU on a templated site (25 template-scoped rules) | 13% less | |
| Report assembly at 1,000 pages, peak memory | about 150 MB less | |
| Hosted publish merge carrying 60,000 prior findings | 330 MB | under 100 MB |
| Hosted re-audit of an unchanged 150-page site | 115 pages rendered | 17 rendered, report byte-identical |
| Project database after six audits of a 40-page site | 14.7 MB | 11.9 MB |
Added
-
squirrel self diskshows where~/.squirrelgoes, and--prunegets 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 disklists per-project and total usage.squirrel self disk --prune --keep Nretires the audits beyond the newest N, prints the plan, asks, and then rebuilds the database so the space returns to the filesystem.--keepis required: a retired audit can no longer be rendered, andreport --list,--diffand--regression-sincereach 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 itsETags. A retired audit stays listed, marked with the date its data was reclaimed, andreport,--diff,--regression-sinceandanalyzerefuse it instead of rendering an empty report. -
squirrel keystakes--org, andauth whoamisays which org you are. On an account with more than one organization,keys createminted 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, andkeys listandkeys revokeare scoped the same way.auth statusandauth whoamiprint the active organization. -
The audit says when
--max-pageswas 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=0turns it off. -
The crawl says when its base is the wrong host of a pair. When the seed redirects between the apex and
wwwand 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.dbgrew 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 with0orfalse. A retired audit stays listed and says when its data went; it can no longer be opened, diffed, or used as a--regression-sincebaseline, so raise the window if you keep an old audit as a reference. A run that endsfailedorblockedneither 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 --prunenow 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-pagesand[crawler] max_pageswas 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 withincludepatterns. 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_BYTESsets that budget (48 MB of raw HTML by default) andSQUIRREL_STREAM_BATCH_PAGESpins 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.
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-textflags lorem ipsum, unrendered template tags such as{{ user.name }},[object Object],undefinedin a table cell andTODOmarkers in visible copy.content/unrendered-markupcatches 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-leakagefindslocalhost, private IP ranges,staging.anddev.hosts, preview deploys (vercel.app,netlify.app,pages.dev, ngrok) and plainhttp://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
curlininstall.shnow 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
squirrelbinary 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.
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-Afternow pauses every worker aimed at that host, drops it to one request in flight, and waits out the backoff (honouringRetry-Afterwhen the server sends one) instead of retrying three times in a second while the other workers keep firing. A new[crawler] max_backoff_mssetting (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 arateLimitedcount and host list, and a crawl that lost pages this way is markedpartialwith 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-compressionrule 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 loginnow 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
sha256key 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_statsnow defaults tofalseand the section is no longer rendered or charged for. Reports that already captured it keep their section. It may come back.
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, orALTin 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=""andaria-hiddenis 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,sampandkbd, 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 | shinstall 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.
Release Notes
Two rules about the difference between a page that is linked and a page that is supported. A site's navigation links every page from every other page, which makes the whole site look evenly connected when most of it is not: a page whose only inbound links are the ones repeated in the header and footer carries no signal about what it is for or why it matters. The same template logic cuts the other way for the site's own assets, where one shared layout should give every page the same favicon and share image, and a page that disagrees is usually running an older template nobody remembers owning.
The engine is now at 278 rules across 21 categories.
Added
-
links/no-contextual-inboundreports pages whose only internal inbound links come from sitewide chrome: the nav, header, footer or sidebar. Those links help discovery, but they are identical on every page, so they say nothing about the page they point at. The fix is a link from the body copy of a related article, hub or category page with anchor text that describes the destination, which is what passes topical relevance. The minimum count is configurable for sites that treat a single contextual link as enough. -
social/asset-divergencereports pages whose favicon, theme-color or fallback share image differs from the rest of the site. These come from one layout, so every page should carry the same three, and the ones that do not are a section that missed a redesign or a second layout still in service. The visible cost is a tab icon that changes as visitors move around and link previews that do not match. Per-page share images on articles and products are correct and are not reported: the comparison is against the fallback the rest of the site shares.
Changed
- Custom request headers are available on every plan. Auditing a staging site behind an auth header is how you check work before it ships, and it was gated behind Pro. It is not any more.
Fixed
-
squirrel creditslinked to a page that did not exist. The top-up link it printed had never resolved to anything. It now lands on the billing page for your account, and older versions of the CLI that still print the old address are redirected there too. -
Running out of credits during an audit now says what to do about it. The CLI printed a bare warning with no price and nowhere to go. It now reports the balance, what the audit needed, and where to top up or upgrade.
-
Reinstalling over a stale
squirrelsymlink no longer fails. If~/.local/bin/squirrelpointed at a release that had been cleaned up, the installer refused to replace it. It now does. The install script also no longer dies part way through on systems whereshis dash, which is the default on Debian and Ubuntu.
Release Notes
A reliability fix for long audits. An audit is not just its crawl: once the last page is fetched there is still link checking, rule evaluation, scoring, rendering and publishing to do, and on a large site that second half can take longer than the first. The CLI reported its progress only while pages were being crawled, so for the whole of that second half it went quiet, and a run that was working normally became indistinguishable from one that had died. Past a point the server concluded it had died and marked it failed, discarding a completed crawl and delivering no report.
The bigger the site, the more likely this was, which is the wrong way round.
Fixed
-
A long audit is no longer given up on while it is still running. The CLI now reports that it is alive for the entire run rather than only during the crawl, so the time spent on rules, scoring and rendering no longer counts against it. Audits of large sites that previously failed near the end, after the crawl had already finished, should now complete and publish. If one of yours did this, re-running it on this version is worth a try.
-
Interrupting a crawl now tells you how to resume it. Crawls have always been saved as they go, and an interrupted one can be picked up where it stopped with
--resumeinstead of started again. Nothing said so, so a cancelled crawl of a few hundred pages looked like lost work. Cancelling now reports how many pages are saved and prints the exact command to continue.
Release Notes
Three rules about the gap between what your markup claims and what your pages
actually show. Structured data is a promise made to a machine that never reads
the page, so nothing in a per-page validity check can tell you the promise is
false: a rating block is well-formed whether or not anyone can see a rating, and
a lastmod is a valid date whether or not anything changed that day. These
rules ask the second question.
The engine is now at 275 rules across 21 categories.
Added
-
schema/rating-scopereportsAggregateRatingmarkup that is not about the page it sits on.schema/reviewvalidates the shape of a rating block, so one sitewide rating emitted by a template passes on every page of a site, including the privacy policy. Two checks: whether a rating is visible anywhere in the page's own text, and whether the rated entity is the subject of a page where a rating could apply at all. A third-party review widget clears the first, since its badge is injected client-side and a raw crawl cannot see it. The finding names the page type and the rated entity, and frames the risk as a structured-data manual action rather than a validation error, because that is what it is. -
crawl/sitemap-lastmod-driftreports sitemaplastmodvalues that disagree with the page's own date. Two directions, because they come from different mistakes: alastmodolder than the page'sdateModifiedusually means a stale sitemap or an inverted published/updated precedence, and one newer by more than a month usually means the field is stamped when the site builds. Each finding reports both dates and the gap in days. Pages carrying no date of their own are skipped rather than guessed at. -
crawl/sitemap-lastmod-churnreportslastmodvalues that have collapsed onto one or two days across the whole sitemap, which means they are stamped at build time and carry no freshness signal at all. Google News sitemaps are excluded: they hold about 48 hours of articles by design, so their dates are supposed to cluster.
Fixed
- Anchor text is judged per destination, not per link. A card that links to the same page twice, once from its image and once from its headline, was reported as having empty anchor text for the image link. Only one link in such a group needs to describe the destination, and screen readers announce the group together, so the checks now evaluate links to a target as a unit.
Release Notes
Five new rules that grade a page against the rest of your own site instead of
against a fixed threshold. Every audit rule until now asked the same question of
every site on the internet: is this title too long, is this page too thin. That
works for the rules where one right answer exists, and it is useless for the
much larger class of problem where the only evidence something is wrong is that
one page disagrees with the other nine hundred. A 200 word page is thin on a
documentation site and normal on a news site. A product page missing Product
markup only matters because its siblings have it.
These rules learn the norm from your crawl and report the deviants, which means they need a crawl big enough to have a norm. All five stay silent below 10 crawled pages, and stay silent when no clear majority exists, reporting a skipped check that says which condition was not met rather than guessing. A site that is legitimately heterogeneous should see nothing from them, and that is the intended result, not a failure.
The engine is now at 272 rules across 21 categories.
Added
-
content/thin-vs-site-normreports pages far shorter than comparable pages on the same site. Pages are grouped by section first, so a terse tag listing is not measured against your long-form posts, and the outlier test uses a robust spread rather than a mean, so a handful of very long pages cannot drag the threshold up and hide the thin ones. -
content/title-pattern-outlierlearns your title template and reports the titles that break it. Most sites settle on one shape,Page | BrandorBrand: Page, usually because a template emits it. The pages that do not match are usually the ones a human wrote by hand or a migration missed: brand missing entirely, brand on the wrong end, a different separator. -
schema/coverage-outlierreports pages missing the structured-data markup their siblings have. If 90% of your product pages carryProductand a dozen do not, those dozen lose rich results, and nothing in a per-page check can see it because each page is individually valid. -
url/slug-conventionreports URLs that break your site's own conventions: case, word separator, trailing slash, file extension. Mixed conventions usually mean two generations of routing coexisting, which is where duplicate content comes from. -
core/canonical-form-driftreports canonical URLs that disagree in form across the site, for example some absolute and some relative, or some with thewwwhost and some without. Each one is individually valid, so per-page validation passes while search engines see an inconsistent story about which URL is the real one.
Fixed
-
A site behind a WAF challenge is no longer reported as unreachable. Adding a domain probed it first and accepted only a normal response. A site sitting behind Vercel's Attack Challenge Mode, Cloudflare's, or any similar protection answers that probe with a challenge rather than the page, and the domain was refused as if the site were down. The probe now treats any HTTP response as proof the site is there, because it is.
-
A crawl that lost its handshake no longer disappears. If the network dropped the call that registers a run, the CLI retried and the server treated the retry as a second, unrelated run. The original became an orphan: work that had been done, could not be found, and showed up later as a failed audit. The registration now carries an idempotency key, so a retry resolves to the same run, and an orphaned run is adopted when its report is published.
-
Long crawls are no longer killed while they are working. The job that cleans up abandoned runs measured age alone, so a genuinely slow crawl of a large site could be marked failed while it was still making progress. It now looks at progress before deciding a run is dead, and audits that were reaped and then delivered anyway are repaid.
-
Renders you did not get are no longer charged. A page render that failed was still billed, and a run could spend past the estimate it quoted you when it started. Failed renders are refunded and spend is capped at the run's own quote.
-
Cloud audits run every rule you are paying for. Audits started from a website or from the GitHub integration were dispatched without the flag that enables the paid services the rules depend on, so eight rules quietly skipped while the audit billed in full. Both paths now dispatch with those services enabled and a bounded credit cap.
-
A failed audit refunds the organization that was actually charged. When a run failed after its base charge, the refund was aimed at the organization named in the request rather than the one billed. If you had not named one explicitly, those two were different and nothing was refunded at all.
-
A missing
robots.txtis reported as an error, not a warning. Norobots.txtmeans no place to declare a sitemap and no control over crawling. That is not a nice-to-have. -
Report issues are ordered by severity across the whole report. Sorting happened inside each category, so a critical issue in the last category sat below a warning in the first, and the top of the report was not the worst of the findings.
-
Windows installs no longer fail with a permission error. The installer linked the binary with a symlink, which needs Developer Mode or an elevated shell on Windows, and failed with
EPERMotherwise. It now falls back to a copy. A failingsquirrel self installalso prints what actually went wrong instead of a bare exit code.
Release Notes
A crawl-correctness release. squirrelscan was requesting URLs your site never links and then reporting the redirects it had caused. On any site whose URLs end in a slash, which is the default on WordPress, Hugo and Jekyll, that invented findings, and on some hosts it also spent part of the page budget on redirect pages instead of your content.
Fixed
-
The crawler no longer asks for a URL your site never linked. URL normalization dropped the trailing slash before the request went out, so a site whose every internal link ends in
/was asked for/aboutwhen it only publishes/about/. Two things followed from that, and both are fixed.First, the origin answered with a redirect, and
links/redirect-chainsandcrawl/canonical-chainreported it: a redirect that existed only because we asked for it. Those findings were wrong rather than merely noisy, and they fanned out, because the check for links pointing at redirecting URLs then blamed nearly every page that linked the correct form. If your report listed pages redirecting to themselves with a slash added, that section should now be empty. Genuine redirects are unaffected: a page you really do link at a URL that really does redirect is still reported.Second, and only on some hosts: a few sites answer the no-slash URL with a
200and a small JavaScript redirect page rather than an HTTP redirect. There is no redirect for the crawler to follow, so that page was stored and graded as though it were your article, and title, charset and word count were judged against a few hundred bytes of redirect script. In one 15 page crawl of a blog hosted this way, 12 of the 15 stored pages were redirect pages rather than posts. Sites that answer with an ordinary HTTP redirect were never affected this way, because the crawler followed it and stored the real page. If your site is in the first group, expect finding counts and scores to move in both directions: some checks stop failing because they had been grading a redirect page, and others start reporting because your content is finally being read. -
A redirect chain no longer shows a status the hop never returned. When a page is rendered in the cloud, the render service reports the page it landed on, never the statuses of the redirects that got it there. Those unseen statuses were filled in with the landing page's own status, producing chains that read
(200) → (200). A first hop that returned 200 did not redirect at all. Hops squirrelscan did not observe are now recorded as unknown and shown without a status, and no chain can claim a hop redirected while also reporting that it returned 200. -
squirrel auth statusexplains an organization API key instead of blaming it. Ansq_organization API key works for audits, publishing and credits, but the identity lookup behindsquirrel auth statusandsquirrel auth whoamiaccepts only the login token thatsquirrel auth loginissues. That rejection was reported as the key being invalid, revoked, expired or from the wrong environment, none of which was usually true. It now says what actually happened, and tells you to unsetSQUIRRELSCAN_API_KEYafter logging in, since an environment key takes precedence over a session for every cloud call.
Changed
- A site that links both
/aboutand/about/now has both crawled. They are different URLs, and only a request can tell you which one redirects, so collapsing them into one meant the answer depended on which form the crawler happened to see first. Sites that link a single consistent form, which is nearly all of them, crawl exactly as before.
Release Notes
Three new rules, a rebuilt local-SEO check, and a crawl-correctness fix for seeds that redirect off your site. The rule set is now 267.
Added
content/hidden-textflags text and links hidden from visitors but left visible to crawlers. Google's spam policies treat this as deceptive, and pages doing it can lose rankings or drop out of search entirely. The rule looks for the techniques that have no innocent explanation: an off-screentext-indent, a zero font size, same-on-same colour, a zero-size clip. It deliberately does not accuse you overdisplay:noneoropacity:0on their own, because that is what every accordion, modal, and scroll-reveal library writes. Content revealed inside a@mediablock, by a transition on a compound selector, or alongside a transform is read as UI rather than concealment. It is a warning at weight 6, and escalates to a failure only when hidden links are involved, which is the stronger spam signal.a11y/autocomplete-tokenschecks that fields collecting a person's own data carry the right autofill token. Name, email, phone, address, and payment fields with the correct WHATWG token fill in one tap, which is the difference between a completed checkout and an abandoned one, and WCAG 2.1 success criterion 1.3.5 requires it. Section and shipping/billing prefixes are understood. A token the browser does not recognise is ignored outright, so a typo likefirstnameis worse than nothing, andautocomplete="off"on personal data does not stop autofill in modern browsers, it only stops the accurate kind.a11y/input-typeschecks that fields use the right input type and keyboard hint.type="email",type="tel",type="url", andtype="number"each summon the right mobile keyboard and bring free browser validation with them. The rule also flagstype="number"on digit strings such as postal codes, phone numbers, and card numbers, where it strips leading zeros and silently discards anything that is not a valid float, andnovalidateon a form that still declaresrequiredorpatternand ships no replacement validation.
Changed
local/nap-consistencynow compares your business details across the whole site. It used to judge one page at a time. It now collects the name, address, and phone from every crawled page and reports when the same value is rendered several different ways, which is the drift that actually costs you local citations. Phone numbers declared intel:links count alongside JSON-LD, so a footer that formats the number differently from your markup is visible. The cross-page half only fires once enough pages declare a signal, and the rule still skips sites with no local-business signal at all.security/form-httpsnow separates a real downgrade from an already-insecure page. Actions are resolved against the page URL, so a relative or protocol-relativeactioninherits the page's scheme instead of being guessed at, andformactionon submit buttons is read too. An HTTPS page posting tohttp://now fails: the padlock tells the user they are safe while the submission travels in the clear. An HTTP page posting tohttp://stays a warning, and a page with nothing to submit reports as info rather than passing silently.
Fixed
- A seed that redirects off-site no longer re-bases the whole audit. If the
URL you passed redirected to another site, that target became the audit's base:
every root probe (robots.txt, llms.txt, sitemap discovery, the
.well-knownand agent-manifest sweep) went to a host you never named, and the target's content was stored and reported under your seed's name. A site redirecting to a CDN or a parked domain produced a report describing the target while claiming to describe the seed. The base is now pinned to the seed. A redirect is adopted only when it stays on the same registrable domain and port, sohttptohttpsupgrades and apex towwwbounces still work; one that leaves is refused, and the refused target is recorded as the report'sfinalUrlso you can still see it. The same rule applies per page: a page whose redirects landed off-site is dropped rather than filed under your audit, unless your own scope config admits it. - Building objects from untrusted keys is hardened. Keys taken from crawled content can no longer reach an object's prototype.
- The install endpoint always serves the current release. Release channel metadata and the install scripts are now published as part of the release itself and verified against the live endpoint before the run is allowed to finish, so a new version cannot be announced while the installer still hands out the previous one.
Release Notes
Follow-ups to the v0.0.81 security release, plus two crawl-correctness fixes. Pages listed in a sitemap with query strings are now crawled at the right URL, and a single network blip no longer takes down a whole audit.
Fixed
- Sitemap URLs with query strings are crawled correctly. XML requires
&to be written&inside a<loc>, and that escape was surviving into the URL the crawler enqueued. A sitemap entry for?a=1&b=2was fetched literally, so those pages 404'd or were audited as the wrong URL, and on a site that paginates or filters through query strings that can be most of the sitemap. All five predefined XML escapes are now decoded, in a urlset and in the child entries of a sitemap index. - A single network blip no longer aborts the audit. The pre-flight reachability probe had no retry, so one transient DNS failure, connection reset, or timeout ended the run before the crawl started. It now makes up to three attempts with a short backoff, inside a 20 second overall budget so a genuinely unreachable site still fails quickly. Only transient failures are retried: a refused connection or an HTTP error is still reported on the first attempt.
- The bounded-read guard now covers every path. v0.0.81 bounded untrusted
response bodies as they stream. On the fallback path taken when a runtime
hands back no readable stream, the limit was still applied only once the body
was in memory. That path now refuses an over-declared
content-lengthbefore reading anything, and truncates by bytes rather than characters, so a multi-byte character cannot split at the boundary. - Builtin crypto imports are explicit. The crawler and the fetchers imported
cryptounqualified. That is an undeclared dependency which can resolve to a deprecated npm stub instead of the runtime builtin, depending on what else is installed. Both now importnode:crypto.
Release Notes
A security release, plus two new content rules. Audits now treat everything a
site tells them to fetch, and everything a site tells them to print, as
untrusted, and squirrel self settings no longer prints your credentials.
Everyone should update. If you audit sites you do not control, or you use
-H/--header to pass authenticated headers, update before your next run.
Added
content/stale-copyrightflags a footer year that has fallen behind. A copyright year stuck in the past is the most common "this site is abandoned" signal a visitor sees, and it costs nothing to fix. It is a warning at low weight, so one templated footer repeated across every page cannot dominate your content score.content/mojibakeflags encoding corruption in visible text. Catches garbled sequences like’andé, and replacement characters, in the text your visitors actually read. These almost always mean a template, export, or CMS migration lost the original encoding, and the same corruption is usually sitting in fields you cannot see, such as meta descriptions and alt text.
Fixed
- Redirects can no longer change protocol. Every fetch derived from page
content, including redirect targets, sitemap and RSL references,
Linkheaders, and client-side meta refresh, is now restricted tohttpandhttps. Previously a site could redirect an audit to a non-http URL and have the result stored in the report. - Custom headers stay on their origin. Values passed with
-H/--headerare now scoped to the origin you sent them to and are dropped when a redirect crosses to another origin, on auxiliary probe requests as well as page fetches. These values are documented as secrets, so this closes a path where a redirect could forward them to a host you did not choose. - A hostile response can no longer exhaust an audit's memory. Every read of
an untrusted response body is now bounded as it streams and cancelled at the
limit: robots.txt, sitemaps,
llms.txt,.well-knowndocuments, RSL, fetched scripts, and page bodies. The previous guards could not do this. Acontent-lengthheader is absent on a chunked response and reports the compressed size on an encoded one, and the size limit was otherwise applied only once the whole body was already in memory. A small compressed file that expands to gigabytes now stops at the limit instead. - A site can no longer repaint your terminal. Page text reaches the console
report and the
textandllmoutputs through many fields, and it could carry terminal control sequences: a site could embed an escape that clears your screen and prints its own "0 issues found" over the real result. Control characters are now stripped where the report is written. The console keeps colour codes, which can only change how text looks and never move the cursor or clear the screen, so the report itself is unchanged. squirrel self settingsredacts credentials. The command printed the stored auth token, and any provider keys held in the plaintext fallback used when the OS keychain is unavailable. Output is now redacted. If you have pasted this output anywhere, rotate the token withsquirrel auth login.- Project names cannot escape their directory. A
[project] nameinsquirrel.tomlis now contained before it is used to build the local database path. - Repo-local settings are limited to safe keys. A
settings.jsoninside a checkout can now only influence the documented writable settings, so cloning an untrusted repository cannot redirect where the CLI installs updates. - Consistent lowercase branding. The product is "squirrelscan" everywhere, including the text report title and the database lock warning.
Release Notes
The CLI is now open source under MIT, with the public repo set up as the canonical home for the CLI and docs. This release also hardens the public surface and makes sure the telemetry opt-out is honored everywhere.
Added
- MIT licensed CLI.
squirrelscanis now open source under MIT. - Public repo source of truth. CLI builds and docs now come from the public repository.
- Telemetry opt-out respected everywhere.
NO_TELEMETRYis honored by the CLI and install flow.
Fixed
- Public-repo hardening. Release and update paths were tightened to avoid command injection, traversal, unsafe URL handling, and Markdown injection issues.
- Parser and sanitizer performance issues. Several regex-heavy paths were rewritten to avoid ReDoS-style slowdowns.
- Secret and scheme filtering tightened. Unsafe URLs and non-http(s) inputs are now rejected more consistently.