I’ve shipped a few Chrome extensions before. All of them were client work: someone hands you a spec, you build to the spec, you hand it over, and whether the thing is actually pleasant to use every day becomes somebody else’s problem.
FeedSanitizer was different. I built it because my own feeds had become unusable.
The problem I actually had
Open LinkedIn on any given morning and count what’s genuinely for you. A recruiter’s engagement bait poll. Four “I’m humbled to announce” posts. A promoted post for a course. Two posts from groups I never joined, surfaced because the algorithm decided I might like them. Somewhere in there, maybe two posts I actually wanted to read.
X is the same shape with different noise: crypto giveaways, airdrops, “retweet to win,” an entire sidebar of trending topics I have never once found useful. Facebook shows me posts from pages I don’t follow and can’t seem to stop suggesting them.
The platforms all ship a “not interested” button. I’ve clicked it hundreds of times. It doesn’t do much, and even when it does, you’re training a recommendation model, not setting a rule.
What I wanted was a rule. Crypto: gone. Giveaway: gone. This one specific person who posts eleven times a day: gone. And when something I actually care about shows up, put a box around it so I don’t scroll past it.
So I built that.
The constraint that shaped everything: it stays on the device
Before I wrote a line of code, I wrote a rules file for the project, and the first rule was that no feed data leaves the browser. Ever.
This wasn’t a marketing decision. It was a “would I install this” decision. A feed filter sees everything you read. If I’m going to build something that watches my entire LinkedIn timeline, that thing runs locally or it doesn’t run.
That single constraint made a lot of other decisions easy. The extension asks the browser for exactly one permission: the ability to store your rules locally. It also needs access to the handful of sites it actually works on, and nothing else. No tracking permission, no analytics, no backend server. Your rules live only on your own machine.
There is no server, so there is nothing to breach and nothing for me to be tempted by later.
It also kept the whole thing small. The entire extension is a few thousand lines of straightforward code, no build pipeline, no framework, nothing you’d need a team to maintain. A narrow, well understood scope is worth protecting; I’ve seen what happens when a shortcut goes wrong in a website I once broke with one click.
Architecture: one engine, three adapters
Every platform renders its feed differently, but the question you’re asking about a post is identical everywhere: given this text and this author, do I hide it, highlight it, or leave it alone?
So I split it exactly there. One part of the extension is the decision maker: it takes a post’s text, its author, and which platform it came from, and decides whether to hide it, highlight it, or leave it alone. It has no idea what a LinkedIn post looks like on screen, and it doesn’t need to.
The other part is a small set of platform adapters, one each for LinkedIn, X, and Facebook. Each adapter’s only job is to answer four simple questions for its platform: where are the posts on the page, what’s the text, who wrote it, and what makes this particular post identifiable.
Adding a new platform later would mean writing one new adapter. The decision-making logic never changes. That separation has held up through every feature I’ve added since, and it’s the single design decision I’d repeat on any project like this.
Spam adapts, so matching had to
A while after launch I hit a post that should have matched one of my rules and didn’t. The post said something like: Get Yours Now, but rendered in a strange, heavy bold font.
That isn’t actually bold text. It’s a different set of lookalike characters that render as bold or italic letters but aren’t the plain letters they appear to be. Spammers run their copy through “fancy text” generators to get fake formatting past platforms that don’t allow it, and as a side effect, the same trick slips straight past any ordinary keyword filter. Simply converting everything to lowercase does nothing to catch it.
The fix turned out to be a single, well established text-cleanup step: a standard normalization that converts these lookalike characters back to their plain letter equivalents before any rule is checked. Ordinary text passes through completely unaffected. One small addition, and a whole category of spam evasion stopped working.
The heuristics I’m not proud of but which work
Not everything has a clean signal to match on. A few of the module hiders, the toggles that strip promoted posts, unfollowed pages, and un-joined groups, rely on smaller, more fragile cues rather than anything solid. I documented each one honestly rather than pretending it was a clean solution: sometimes the answer is simply that there’s no stable hook, here’s the tradeoff, here’s the note for future me.
I also carved out one deliberate exception: hiding is disabled on Facebook search results, since it would end up hiding the exact page you searched for. Highlighting still runs there, since it doesn’t remove anything.
The feature I deleted
Version 1.0 shipped with a toolbar badge counting filtered posts. It looked like a metric. It was actually meaningless: it summed hidden and highlighted posts into one number, two opposite actions, and with infinite scroll it only ever went up, with no denominator. “You’ve filtered 847 posts” out of what. There’s no answer.
I removed it in 1.1. Building for yourself makes this easy in a way client work never does: nobody had to be convinced, I just noticed I’d never once looked at the number and deleted the feature. Carrying a feature nobody uses is its own quiet form of technical debt, the kind I’ve written about before.
What building for myself changed
The real difference wasn’t technical. It was that I felt every rough edge myself, because I was scrolling my own feed with my own extension running every day. Nobody had to convince me something was worth fixing. I noticed it, and I fixed it.
Client extensions ship and you find out how they went months later, filtered through someone else’s summary. This one, I use every morning. Staying close enough to the code to build something like this is part of why I still think staying technical is worth defending, even from a product seat.
FeedSanitizer lives at feedsanitizer.com, and you can install it free from the Chrome Web Store, for X, LinkedIn, and Facebook. Everything runs on your device. It asks for one permission.
If your feed has stopped being useful, you don’t have to accept that. You can just write a rule.
What would you filter out of your own feed first?
