Skip to content

Releases: codepurse/BlockNSFW

2.0.2 — Built for the hard moment

Choose a tag to compare

@codepurse codepurse released this 24 Oct 01:44

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.js drives 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) sets https://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 is ui/release-film.jpg; the film's source is trailer/v2-launch/ (not shipped).
  • release.js is 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 offer in data/version.json (what Settings says). Supporter.cleanOffer keeps only the words (name, title, text), a YYYY-MM-DD last day and prices that lower a plan that exists; activeOffer(now, remote) prefers it over the built-in OFFER. A test fails if the file's offer, or one of its prices, would be dropped.
  • Supporter.checkoutUrl(plan, door) adds utm_source=<door> and utm_medium=extension. Doors are a fixed list (Supporter.DOORS); anything else counts as settings. Pages outside Settings link to options.html?from=<door>#supporter, and links inside Settings carry data-door. A test checks every door a page names.
  • The update check stores offer with 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 Date header 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 aga...
Read more

1.8.0

Choose a tag to compare

@codepurse codepurse released this 24 Sep 13:28

BlockNSFW 1.8.0: blocking moves into the browser

Until now, the extension stopped a blocked site only after the page had started loading. With 1.8.0, the browser itself refuses the request. This closes two gaps that have been there since the beginning.

🛡️ Blocked sites no longer receive the request

Before, you never saw a blocked site, but the site still saw you. Your address and cookies had already gone out by the time the extension stepped in. The full blocklist of 204,000+ sites is now enforced by the browser's own request engine, so blocked images, videos, frames and scripts are refused before anything is sent.

Visiting a blocked site directly still shows the BlockNSFW blocked page instead of a blank browser error, so you always know why something was blocked.

🧩 Content inside embedded frames is filtered

Filtering used to run only on the main page. Content inside an embedded frame was checked by address and nothing else, which made "iframe proxy" sites an easy way around the extension. Image, media and AI filtering now run inside frames too. Tiny ad and analytics frames are skipped, so ordinary pages stay fast.

🔒 The blocked page no longer leaves the address in your history

The blocked site's address, and the words that triggered the block, used to end up in your browser history and address-bar suggestions. With history sync on, they also reached your Google account. That information is now held in memory only and never written down.

If you have history sync on, entries from before this update may already be in your account. Clearing them is worth doing.

⚡ Also fixed

  • The AI image filter remembers its results again. Its cache had been failing silently on Chrome, so every image was re-analysed on every page. Pages with lots of images should feel noticeably lighter.

📦 Also includes everything from 1.7.7

1.7.7 was never published on its own, so its fixes arrive here:

  • Blogspot blogs and Mexican government sites are no longer blocked by mistake. If you whitelisted one to work around this, you can remove the entry.
  • A blocked-word pattern can no longer freeze a page.
  • Sites can't slip past the keyword filter by having an ordinary word like "safe" or "help" in their address.
  • A website can no longer inject its own content into a custom HTML blocked page.
  • Nothing about your browsing is written into the page's console any more.
  • Blocked videos and embedded content are counted in your stats again.

⬆️ Upgrading

Nothing to do. Your settings carry over.

  • Chrome 101+ is now required. Network-level blocking needs it. Chrome updates itself, so this affects almost nobody. Older browsers stay on 1.7.7. Firefox is unchanged (113+).
  • If a site you allowed still looks partly blocked, reload it once.
  • Blocked-word patterns that nest repeats, like /(a+)+/, no longer match. Rewrite them without the inner +.
  • A site may be newly blocked if its address contains one of the eight over-broad "safe" words removed in 1.7.7. Whitelist it, and please report it.

Install

Full technical details are in CHANGELOG.md.

1.7.6

Choose a tag to compare

@codepurse codepurse released this 10 Sep 09:24
b5d79ed

BlockNSFW 1.7.6

A large release. The headline is that the extension stops slowing your browser
down — that was the most-reported problem and it had four separate causes. Also
here: a choice of DNS resolver instead of one hardcoded provider, a second and
more accurate image-detection model that made the download smaller rather
than bigger, and a run of fixes for people who develop software locally, use X,
or rely on the access code.


Highlights

Your browser stops feeling slow. Several of you reported Firefox feeling
janky, and busier pages lagging the whole machine. It was not one hot loop but
four costs multiplying each other:

  • Blocking an image used to cost about thirteen writes to storage — and on
    Firefox each is a disk transaction against the same disk the page is loading
    from. A page blocking 150 images made roughly 1,950 of them. It now makes
    six, and that number no longer grows with how much a page blocks.
  • Every time the browser woke the extension after an idle pause, it re-saved
    its settings, which told every open tab to re-scan its page from scratch. One
    wake cost a full re-scan in every tab you had open. It now costs nothing.
  • The image scan measured each picture's position while it was hiding others,
    forcing the browser to recalculate the whole page layout once per image. On
    an image grid that meant hundreds of recalculations in a row, on every pass.
  • The page-text scan re-read the entire rendered page — another full layout
    recalculation — as often as ten times a second on any site with a live feed,
    a chat widget or a rotating ad.

Measured on a dynamic-feed benchmark in Firefox: median frame rate up from 27
to 34 fps, average frame time down 22%, worst frame of a run down 41%, with the
same number of elements blocked before and after.

Long sessions stay fast too. Feeds that recycle rows as you scroll — X,
Reddit, Instagram — left every picture the tab had ever shown held in memory
for the life of the tab. That is why things got slower the longer you browsed.

Choose who answers your DNS queries. DNS Protection was hardcoded to
Cloudflare for Families, which made one company both the only option and a
single point of failure — when it was unreachable, the layer quietly stopped
working with nothing in the interface to say so. You can now pick Cloudflare
for Families, AdGuard DNS Family, Mullvad DNS Family or the CleanBrowsing Adult
Filter, and if one does not answer, a second on a different network is asked.
All four are free, need no account, and are queried straight from your own
browser — nothing passes through our servers.

Picking Custom takes any DNS-over-HTTPS address: your own NextDNS config, a
Control D profile, DNS for Family, or a resolver you run yourself. A custom
resolver deliberately has no fallback — you chose who sees your browsing, and
quietly redirecting those queries elsewhere when your resolver hiccuped would
override that choice without telling you.

A second, more accurate image-detection model — and a smaller download.
Settings now offer a detection-model picker: the bundled NSFW.js model (still
the default, still works offline) or Marqo's Vision Transformer. The reason to
want the second one is not a headline accuracy figure; it is that it answers
one question — is this explicit? — instead of five. The old model's "Sexy"
class fires on beaches, fitness photos, fashion and ordinary portraits, which
is why its bar had to sit very high to avoid blurring holiday snaps.

Its weights are not in the extension package — bundling them would have
grown the store download about five-fold for a feature that is opt-in and off
by default. They are fetched once on first use and cached by your browser. Your
images are never uploaded; the traffic goes one way, and the model comes to
you.

Also new

  • DuckDuckGo and Brave now use their dedicated locked SafeSearch endpoints,
    so strict filtering is enforced by the search host itself and its own
    SafeSearch controls are switched off. The non-JavaScript DuckDuckGo layouts
    keep working and still get strict mode.
  • Strict search enforcement now covers image, video and news tabs on Bing,
    Ecosia and Presearch, which previously only had their main web-search route
    protected.
  • Yandex now runs in Family mode, applied before its first search request
    and kept in step across its regional domains, with the weaker options locked
    on its settings page.
  • First-run setup has a fifth step covering blocking beyond this browser.
    It offers the in-extension DNS check as a toggle (off by default, like
    everything that sends anything off-device), then gives you copyable addresses
    to set on your device or router — which covers every app rather than one
    browser, and survives the extension being removed. The step is skippable.
  • Firefox's documented minimum is now 113, the first release with the
    request-header rules SafeSearch enforcement uses.

Fixed

  • The AI image filter flagged explicit images on X without blurring them.
    (#17) The model was
    working — the blocks were counted — but the blur kept coming back off. X
    keeps a pool of image slots and swaps their contents as you scroll, so the
    picture in a slot could change while its check was still running, and the
    previous occupant's verdict was applied to whichever image had arrived. A
    verdict now remembers which picture it was for and is discarded if the slot
    has moved on.
  • None of the buttons on the audit log's pagination worked.
    (#17) Every one was a
    silent no-op, so the log was stuck on its first 20 entries.
  • The popup could unblock a whole site with nothing but the PIN, however
    you had configured the access code.
    (#29) The access code —
    whose entire point is that retyping 32-256 characters outlasts an urge —
    existed only on the Settings page. The popup had its own older gate that
    checked four digits and returned, so unblock this site wrote a whole-site
    exception without ever asking for the code. Both now use the same gate.
  • Changing detection strictness demanded the full access code. Nudging
    image strictness down one step cost the same 32-256 characters as turning
    blocking off entirely. Nobody doing that was trying to switch the AI off —
    they were trying to stop a holiday photo being blurred. Sensitivity dials are
    now their own tier and never ask for the code; your PIN still applies, and
    the master switches are gated exactly as before.
  • Local development servers were blocked, with no way out of it.
    localhost and every other local address was redirected to the blocked page
    on every load, by two separate mechanisms — and the whitelist refused to
    accept localhost or a bare IP address, so there was no spelling of the one
    thing that would have helped. The heuristic scans now skip local and private
    addresses, and the whitelist accepts them. Your own blocklist and title
    patterns still apply there, because those are an instruction rather than a
    guess.
  • The AI text classifier could switch itself on. It is an opt-in beta and
    documented as off by default, but a missing setting read as "enabled" —
    downloading and scoring a model on every page for a result that was then
    discarded anyway.
  • A page that changed only in the middle could escape the text scan. The
    scan recognised pages it had already judged by their first line, last line
    and line count, which a site swapping content under a fixed header and footer
    leaves untouched. It reads the whole text now.
  • Embedded frames were re-checked on every pass, even after being cleared.
    A frame later pointed at a different address is still re-checked.

Changed

  • Whitelisting a whole site is now treated as a critical action. It unlocks
    every page on the domain, which is as total as switching blocking off, so it
    asks for the access code under the default scope — as does importing a
    whitelist file. Whitelisting a single page opens one section and is not
    critical. Both ask only after the entry has been checked and found not to be
    a duplicate, so a typo cannot cost you 256 characters of typing.
  • Chrome no longer loads the image-classifier runtime unless it is used, and
    releases it after five idle minutes or immediately when the AI image blocker
    is switched off.
  • The blocklist is no longer rebuilt every time the background wakes.

Removed

  • The remote announcement banner. It rendered a message fetched from this
    repository, which meant a notice could be broadcast to every install by
    editing one file, with no store release. The banner and its six-hourly fetch
    are gone, so opening Settings makes one fewer network request.
  • About 856 KB of dead code that shipped in every release — a worker and
    the two libraries it imported, left behind by an earlier change and copied
    into the package by the build even though nothing referenced them. The Chrome
    package drops from 4.99 MB to 4.13 MB.

Upgrading

Nothing to do. Settings carry over, and the default detection model is
unchanged — the new one is opt-in under Settings → AI Image Blocker.

Two things worth knowing:

  • If you use DNS Protection, it still defaults to Cloudflare for Families. The
    other three resolvers and the custom option are there when you want them.
  • If you had the AI text classifier appearing to run despite never enabling it,
    that was the bug above; it is genuinely off now unless you turn it on.

The full technical detail for every item is in
CHANGELOG.md.

v1.7.5

Choose a tag to compare

@codepurse codepurse released this 25 Aug 04:54

Firefox: AI image blocking now actually works

If you run BlockNSFW on Firefox and turned on the AI image filter, it has never
done anything
— not since the feature shipped. The classifier was loaded in a way
that only works in Chrome, so on Firefox the model was never there to ask. Nothing
looked broken, because an unavailable model shows images rather than leaving blank
holes in the page — a page with the filter on looked exactly like a page with
nothing to block.

It works now. The filter is still opt-in beta, so if you had given up on it,
switch it back on in Settings → AI Image Blocker.

The same underlying cause was also quietly shrinking what Firefox blocked: three
pieces of shared matching logic never reached the background, so Firefox checked
addresses against a 27-word fallback list instead of the full one, missed look-alike
domains written in other alphabets, and ignored the path on a whitelist entry —
meaning allow this one section of a site allowed nothing at all. Chrome and Edge
were never affected by any of this.

New

  • A count of what was blocked. The toolbar icon carries the number of things
    blocked in the current tab, cleared when you navigate.
  • Or keep the count in the page. A small floating pill in the corner, for
    anyone who has not pinned the toolbar icon. Clicking it lists the sites the
    blocks came from, busiest first.
  • Subscribed lists. Settings → Customization → Subscribed Lists takes the
    address of a ruleset file and blocks everything on it alongside your own rules.
  • Comments in your lists. Lines starting with # or ! are ignored, so you
    can annotate your blocked words and domains.
  • A prompt to pin the extension, since the block count and the quickest route
    to unblocking a site are both hidden while the icon is unpinned.

Changed

  • Blocked search results are removed, not papered over. Each one used to become
    a full-width card with a logo and a BLOCKED badge — bearable at two results in
    ten, a wall once image and video results started being filtered too. Blocked
    results now simply go, and one line above the results accounts for them:
    "3 results blocked by BlockNSFW". Settings → Customization → Blocked Search
    Results brings the old card back for web results if you prefer the evidence.

Fixed

  • DuckDuckGo's Images and Videos tabs were not filtered at all — a blocked word
    stopped the web results, then the same search on those tabs showed everything.
  • A search result that arrived late was never filtered, because it was marked as
    already inspected on the way past.
  • Your own blocked words were ignored in image search below the strictest
    filter level.
  • "Go Back" on the blocked page did nothing — its click was being discarded by
    the extension's own security policy.
  • "Unblock this website" whitelisted the extension instead of the site when used
    from a blocked page. Reported in #26.

Install

Chrome Web Store and Firefox Add-ons builds are rolling out separately — store
review can take a few days. To install this build directly, download the zip for
your browser below.

Full changelog: v1.7.4...v1.7.5

1.7.4 — Security, list improvements, and fewer false blocks

Choose a tag to compare

@codepurse codepurse released this 17 Aug 05:27

This release closes a way around your own PIN, adds a much stronger optional lock, and brings regular expressions to both custom lists. Most of it came from users writing in. Thank you.

Your settings are harder to unlock in a weak moment

Some settings could be changed without your PIN, including deleting your own blocked words, adding a trusted image site, lowering AI strictness, turning off DNS Protection, and pointing the blocked page somewhere else. A user described exactly what that meant in practice: remove a blocked word, view the content, put the word back afterwards, all in seconds.

Every change that weakens protection now asks for your PIN first. Adding words, tightening settings and turning protections on stay instant, as they should. Making it harder to protect yourself would defeat the point.

New: Access Code

An optional second lock on top of your PIN, inspired by LeechBlock and requested by two users.

A random code appears and you retype it by hand. Copy and paste are switched off, and a typo gives you a completely new code, so it cannot be chipped away at. Choose 32, 64, 128 or 256 characters.

It is not about secrecy. The code is displayed in full, right above the box. The protection is the minute of deliberate effort, which is long enough for an urge to pass.

By default it guards only the big moments (turning blocking off, clearing your PIN), so day to day edits stay quick. A switch extends it to every change that weakens protection.

New: patterns in both lists

One line can now cover many spellings instead of a long list of near duplicates. The syntax matches uBlacklist, so lists you already keep will work.

Blocked words

/p[o0]rn/          catches "porn" and "p0rn"
/escort(s|ing)?/   catches escort, escorts, escorting

Blocked sites

/example\.(net|org)/     matches the address
title/Example Domain/    matches the page title

Plain words and wildcards work exactly as before. Patterns are checked when you save, so a mistake tells you what is wrong instead of quietly failing, and a pattern slow enough to freeze pages is refused.

New: import and export

Your blocklist, blocked words and trusted sites now have Import and Export buttons. Export saves a file you can keep as a backup or open in a spreadsheet. Import accepts .csv or .txt and adds to what you already have rather than replacing it, so nothing is lost by accident. Imported entries appear in the box first, so you can look them over before saving.

Lists are also tidied when you save: sorted A to Z and de-duplicated.

Fixed: YouTube and other ordinary sites were blocked

The AI text detector was flagging normal pages, most visibly mobile YouTube at 99% confidence.

The cause was the model itself. Measured word by word, it scored "videos" as a stronger adult signal than "nude" or "naked", and had picked up "youtube" from the fragment "tube" without ever having seen the word. That came from unbalanced training data, and no setting could correct it.

So the text detector no longer blocks a page on its own. It now only blocks when the image scanner has independently flagged something on the same page. This makes it weaker, and it will miss pages it used to catch. That is a deliberate trade while the model cannot be trusted alone, and a retrained model is planned. Your other layers, the blocklist, keywords, address matching and image scanning, are unaffected.

Fixed: blocked sites loaded before being stopped

A long standing bug. Sites on your custom blocklist were only caught after the page had begun loading, never before, because of a mistake in how patterns were compiled. They now block at navigation, as intended.

Also in this release

  • Sites you block are blocked as a source of images too, including in image search results
  • Names like "Jerome" are no longer blocked for containing an adult site name
  • A search engine's own safety filter is no longer mistaken for adult content
  • Pages no longer stall while loading, especially on Firefox
  • BlockNSFW is now on Android

Install

Most people should update through the Chrome Web Store or Firefox Add-ons, which happens automatically.

The packages below are for manual installation and for anyone who wants to inspect what shipped.

Thank you to everyone who wrote in with reports and suggestions. Nearly everything above started as an email from a user.