Every Chrome extension I install asks for something. Read and change all your data on all websites, usually. So when I built five small developer utilities as extensions, I wanted to know how far I could get with the permissions array empty.
All five shipped with this:
{
"manifest_version": 3,
"name": "Base64 Encoder & Decoder",
"version": "1.0.0",
"description": "...",
"icons": { "16": "...", "48": "...", "128": "..." },
"action": { "default_popup": "popup.html", "default_title": "..." },
"homepage_url": "https://base64.withuse.io"
}
That is the whole manifest. No permissions. No host_permissions. No content_scripts. No background. Not an empty array — the keys are absent.
What you actually give up
Three things, and they are the three that most tutorials reach for reflexively.
No access to the current page. Without activeTab or a host permission you cannot read the selected text, the URL, or anything in the DOM. For a tool that encodes text the user types into your own popup, that turns out not to matter. For "encode whatever I highlighted", it does — that is a real feature you are declining.
No storage. No chrome.storage, so nothing survives closing the popup. No history, no remembered settings. I replaced the usual "last 10 results" list with nothing and have not missed it; the work in these tools is a single transformation, not a session. localStorage is still available inside the popup's own origin if you want per-install state without declaring the permission — worth knowing, though I did not use it.
No clipboard permission. You do not need one. navigator.clipboard.writeText() works from a user gesture inside the popup without clipboardWrite in MV3. This is the one that surprised me; a lot of extension code still declares it.
What you keep
More than I expected. The popup is a normal web page with a normal origin, so Web Crypto, TextEncoder, Intl, and the whole DOM API are there. Every one of these tools is a pure function over text:
- UUID generation —
crypto.getRandomValues, RFC 9562 layout, no network - Base64 —
TextEncoderplusbtoa, with the padding rules handled locally - JSON formatting and duplicate-key detection — a scan over the raw source text
- JWT decoding — three base64url segments, split and decoded in place
- cron parsing — field expansion and arithmetic
None of them has a reason to touch the page you are on, and none has a reason to make a request. I grepped the five source trees for fetch, XMLHttpRequest and any CDN-shaped import: zero hits. The token you paste into the JWT decoder cannot leave your machine, because there is no code in the bundle that could send it.
The dependency decision that follows from this
I did not bundle anything from npm. The UUID logic, the base64 helpers, the cron parser — all hand-written, totalling a few hundred lines across the five.
That is not about bundle size, though the zips do come out around 16 KB each. It is that the source a reviewer reads is the source that ships. A build step that pulls a transitive dependency tree breaks that property, and an extension is exactly the place where it matters: it runs with whatever permissions you declared, in the browser, forever, updated silently.
The cost is real. I reimplemented UUID v7 and got the same-millisecond ordering wrong on the first pass, which a mature library would have had right. I caught it with a test, but only because I went looking. A dependency would have saved me that bug and cost me the review property. For five files of pure logic I took that trade; for anything with a protocol or a parser of real complexity I would not.
What the store does with it
Two observations from actually submitting these.
The review was fast — both published within a day. I have no way to know whether the empty permissions list is why, and I would not claim it. But a reviewer's job is easier when there is nothing to assess.
The thing that actually blocked me was unrelated and badly documented: a new developer account can have only two extensions published at a time. Not two per day — two total, until you request an increase from the dashboard. I had five packaged and found this out by hitting it. The error that surfaced first was a generic file-upload failure whose only useful detail was a line about enabling 2-Step Verification, which is a separate prerequisite. Searching for the limit turned up very little, so: two, total, request an increase in the developer dashboard.
Worth trying before you declare anything
Write the manifest with no permissions first and add them one at a time as something actually breaks. I expected to need three and needed none.
The exercise is cheap and it tells you something about the tool. If nothing breaks, your extension never needed access to the user's pages — and the install prompt that says so is the most honest thing in the listing.
The five are open on the store under the same publisher; the UUID one is here.
Top comments (2)
withuse, this is a masterclass in the principle of least privilege. shipping chrome extensions with an empty permissions list is the ultimate form of constraint-driven, privacy-first engineering.
your point about the "review property" is incredibly profound. avoiding npm dependencies to ensure that the source code a reviewer reads is exactly the source code that ships is a security mindset that most developers completely overlook in favor of convenience.
it's also fascinating that
navigator.clipboard.writeText()works without theclipboardWritepermission in mv3. that's exactly the kind of mv3 nuance that saves us from over-declaring permissions out of habit.this perfectly mirrors the philosophy behind building tools like koda at 142kb: minimize the attack surface, eliminate unnecessary dependencies, and let the browser's native, sandboxed capabilities do the heavy lifting. if a tool doesn't need to touch the dom or make network requests, it absolutely shouldn't ask for the permission to do so.
fantastic, highly disciplined approach to extension development. the web needs more of this level of transparency! 🐯🛡️
This is a useful reminder that permission minimization can be a product constraint, not just a manifest exercise. The feature trade-offs are clearest when stated up front: no current-page access, no extension storage, and no persistent background behavior. A small decision matrix mapping each requested capability to the narrowest permission would make that boundary reusable for readers.