A personal attention filter for the web. You add names, keywords, or phrases. Matching content disappears from any site you visit, shortly after it appears. Text matching only. Fully local. No backend.
A Manifest V3 Chromium browser extension (Chrome, Brave, Edge, etc.) that runs a single universal content script on every page you load. The script walks text nodes, tests each against your blocklist, and on a match hides the nearest semantic card-like ancestor — <article>, <li>, ARIA role="article" / role="listitem", or a custom element matching *-RENDERER / *-CARD (which catches YouTube tiles, Reddit cards, etc.).
The rules engine is platform-agnostic. There is no site-specific code anywhere in the codebase. Adding a new site is zero work.
See the “Non-goals” section below for the long form.
display: none on the matched card.npm install
npm run build
chrome://extensions (or brave://extensions, edge://extensions).dist/ folder.The extension takes effect immediately on the next page load. Existing tabs need a refresh.
Open the options page. Each rule has:
| Field | What it does |
|---|---|
| Type | creator / keyword / phrase / regex |
| Value | The text to match (or a regex pattern, for regex type) |
| Aliases | Alternative forms — all matched as if they were the value |
| Whole word | \b…\b boundary — defaults to true for creator rules |
| Case sensitive | Defaults to false |
| Scopes | titles / channels / comments / descriptions — currently informational; the universal scanner tests all text against all scopes |
Use Export to download your rules as JSON; Import to round-trip them onto another machine.
Enable Debug in Settings, then on any page press Alt + Shift + D to toggle a small overlay showing scanned, hidden, and last batch time. Useful for verifying the scanner is doing work.
The scanner exposes window.__heDebug on every page:
__heDebug.stats // live counters and state
__heDebug.kill() // disable the scanner, disconnect the observer, remove all hides
__heDebug.unkill() // re-enable
It also self-terminates if any single drain exceeds 1s or cumulative scan time exceeds 60s, surfacing a structured console.warn with the relevant counters. This is a deliberate failure mode — visible flash > blank page.
┌────────────────────────────────────────────┐
│ Engine (src/engine/, src/shared/storage) │
│ Pure logic. Compiles rules to one regex │
│ per scope. No DOM imports. │
└────────────────────────────────────────────┘
▲
│ "does this text match?"
│
┌────────────────────────────────────────────┐
│ Universal scanner │
│ (src/content/universal-scanner.ts) │
│ One content script, all sites. │
│ TreeWalker-free, per-child decomposition, │
│ idle-callback yielding, kill-switches. │
└────────────────────────────────────────────┘
npm install
npm run dev # vite build --watch — rebuilds dist/ on file change
npm run build # one-shot production build
npm run test # vitest run — engine + shared utilities only
npm run typecheck # tsc --noEmit
Reload the extension in chrome://extensions after each build (the reload icon on the card).
src/
background/ # service worker: storage subscriptions, lifecycle
engine/ # pure matcher — NO DOM imports allowed
content/
universal-scanner.ts # the one content script — runs on all sites
debug-overlay.ts # optional stats overlay (Alt+Shift+D)
ui/
options/ # React app: full blocklist management, import/export
shared/ # storage wrapper, shared types
vite.config.ts # manifest is defined inline here
npm run test
The engine has full coverage (rules, scopes, normalization edge cases including ZWJ, diacritics, fullwidth Unicode). The scanner is not unit-tested — it’s DOM-coupled and JSDOM tests of MutationObserver behavior have low ROI. Verification is the kill-switch safety net + the debug overlay + manual smoke testing.
Explicit non-goals — these will not be added without an explicit request and should never be introduced incrementally:
chrome.storage.syncchrome.storage.sync for rules, chrome.storage.local for hit counters@crxjs/vite-pluginMIT — see LICENSE.