Repository navigation
BlockNSFW 2.0: built for the hard moment
This release brings 2.0.0, 2.0.1 and 2.0.2 to GitHub together: every change since 1.8.0, newest first.
2.0.2 (2026-10-21)
Added
- The 2.0 film. The 2.0 page opens on a one-minute film about the update, and Settings › What's new links straight to it. It plays from YouTube in a window over the page. Nothing loads from YouTube until you press play, closing the window stops it, and "Watch on YouTube" opens it in a tab instead (privacy policy, section 13).
For contributors
release.jsdrives the film's<dialog>. YouTube won't play inside an extension page (error 153: extension pages send no web referrer), so while the dialog is open a session rule (id 20001) setshttps://monolab.dev/as the referrer of that one embed request, in that tab only, and closing the dialog removes it. If the player still reports an error, the dialog swaps to a "Watch on YouTube" link. The poster isui/release-film.jpg; the film's source istrailer/v2-launch/(not shipped).release.jsis in both builds' file lists, and the privacy-policy test now reads it for hosts.
2.0.1 (2026-10-14)
Added
- The founding offer, in Settings. Until November 13, Supporter's Yearly and Lifetime plans are 30% off, and Settings › Supporter opens with it: what it is and until when, above the plans, and each plan it lowers shows the offer's price next to its own, struck through. The sidebar's Supporter entry says it too ("Founding offer · until Nov 13", "last day" on the last day). Settings drops it by itself after its last day. A later offer can also arrive with the small file BlockNSFW already reads about twice a day to check for updates, so it can start and end without a new version.
Changed
- The Supporter checkout says which page led to it: the 2.0 page, the popup, a page of the path, or a place in Settings, so we can see what helps people decide. Only that one word goes to the checkout, with nothing about you or your browsing (privacy policy, section 10).
For contributors
- An offer has two halves, and both have to be set: a Polar discount preset on the checkout links (what buyers pay) and
offerindata/version.json(what Settings says).Supporter.cleanOfferkeeps only the words (name,title,text), aYYYY-MM-DDlast day and prices that lower a plan that exists;activeOffer(now, remote)prefers it over the built-inOFFER. A test fails if the file's offer, or one of its prices, would be dropped. Supporter.checkoutUrl(plan, door)addsutm_source=<door>andutm_medium=extension. Doors are a fixed list (Supporter.DOORS); anything else counts assettings. Pages outside Settings link tooptions.html?from=<door>#supporter, and links inside Settings carrydata-door. A test checks every door a page names.- The update check stores
offerwith its answer. An answer saved before 2.0.1 has none, so the first check after updating asks again.
2.0.0 (2026-10-12)
BlockNSFW 2.0 is built for the hard moment, not only the blocking. The Pact makes every change that loosens protection wait, and lets a friend you trust hold the key. Storm Mode and risk hours raise everything to its strongest when it's hardest. The blocked page shows your own words first. And every page is redrawn in monolab's design system, in light and dark. Supporter, optional, adds a 30-day path, an evening check-in, a letter every month and more for going further; protection stays the same for everyone.
A PIN you set yourself stops nobody, because you know it. A wait longer than the urge works even when you know every PIN.
Added
-
The Pact (Settings, Security). Choose a wait: 15 minutes, 1 hour, 24 hours or 3 days. While a pact is active, every change that loosens protection waits it out before it happens. That covers turning protection off, whitelisting a site, switching off a layer, removing blocked words or a subscribed list, trusting an image site, weakening the access code, clearing the PIN, using your own DNS resolver, resetting settings and clearing the activity log. Making protection stronger never waits. A waiting change can be cancelled at any time, from Settings or the popup. The sensitivity dials still apply at once, so a mistaken block can always be fixed. Lengthening the wait applies at once; shortening it, removing the witness or ending the pact waits too.
-
A witness. Someone you trust scans a QR code into the authenticator app they already use (Google Authenticator, Aegis, 1Password). When you want a change without the wait, you ask them for the six-digit code it shows. They never see your browsing, and there is no server: the code is worked out from the key and the clock, on your device and in their app separately (standard TOTP, RFC 6238). They also keep eight one-time recovery codes. Each code works once; five wrong ones lock entry for five minutes, doubling to an hour. Their code also works wherever the PIN is asked for, since the two are easy to mix up, and under a pact it lets the change through at once.
-
A PIN only your witness holds. When pairing, you can let BlockNSFW make up a PIN shown only to the witness. Clearing it yourself waits like any other loosening change.
-
Honest time. The wait is measured against the server clock from the
Dateheader of the version file the extension already reads, so moving the computer's clock skips nothing, and a night with the laptop shut still counts. Offline, only time the browser was seen running counts. -
Storm Mode (Settings, Security, and the popup). For 1, 4 or 12 hours, every layer goes to its strongest: strict image and text filtering, the AI models on every site, SafeSearch, Reels blocking, and image search results blurred. It can be made longer but not shorter, and nothing that loosens protection can be changed until it ends, not even with a witness code. When it ends, each setting goes back to what it was, and protection stays on. The end is checked against the server clock.
-
Risk hours. Name the hours you know are hardest (11 PM to 2 AM by default). Every day during them, strictness goes up, image searches are blurred, and loosening changes wait until they end. Turning them off or making them shorter counts as loosening protection.
-
Your own words (Settings, Customization). Written in a calm moment: your plan for when it's hard, a note to yourself, and someone to reach. A held page shows them first, with their number as a call link where calls are possible, and offers to wait it out with you for 10 or 20 minutes. "I'm OK now" counts as a moment kept. Nothing leaves the device.
-
After a slip (the popup's "I slipped" link). A short, plain page: what was going on, roughly when, and nothing else to fill in. It's recorded as one day, never as a reset, and it offers what would help next time: risk hours around that hour, a longer pact (or a first one), a call to the person you named, or Storm Mode for four hours.
-
Gateways (Settings, Protection). Most slips begin somewhere ordinary: Instagram Explore, a subreddit, image search, late at night. Name those sites (Instagram Explore, Reddit, image search, X search and Explore, TikTok and YouTube Shorts are offered, all off until chosen, or add your own site or part of one), and opening one shows a pause first: "Is this where it starts?", ten seconds drawn as one line, your own words, and a choice of Not tonight or Go on. Not tonight is met with "You turned back at the door", the count so far and a line drawn for each recent time you did, with a ten-minute wait if the pull hasn't gone. Go on opens the site for fifteen minutes. During risk hours and Storm Mode a gateway is closed outright, with someone to call and a ten-minute wait instead. It catches apps that move into the gateway without loading a page, as Instagram does. Turning one off, removing a site or shortening the pause waits under a pact like any loosening change. The slip page asks where it started and offers to make that a gateway, and Statistics counts the times you stopped at the door.
-
Your week. Every Sunday from 6 pm, a short page about the week just gone, Monday to Sunday: each time you stopped at a gateway's door, waited it out or slipped, drawn day by day as lines that come to the door and turn back, with a brass square for each day kept. Under it, three numbers beside the week before, one thing to try taken from the week itself (usually starting risk hours before the hour it tends to get hard, and only ever widening them), and a plain closing line. The popup marks it New until it's opened, Statistics links to it, and earlier weeks are a link away. Times of stops and waited-out moments are now kept for five weeks so it can be drawn. Nothing leaves the device.
-
The 2.0 page. Updating from 1.x opens one page, once: "Built for the hard moment", the six things that are new and where each one lives in Settings. Fresh installs get onboarding instead, and later 2.x updates stay quiet. Settings › What's new links back to it.
-
Days kept (Statistics). Days in the last 30 with protection on all day and no slip, shown beside the current run, the longest run and the moments waited out. A slip costs a day, not the count.
-
Supporter (Settings › Supporter), optional, for this browser extension. Protection is the same for everyone: paying never makes blocking stronger or weaker, and nothing that's free today will ever move behind it. Support pays for what comes next, and adds ten things for going further:
- The path. Thirty short pages, one a day, each with one small thing to do: a step to take, a line to write, or a page to open. Four weeks: seeing it, changing the setting, the inside work, and the life after. A new page opens each morning (from 4 am), and a missed day waits for you instead of being lost. The lines you write are kept as your own book, shown on day 30, and the path can be walked again without losing them. Days 1 to 7 are free for everyone.
- The evening check-in (the popup's "How was today?"). Five taps, about ten seconds: last night's sleep, stress, time alone, mood, and whether you moved your body.
- Your good days. Read beside the hard moments the extension already records (a stop at a gateway's door, a moment waited out, a slip), the check-ins show what your calm days have in common, in plain sentences, with one thing to try. It needs 14 evenings, at least three hard and five calm, and says how many more until then. These are patterns in one person's days, never causes, and the page says so.
- When it's hardest. Every hard moment of the last year laid out as seven weekdays by 24 hours, darker where more of them came: the two hours it tends to get hard, ringed in brass, the weekday that stands out, and risk hours to match in one tap (only ever widening the ones set now).
- Your month and your year. A letter on the morning after each month ends, like Your week: how many days were kept, the times you turned back, the moments waited out, set on the month's calendar beside the month before, with one thing to try. The popup marks it New until it's read. Your year puts every day of a year on one calendar, a month to a row, with the longest run.
- A photo for the hard moment (Settings › Your own words). Someone or something worth staying for, shown on the blocked page above your words. It is shrunk on the device and never leaves it; it stays after a plan ends, and removing it never needs Supporter.
- A message in your own voice (Settings › Your own words). Up to a minute, recorded on a good day, to the you who's struggling, or a sound or song of up to ten minutes and 15 MB chosen from your computer (MP3, M4A, Ogg, WebM or WAV), kept as a file in the extension's own storage. The blocked page and the gateway pause offer "Hear yourself"; with "Play it by itself when a page is blocked" on, the blocked page starts it when it opens, and if the browser holds the sound back it says so and one tap plays it. The microphone is open only while recording; the sound stays on the device, and after a plan ends, like the photo.
- Your progress on every computer (Settings › About › Your data), off until switched on. Days kept, the path, check-ins and your words follow you through the browser's own sync, wherever you're signed in to Chrome or Firefox. Each computer's history stays its own and is added up when shown: a day counts as kept only if it was kept on every computer. Protection settings, the PIN, the Pact and your Supporter code never sync, so a change on one computer can't loosen another. It runs when the popup or Settings opens, never in the background.
- Supporter looks (Settings › Appearance). True black for dark, and an accent (Pine, Ink, Plum or Graphite) on every BlockNSFW page. Every accent keeps text at 4.5:1 or better, light and dark. Looks go back to plain when a plan ends.
- Four more blocked-page designs: Calm, Verse, Motivation and Play (below). Classic stays free, and anyone can preview the others. A plan that ends sets the blocked page back to Classic.
Pay monthly, yearly or once for life; yearly is chosen first. Payment goes through Polar, whose receipt links to your purchases, where the code is shown; paste it into Settings › Supporter. The extension checks the code with Polar when you unlock it, then about once a day when you open the extension, sending only the code and our store's ID, without cookies. Offline, the last check stands for two weeks; after that the extras pause until a check gets through, and nothing is lost. A plan works on up to three of your browsers at once (Polar's activation limit), and Settings › Supporter can remove it from one so it can move to another. A cancelled plan runs to the end of the time paid for, then the extras close; your check-ins and your book stay. Settings shows what's free and what Supporter adds side by side, links to manage or cancel a plan, and refunds a purchase made by mistake. The sidebar's Supporter row says thank you once you are one. BlockNSFW for Android has its own Supporter, in the app. Everything the extras write stays on the device.
-
Back up, and restore (Settings › About › Your data), for everyone. One file holds your settings and lists, this computer's history and any others it knows, the path, check-ins, your words, photo and voice, and your run. It never holds the PIN, the Pact or its witness, an access code, your Supporter code or which sites were blocked. Restoring adds to what's here and never takes away: history from another computer is kept beside this one's, the path and check-ins are merged, and the photo, voice and words come back only where there are none. Settings and lists are replaced only after your PIN when one is set, and never while the Pact is on. On a new computer, your current run comes along.
-
A choice of design for the blocked page. Settings > Blocked Page now offers four designs beside Classic, each with a thumbnail and a Preview button. Each one is built around getting through the moment rather than only reporting the block:
- Calm: an ensō, the Zen circle, drawn one breath at a time. Hold to breathe in and the brush starts the circle; let go to breathe out and it closes. A red seal signs the page after the fifth breath.
- Verse: a King James verse under a cross of light cut through a concrete wall. The cross is placed from where the words fall, so it never crosses the text, from a 320px phone to a 4K screen.
- Motivation: "don't break the chain". A wall calendar of the last five weeks with a red X on every day protection stayed on, and today's square left open to mark. The next milestone (7, 30, 90 days) is counted down.
- Play: a six-by-six board of channel tiles to turn until the ink runs from the spring to the sea. Every river is solvable and never dealt solved. No clock, no score, nothing saved.
Classic is free; the other four come with Supporter in the store versions, and every one can be previewed. The choice is presentation only, so it is not behind the PIN, and the blocked page never asks about Supporter: it shows the design that's saved. A custom blocked-page URL or your own HTML still takes over from any design. Every design fits from a small phone to a 4K screen, and follows light and dark.
Changed
-
The PIN is stored as a salted PBKDF2-SHA256 hash, not as plain text. A PIN saved by an older version keeps working and is re-saved as a hash the next time it is entered. Five wrong tries now lock PIN entry for a minute, doubling to an hour, across Settings and the popup.
-
With a pact, the wait replaces the commitment sentence and the access code. Without one, both work exactly as before.
-
One look on every surface. The popup, the blocked page and its four designs, Settings, setup, Statistics, the audit log, Community stories, the changelog and the notices placed on web pages now share one set of tokens (
ui/tokens.css) and one set of components (ui/components.css): square switches that say "on" and "off", hairline rows instead of cards, buttons that say exactly what they do, and no emoji, gradients, shadows or pills. -
Light and dark, your choice. Every page used to be dark only. Settings, Customization, Appearance now has a theme: Light (the default) and Dark keep every BlockNSFW page that way, and System follows your computer, along with what it shows on websites. The choice reaches pages that are already open, and a page opens in it with no flash of the other. The light theme is monolab's own; the dark theme is a warm near-black, checked for contrast pair by pair.
tests/ui-tokens.test.jsholds every text pair at 4.5:1 and every control edge and focus ring at 3:1, in both themes. -
Pages have grounds again, not one flat colour. Every page carries monolab's faint paper texture. The popup opens on a deep pine band that says "Protected" and holds the protection switch; when protection is off the band turns plain and says "Protection is off", with when it comes back on after a pause. Statistics sets each section on its own band, its records in two columns, and draws the last seven days as bars instead of a list.
-
The Warden banner at the top of Settings is a pine band. It is the one deep ground in Settings, with a serif headline and the cool-down dial in brass, and it now says what Warden adds beside a pact: the wait across every browser and app on the PC, not only this browser. It folds to one pine line.
-
The type is bundled. Newsreader (headlines), Instrument Sans (text) and JetBrains Mono (figures and labels) ship in
fonts/, so no page loads anything from the network to draw itself. The Newsreader file is the smaller Latin build monolab uses (58 KB, from 132 KB) and now has a true italic. -
A new icon. A pine shield with the eye struck through in brass, on the toolbar and on the stores. The 16px toolbar icon is drawn to the pixel; 32px and up share one drawing, and the manifests list 32 and 64 as well. The count of blocks on the toolbar icon is pine instead of indigo.
store-assets/holds the new store images and the scripts that make them. -
Hard to remove, with Extension Guard (Settings › Security). A group says whether this browser can remove or turn off BlockNSFW, read from
management.getSelf()(an install type ofadminmeans a policy put it there), and links Extension Guard, the free Windows app that force-installs it with a password to uninstall. Onboarding's step 4 links the download instead of asking people to write in for a beta, and both say plainly that the installer isn't signed yet. -
Problems and ideas go to support@monolab.dev. Settings › About shows the address with a Copy button, in place of the links to GitHub. Sites that should or shouldn't be blocked are still reported from Community.
-
The blocked page says why, first: "Held at the second door." Classic draws the six layers between you and an adult page (your list, the built-in list, DNS, the address and search, the page's text, the on-device AI) as six doors, and shows which one held this page. Under the headline, one plain sentence says what that means ("This site is on BlockNSFW's list of adult sites. It never loaded."). Your own words, the wait timer and Go back sit below; the address stays hidden. The reason used to sit in a fold-out at the bottom. Calm, Verse, Motivation and Play keep everything they do, drawn in ink and pine instead of vermilion, without the paper texture, and in light and dark, with the same plainer reasons.
-
The popup is 360px wide. It opens on the protection switch, shows the counts as figures, and says beside each guarded control what it will ask for ("Asks for your PIN.").
-
Settings shows one section at a time. The sidebar opens Welcome, Protection, Customization, Security, Community or About as a page of its own, with its own address (
options.html#security), so Back returns to the section before. Each section opens on its own header: a numbered label, the title, what the section is for, and behind it a hairline drawing of the monolab mark's idea, things arriving at a line and one let through. Each group is two columns: its name and what it's for on the left, its controls on the right. When everything in a section asks for the same thing (under a pact, the same wait), that is said once under the section's title instead of under every switch, and the longest explanations fold to one line until opened. Clear PIN only shows once there is a PIN. The column is 960px, centered beside the sidebar, and its sentences keep to a reading measure; the full-width button still lets it run to the edge. Notices are read out by screen readers, and every confirmation is a dialog that keeps focus, closes with Escape and returns you to where you were. Clearing your uploaded HTML and resetting trusted sites now ask to confirm first, like the other destructive actions. -
Community stories open on a voice from the community. The page starts on the deep band with a few words from one story, set large: its closing sentences, where people tend to say what keeps them going. A different one is picked at random on every visit; Read another steps through the rest, and Read it all opens the whole story. Stories are set in the serif at reading size and signed "— Anonymous", long ones fold after four lines, and Like is now "This helped me". The page ends on an invitation to write the next one.
-
What BlockNSFW draws on web pages lives in a closed shadow root. The held image placeholder, the search-result notice, the summary line and the floating counter can no longer be restyled or read by the page they sit on, follow light and dark live, and no longer load the extension's icon into the page (which also exposed the extension's ID to it).
-
Settings uses the whole window on wide screens. Suggested by a supporter on a wide monitor. The page was one centred 1160px block, which left the sidebar floating between two large empty gutters. The sidebar is now a full-height rail pinned to the left edge, and the content is centred in the rest, capped at 960px so text stays a readable line length.
- A full-width toggle sits beside the name at the top of the sidebar. It lets the content run to the right edge, and is remembered on that device. It only appears where it makes a difference, from 1304px wide.
-
What's New and the Message from the Dev no longer take over the top of Settings. Users said the two cards took up the top of the page, about 690px before the first setting at a typical width; it is now about 400px.
- The Message from the Dev is a short Warden banner, in Warden's own type and colours: "In a weak moment, Warden makes you wait", one sentence, and links to join the beta list and to learn more. A watch-face dial on the right counts down a cool-down, as an illustration of Warden's delay; it stands still for anyone who prefers reduced motion. Folded, its line keeps the countdown.
- What's New lists headlines. Each change is one row; its explanation opens under it with a click. The card is about 270px instead of 475px.
- "Got it" folds What's New to one line until the next version, when it opens in full again by itself. Opening it from its line is for that visit only.
- The banner opens on every visit. Its × folds it to one line for that visit only.
-
The AI Text Blocker has a new model, and text alone can block a page again. Since 1.7.4 a temporary catch in
content.jsdowngraded every text-only block to "allow", because the v3 model could not be trusted on its own. v4 replaces the model and the catch is gone.v3 had three defects, and no threshold could fix any of them:
- Its vocabulary was inverted. It was trained on 217 hand-written adult phrases averaging 1.7 words against benign sentences averaging 8, so generic words absorbed the adult signal: "videos" outweighed "nude", "naked" and "xxx" combined. That is what blocked
m.youtube.com. - Its score grew with page length. It summed weights over the whole page, so every real page scored exactly 0 or 1. The three strictness levels were unreachable and "AI confidence: 100%" was always 100%.
- Ordinary text drowned out explicit text. One explicit sentence inside 700 ordinary words scored 0, so it could only catch pages that were adult from top to bottom, which the blocklist already catches.
v4 is trained on real page text from Common Crawl's public archive (about 2,900 pages from 817 sites, in the shape
content.jsreads a live page), labelled by the blocklist and a curated list weighted toward sites that share vocabulary with adult content but must never be blocked: porn-addiction recovery, sex education, sexual health, lingerie, dating, LGBTQ, art, parenting, video sites. It scores ~200-character windows, and a small second model turns those into one calibrated page probability. The thresholds for Relaxed, Balanced and Strict now live in the model file, chosen on held-out pages for a target false-positive rate.Measured on sites the model never saw (
tools/text_corpus/EVAL.md):- At Balanced, 0 of 297 ordinary pages and 0 of 130 trap pages blocked.
- 63% of adult pages caught at Balanced (76% of English ones). The detector is a second line behind the blocklist, not a replacement for it.
- An explicit passage inside a long ordinary page is found 76% of the time, against 0% for v3.
- Pages that simply talk about videos stay well clear of every block bar (cooking, cat, workout and stock-footage videos, YouTube's own pages). An early v4 blocked those at 0.99, the same failure as v3's YouTube block; see For contributors for how it was fixed.
It stays labelled Beta. It reads English and most European languages well, but Japanese, Chinese and Russian adult pages mostly get past it: the archive held too few of those sites to learn from. Settings now says so.
- Its vocabulary was inverted. It was trained on 217 hand-written adult phrases averaging 1.7 words against benign sentences averaging 8, so generic words absorbed the adult signal: "videos" outweighed "nude", "naked" and "xxx" combined. That is what blocked
-
Settings describes what the text blocker actually does. It no longer says text alone cannot block, and the strictness levels describe blocking rather than "agreeing" with the image filter.
-
The text model file is smaller and loads faster: 223 KB to 157 KB, with weights stored as packed binary instead of a JSON array of pairs.
Fixed
-
Settings' Save could switch protection and smart detection off without any check, because it copied those two switches from the page. It can now only switch them on.
-
The setup wizard could be reopened later to switch layers off or overwrite the PIN without asking for anything. Once setup is done, or a pact exists, it opens Settings instead, and it never replaces an existing PIN.
-
The popup's PIN prompt threw away every second try: a wrong PIN opened a separate prompt whose answer was ignored. The next try now happens in that prompt.
-
Trusting an image site from its own Save button had no check at all. Under a pact it waits like any other loosening change.
-
Pages with relative links logged errors on every scan: the adult-URL check read
/pathlinks as bad URLs. Links are now read against the page's own address. -
The blocked page showed the blocked address. Classic printed the full URL, and the words a page had matched. It now shows the site's ending only (
••••••••.com) and never the words. The audit log shows a site's name instead of its full address, and names the rule that held a page instead of printing the reason text, which could carry the page's title or the words it matched. Its CSV export is unchanged. -
The audit log's "Disable Events" counted every switch, on and off. It now counts the times protection was turned off, beside a new count of the times it was turned on.
-
The popup's switches could not be reached from the keyboard. They were plain
<div>s; they are switches now, with their state announced. -
An access code could show a different character from the one checked. The code was written into the dialog as HTML, so an
&followed by letters could be read as an entity. It is written as text now. -
Statistics wrote site names into the page as HTML. They are set as text.
-
The Settings sidebar never showed which section you were on. Its highlight and smooth scrolling were an inline
<script>inoptions.html, which the extension's content security policy (script-src 'self') refuses to run. In the installed extension the links jumped without scrolling and no section was ever marked; it only worked when the page was opened outside the extension. The code now lives inoptions-layout.js. -
The streak ring on the Statistics page never filled. The same cause as the sidebar above: the code that fills it toward the 30-day goal was an inline
<script>instats.html, so in the installed extension the ring always stayed empty. It now lives instats.js, and the fill animation the ring was styled with plays when the page opens. -
"Unblock this website" on the blocked page whitelisted the extension again, not the site (#44). The same symptom as #26, back in 1.8.0. To keep blocked addresses out of browser history, the blocked page stopped carrying
?url=and holds only a key into session storage, and deletes the record once read. The popup found nothing to unwrap and fell back to the tab's own address, so the entry it saved was the extension's ID and the site stayed blocked. The popup now asks the open blocked page which site it stands for. Only that page answers, and only to the popup. A page that is not a website (the blocked page once reloaded,chrome://pages, other extensions) no longer shows the unblock or block rows at all, instead of offering to act on a meaningless hostname. That also stops those rows vanishing on real sites whose names start with "chrome", "edge" or "moz-extension". -
Large subscribed lists work, starting with OISD's NSFW list. Subscribing to
https://nsfw.oisd.nl/ublacklistfailed with "That file is too large to use as a ruleset". The file is 12.5 MB and 481,222 rules, against caps of 5 MB and 50,000. Raising the caps alone would have changed the error to "No rules found": the parser did not read uBlacklist match patterns (*://*.example.com/*), which is how real uBlacklist lists are written, so "uBlacklist ruleset files work as they are" was true only for lists that avoided uBlacklist's own syntax.Match patterns are now read. Any rule naming a whole site becomes a bare host however it is spelled, including
*.example.com,.example.comandexample.com/*. A whole TLD (*://*.xxx/*) stays a wildcard, and allow rules (@…) and*://*/*are skipped. The caps are now 32 MB and 1,000,000 rules per list, with at most 2,000,000 hosts across all lists. Non-host rules keep a separate cap of 1,000 per list, because they are checked one by one on every page.Holding that many hosts needed a different home:
- Hosts live in IndexedDB, owned by the background. Chrome gives
storage.local10 MB and the bundled blocklist cache already uses about half. Content scripts, which readstorage.localin every frame, now receive only the small pattern list. They ask the background about hosts throughcheck_blocklist_hosts, the same way they already ask about the bundled list, and images get the same question when a subscription has hosts. - Each list is stored as one sorted string and searched in place. For OISD that is about 10 MB and a 19 ms load on each service-worker wake, against about 30 MB and 180 ms as a
Set. - Hosts no longer become regexes. Every subscribed entry used to compile to its own RegExp and was tested one by one on every navigation. Hosts are now a lookup, and only patterns are compiled.
- Public-suffix entries (
github.io,blogspot.com) are dropped from subscribed lists, as they already are from the bundled one. - Lists saved by earlier versions move to the new storage on first start. If saved hosts go missing (evicted, site data cleared), the list is downloaded again.
- Hosts live in IndexedDB, owned by the background. Chrome gives
-
Update Now no longer gets a list's server to answer 503. A list that failed was retried on every background wake, and the service worker wakes many times an hour. The file was also downloaded in full before the size check refused it, so an oversized list was fetched again and again. The button started a new download on every press. Now:
- an oversized file is refused from its
Content-Length, or cut off mid- stream, before it is read in full; - after a failure, automatic retries wait an hour, and a 429 or 503 is left alone for as long as
Retry-Afterasks (ten minutes if it does not say); - an unchanged list is confirmed with
If-None-Match/If-Modified-Sinceand a 304, with no download or re-parse; - presses while a download is running join it, and a press within five minutes of a successful check is answered from disk.
A refresh saving a stale copy of the list could also overwrite a remove or toggle made during the download. All subscription writes now go through one queue.
- an oversized file is refused from its
-
A
*.entry no longer matches text in the path. The background compiled*.xxxwith a.*subdomain prefix that ran past the host, sohttps://en.wikipedia.org/wiki/a.xxxwas blocked at navigation. The same bug meant a*.example.comentry blocked a Google search forsite.example.com. The prefix now stops at the first/, like the one for bare hosts. OISD's list carries four TLD rules of this shape, so subscribing to it would have exposed the bug widely. -
Subscription status says what is still blocking. A failed refresh showed only "Update failed", even though the last good download stays in force. It now also shows when a retry is due and which rules are still active. The Update Now toast said "Subscriptions updated" even when every list failed. It now reports failures, "up to date", and "checked in the last few minutes" separately.
-
Saving moved entries above the heading they were written under. A note travelled with the one entry directly below it, so a heading over several entries held on to only the first. Under
# A,/anana/sorts ahead of/apricot/, the entry the heading was attached to, and landed above the heading. Notes are no longer sorted at all: they stay on the line where they were written, and only the entries between two notes are sorted. Sections keep the order they were written in. A list with no notes sorts exactly as before. Applies to the blocklist, custom blocked words and trusted image domains.
For contributors
shared/supporter.jsholds the plans, the store settings (STORE.mode,'sandbox'or'live') and both kinds of code. Polar license keys are checked at/v1/customer-portal/license-keys/validate, which needs no secret, and asked about again afterRECHECK_MS(24 hours);status(storage, { local: true })answers from storage and never asks. SignedBN1-codes (ECDSA P-256) are checked on the device; the signing key lives outside both repositories.- The Supporter extras are not open source. Days 8 to 30 of the path, the check-in, Your good days, When it's hardest, Your month and Your year, the photo picker, the voice recorder, sync and the looks live in a private repository, checked out at
extras-private/(ignored here), and the build scripts copy them over the empty stand-ins inextras/(extras/README.md).-Ziprefuses to package without them;-OpenSourcebuilds without them on purpose. What they draw on stays open:shared/ledger.jsis the long record (one line of counts per day, rolled up once a day bybackground.js, 400 days kept),ui/scheme.jslays a kept look over the tokens (colour tokens with plain colour values only), andblocked.jsshows a kept photo (Moments.photoSrctakes plain JPEG, PNG or WebP only) and plays a kept voice message (shared/moment-voice.js;Moments.voiceSrctakes plain WebM, Ogg or MP4 audio only).shared/progress.jskeeps each computer's history separate (a snapshot per computer,pblocker_elsewhere) and adds them up only for showing, so a computer's own record is never rewritten (the sound itself is a file in IndexedDB,shared/sound-store.js, with only a short note in storage.local, which holds about 10 MB in all) with another's;shared/backup.jsmakes and restores the backup, andPath.mergesettles two copies of the path the same way on both sides. Everything that protects stays MIT and in this repository.shared/path-days.jskeeps days 1 to 7 and the titles of the rest, andadd()s the rest from the extra. The blocked page loads no extra and never reads the supporter state: all five designs are open source, and only Settings' picker offers four of them to supporters, in builds that carry the extras (an open-source build offers all five).tests/extras-standins.test.jsfails if a stand-in stops being empty or if anything underextras-private/is tracked;tests/supporter.test.jsfails if anything that blocks or shows the blocked page (background, content, blocked.js and its designs, gateway, offscreen, the AI image blocker) reads the supporter state.RELEASE_CHECKLIST.mdlists what to switch for the live store. blocked-themes.jsis the one registry of blocked-page designs, read by bothblocked.htmland the Settings picker, so the two cannot disagree. The verse is set in Newsreader (SIL OFL), shipped infonts/with its licence.tests/page-scripts-ship.test.jsnow also fails if a stylesheet loads a file byurl()that a build leaves out.tools/text_corpus/build_corpus.pybuilds the corpus from Common Crawl (resumable, byte-capped; about 290 MB of downloads). Its output intools/text_corpus/cache/is git-ignored and must stay so: it holds adult text and third-party content.tools/train_text_classifier.pyis rewritten for v4 and writestext-model.json, the golden vectors andtools/text_corpus/EVAL.md.tools/README.mdexplains the whole pipeline.- Pages are split by site, so evaluation only sees sites the model never trained on. The page model is fitted through cross-fitting; thresholds come from validation plus out-of-fold scores, never from test.
- Two training measures fix the confounds that surfaced during development: classes are balanced within each language (the multilingual blocklist against an English-heavy benign list had taught "not English means adult"), and each adult window is also added with its explicit words removed, labelled benign (
tools/text_corpus/explicit_terms.txt), so page furniture like "watch", "videos" and "updated daily" stops counting as adult. That list is used only in training; nothing in it runs in the extension. tools/text_corpus/label_overrides.tsvexcludes blocklisted sites that are not adult from the corpus, each with a reason.- New tests:
tests/ai-text-wiring.test.jsruns the realcontent.jsscan against the shipped model.tests/text-classifier-core.test.jsnow checks JS/Python parity down to the final page probability, and behaviour on text written for the test rather than rows copied from training data. options-layout.jsis new. It loads from the<head>ofoptions.html, so a saved full-width choice applies before the first paint, and it is in both build scripts' file lists.tests/page-scripts-ship.test.jsfails if a page loads a script that a build leaves out.tests/page-scripts-ship.test.jsalso fails if a shipped page carries code the CSP will refuse: an inline<script>, an inlineon…=handler, or ajavascript:link.- What's New items are now written
<li><details class="wn-item"><summary>Headline</summary><p>Text</p></details></li>: the headline is the row, and the text opens under it.tests/options-welcome-cards.test.jsfails on an item written any other way, or when the hand-written "Show N more" no longer counts what it hides. - A folded What's New is kept in
localStorage(pblocker_whats_new_readholds the version), andoptions-layout.jsreads it from<head>so a folded card never shows in full first.
Upgrading
Nothing to do. Your settings carry over. A PIN saved by an older version keeps working and is re-saved as a hash the next time it's entered. Updating from 1.x opens the 2.0 page once. Chrome 101+ and Firefox 113+, as before.
Install
Store packages
| Package | Files | SHA-256 |
|---|---|---|
blocknsfw-chrome.zip (Chrome and Edge) |
115 | 787207416e8acf14917a46fd650211c22054a46960efe5a452f92b51b5ff998c |
blocknsfw-firefox.zip |
113 | 89e97950757ad38bfbe85dad69acdc7472ab52ce16776b2e0558b180943332a8 |
Built 2026-10-21 with the Supporter extras, which are not open source, so the packages aren't attached here.
Full technical details are in CHANGELOG.md.