Everyone Should Sideload More Browser Extensions
Android users are losing the right to sideload: extensions show why it matters
Consumers get software in two ways: curated marketplaces and DIY installs. Both are legitimate, and each serves a different and irreplaceable role.
Marketplaces
Apple App Store, Google Play, Chrome Web Store, Microsoft Store, Linux Snap Store, Visual Studio Marketplace, Steam.
These are curated stores, operated by the OEM or a trusted third party company, which gather software into a single trusted location. Users browse, search, and install with a single click. The marketplace handles automatic updates, permissions, and payments. The emphasis is on convenience.
DIY
Covers everything outside marketplaces. You might be downloading a prebuilt binary directly from a developer’s website, pulling a package from a GitHub release, installing via a package manager like Homebrew or apt, or even cloning a repository and building from source. Updates are manual, payment is ad hoc, and permissions are a matter of caveat emptor. The emphasis is on flexibility.
The recent news about Google deciding to block sideloaded apps has sharpened the discourse around how we load software onto our devices. (We’ll skip over the debate about how appropriate the term “sideloading” is to describe software installation). The general consensus is that this move is extremely hostile towards users and developers, and I strongly agree with this sentiment.
Let’s explore why this move is hostile with a browser extension analogy.
In the realm of browser extensions, let’s review our install avenues:
Marketplaces: Chrome Web Store, Firefox Add-on Store, Edge Add-on Store, Opera Add-ons Store
DIY: load extension from source
Problem: I have a bad habit of idly browsing social media feeds when I’m supposed to be working, and I suspect I’m not alone. After all, the feeds are designed to be addictive. I don’t want to wholesale block these sites entirely, as I use parts of them for work, I just want to stop myself from winding up on the feed. Of course, these sites try very hard to dump you back on the feed.
Solution: a very simple browser extension that redirects from the feed to your profile.
It intentionally does this in a very dumb way (waiting for the browser to finish redirects, so there is a flash of the feed), but it always works.
I can still use messaging, see my profile, and use the rest of the platform normally.
When I’m inevitably pushed back to the feed, I’m kicked right back to my profile. There’s no opportunity for me to be “sucked in” for 20 minutes
On the rare occasion I do need to see the feed, I can toggle the extension on/off easily.
The NoFeeds extension is less than 100 lines of code and 2 files, scroll down to see the full source.
There’s no need for anyone to install NoFeeds from a Marketplace.
NoFeeds uses dangerous permissions.
tabs
and<all_urls>
give an extension near-total visibility into your browsing. If I install this from a marketplace, I’m implicitly granting these permissions to the developer as long as I have the extension installed.No need for automatic updates. This extension is <100 lines of code, has no dependencies, and uses a handful of Web Extensions APIs. What is there to update?
Keep it dead simple. I don’t need a flashy UI, I don’t need an onboarding flow, I don’t need a homepage or a blog or a settings page or an icon that animates or a modal that asks for reviews or a toggle buried behind three menus or a welcome tour or a privacy policy link that opens in a new tab or a custom font or an update banner or a “rate us” prompt or a cute empty state or a tooltip explaining what a tooltip is. It should work just like an adblocker - install and forget.
Easy customization. Want to add your own social media redirects? Just modify the redirect rules yourself and reload. I don’t need a list of 460 commonly used social media sites that I don’t use.
Zero risk of compromise or tracking. The entire source code is easy to read and understand. I can be 100% sure this extension is self-contained and will stay that way.
Zero technical ability required to install. There’s no build process or compilation. Download the code, enable developer mode in your browser, and load the folder. Anyone proficient with Microsoft Word can do it.
LLMS are great for analysis/modification. Any non-technical user can dump the source into an LLM and ask for modifications, or ask if it’s safe to self-install. I’m comfortable asserting that LLMs are still trustworthy in this respect, and would be able to accurately modify or analyze this tiny program.
Installing self-installed extensions should be way more common
There’s plenty of one-off extensions like this to improve quality-of-browsing-life for desktop browsers. The userScripts
API sort of addresses this, but I continue to find a DIY-installed set-and-forget extension is best and simplest.
It’s easier than ever these days to use a LLM to churn out a small-scale custom browser extension that fixes something we hate about the web. Why not self-install these? LLMs leave a lot to be desired in many areas, but for now I think I’m pretty safe to say that they can be trusted to not shim some spyware into a home-rolled browser extension.
What if the extension DIY install avenue was removed?
Let’s return now to the Android analogy. Suppose that all browsers removed option for DIY install. I’m a layman consumer without a developer account, and I want to use NoFeeds.
Now I must install it from a marketplace.
I can’t turn off silent automatic updates for an extension that needs no updates. What if this extension gets popular and is sold to an untrustworthy developer? I’m getting that developer’s update whether I like it or not.
What if the extension is closed source, and changes in a way I don’t like? The best I can do is bother the developer and hope for the best.
What if I want to use it on a different browser? If it’s only published on one marketplace, I’m out of luck.
This would be completely unacceptable.
This perfectly mirrors the conundrum the Android community is facing. Granted, the mobile device APIs and device surface is far larger, and considerably more money flows through mobile apps than extensions. Proponents might make the argument that there’s security concerns, but I’d argue the data that flows through a desktop web browser is just as sensitive as that on a mobile device.
The freedom to install our own software isn’t a loophole, it’s a cornerstone of personal computing.
NoFeeds source code:
manifest.json
{
“manifest_version”: 3,
“name”: “NoFeeds”,
“version”: “1.0”,
“description”: “Redirects feed pages of common social media platforms to profile pages.”,
“permissions”: [”tabs”, “storage”],
“host_permissions”: [”<all_urls>”],
“background”: {
“service_worker”: “background.js”
},
“action”: {}
}
background.js
const BADGE_COLOR_ON = “#00FF00”; // Green
const BADGE_COLOR_OFF = “#FF0000”; // Red
const ENABLED = “enabled”;
const redirectRules = [
{
feedUrl: “https://www.linkedin.com/feed/”,
profileUrl: “https://linkedin.com/in/”,
},
{
feedUrl: “https://www.facebook.com”,
profileUrl: “https://facebook.com/me”,
},
{
feedUrl: “https://x.com/home”,
profileUrl: “https://x.com/i/profile”,
},
];
async function initializeExtension() {
const { enabled } = await chrome.storage.local.get(ENABLED);
if (enabled === undefined) {
await chrome.storage.local.set({ enabled: true });
}
await updateIcon();
}
async function updateIcon() {
const { enabled = true } = await chrome.storage.local.get(ENABLED);
const badge = enabled ? “ON” : “OFF”;
const color = enabled ? BADGE_COLOR_ON : BADGE_COLOR_OFF;
chrome.action.setBadgeText({ text: badge });
chrome.action.setBadgeBackgroundColor({ color });
}
chrome.runtime.onInstalled.addListener(() => {
initializeExtension();
});
chrome.action.onClicked.addListener(async () => {
const { enabled } = await chrome.storage.local.get(ENABLED);
const newState = !enabled;
await chrome.storage.local.set({ enabled: newState });
await updateIcon();
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status !== “complete” || !tab.url) {
return;
}
const { enabled } = await chrome.storage.local.get(ENABLED);
if (!enabled) {
return;
}
const currentUrl = new URL(tab.url);
for (const { feedUrl, profileUrl } of redirectRules) {
const feedUrlObj = new URL(feedUrl);
if (
currentUrl.hostname === feedUrlObj.hostname &&
currentUrl.pathname === feedUrlObj.pathname
) {
chrome.tabs.update(tabId, { url: profileUrl });
break;
}
}
});
initializeExtension();