I built a Chrome extension that blurs student names before a teacher shares their screen. Gradebooks, seating charts, LMS pages, the stuff that ends up on a projector or in a recorded meeting with 30 kids' names sitting in the open.
The free version is click-to-blur. You toggle a picker, hover, click an element, it blurs. Click again, it comes back. Simple. The part that took real work was the Pro feature: paste a class roster, and every occurrence of those names gets blurred automatically, everywhere, on every page, even when the page redraws itself.
That's where I got humbled.
My first pass was basically indexOf. Take each roster name, find it in the page text, wrap it. Worked great in a demo. Then I loaded a real gradebook and half the page was frosted for no reason.
The problem was substrings. A roster with "May Chen" on it was matching inside "Maybelle Chenoweth". "Sam" matched "Sample". "Ella" matched a dozen unrelated words. If you have 28 students, the odds that one short name is a substring of some random word on the page are basically 100 percent.
So the matcher needed word boundaries. Not the naive \b either, because \b breaks on accented letters and a lot of my test names had them.
// normalize away accents, then match on real word boundaries
const strip = s => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
function buildMatcher(names) {
const parts = names.map(n =>
strip(n).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
);
// \b is unreliable across scripts, so anchor on non-letters or the string edges
return new RegExp(`(?<![\\p{L}])(${parts.join("|")})(?![\\p{L}])`, "giu");
}
A few things I had to get right:
- Accent-insensitive. "Jose" on the roster should catch "José" on the page, and the reverse. I normalize both sides (NFD, strip combining marks) before comparing, so the diacritics stop mattering.
- Both name orders. School exports love "Chen, May". Teachers type "May Chen". Same student, so the parser produces both forms from one roster line.
- Anchored to boundaries. "May Chen" never fires inside "Maybelle Chenoweth" anymore. That one bug is the whole reason the matcher got rewritten.
- Leave URLs and code alone. Names show up in links and code blocks where a blur just looks broken, so those get skipped.
The other thing I underestimated: the page doesn't hold still.
Google Classroom and PowerSchool are single-page apps. You scroll, they swap in new rows. You click a student, a panel renders. My first version blurred what was on screen at load and then did nothing, so every new row showed names in the clear.
The fix is a MutationObserver watching for added nodes, re-running the matcher on just the new subtree, debounced so a chatty SPA doesn't cook the CPU. It's not glamorous. It works.
One design choice I'm glad I made early: the blur is non-destructive. I never rewrite the page text. It's a CSS filter plus a positioned overlay. That sounds like a detail until you remember teachers are blurring live web apps they're also using. If I edited the DOM text, form submissions and the app's own scripts could break. Filtering the pixels means the app underneath keeps working exactly as before, and nothing ever gets submitted or saved differently because of me.
Privacy was the whole point, so the extension makes zero network requests. No fetch, no XMLHttpRequest, no websocket anywhere in the shipped code. Rosters and settings live in chrome.storage.local and never leave the machine. You can audit that yourself, which was kind of the idea.
That created a fun problem for the paid tier: how do you sell a license without a license server?
What I landed on: the purchase page issues a signed key. The extension has a public key baked in and verifies the license offline against it. No callback, no phoning home, nothing to take down or leak. Lose the key and you revisit your purchase link, it re-issues. The extension never talks to the license service at all, which I like, because the privacy claim holds even for people who paid.
What's still rough:
- Standalone first-or-last-name matching is opt-in and off by default, because "May" on its own is going to hit false positives no matter how careful I am. Full names are safe. Single names are a judgment call I pushed onto the user.
- The e2e suite loads a real Chrome with the extension and drives it. It's slow and occasionally flaky on a cold machine. The unit tests cover the matcher and the parsers, which is where the actual bugs live, so I'm not losing sleep, but I'd love the e2e to be less moody.
Repo's here if you want to poke at it or load it unpacked: https://github.com/TiltedLunar123/rosterblur
It works. Not perfect, but it works, and it's stopped blurring Maybelle.
Top comments (0)