Zen Google: Blocking the Sign-In Popup and AI Overviews with Manifest V3
- Chrome Extensions
- Manifest V3
- declarativeNetRequest
- JavaScript
- zen-google
Two things follow you around the modern web: Google's "Sign in with Google" One Tap popup on seemingly every third-party site, and AI Overviews squatting on top of your search results. Zen Google is my Chrome extension that removes both — now live on the Chrome Web Store, with source on GitHub.
Design constraints: Manifest V3, zero tracking, zero servers, zero build step — vanilla JS, plain HTML/CSS, and Chrome's declarative APIs doing the heavy lifting. Here's the engineering that turned out to be interesting.
Blocking One Tap at the network layer
The One Tap popup comes from Google Identity Services. Block the source and the popup never exists. A static declarativeNetRequest ruleset blocks two URL filters:
||accounts.google.com/gsi/client— the GIS script itself||accounts.google.com/gsi/iframe/select— the iframe that renders the prompt
DNR is the right tool here precisely because it's declarative: Chrome evaluates the rules natively, the extension needs no blanket "read all your traffic" justification, and there's no service-worker wake-up cost per request.
But network blocking alone has a flicker problem: if the script is already cached, or a site inlines fallback UI, you briefly see the popup container before anything gets blocked. So a tiny CSS file registered at document_start belt-and-suspenders it:
#credential_picker_container,
iframe[src*="accounts.google.com/gsi/"] {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
Registered dynamically via chrome.scripting, only when the feature is on.
The allowlist: fighting DNR with DNR
Some sites you genuinely want to sign into with Google. Rather than toggling the whole extension, Zen Google uses DNR's priority system against itself: for each allowlisted domain, the background worker registers two dynamic allow rules (IDs 1001/1002) targeting the same gsi/client and gsi/iframe/select filters, but scoped via initiatorDomains to just your allowlisted sites. Allow rules outrank block rules, so One Tap works there and nowhere else.
One subtlety in the background worker: settings changes arrive from the popup, the options page, and storage.sync events — potentially concurrently. Rule updates are serialized through a promise chain so two rapid toggles can't interleave updateDynamicRules calls and leave the ruleset half-applied.
FedCM: the popup you can't block
Chrome is migrating One Tap to FedCM, where the prompt is drawn by the browser itself, outside any page. No content script, CSS, or network rule can touch it. Instead of pretending otherwise, the popup links straight to chrome://settings/content/federatedIdentityApi so you can flip the native switch. Knowing where your extension's power ends is a feature.
Hiding AI Overviews without brittle selectors
Google's class names are compiler output — .WaaZC today, gone tomorrow. Any extension keyed to them breaks weekly. Zen Google instead anchors on the accessibility tree, which Google can't churn without breaking screen readers: it looks for elements whose aria-label is "AI Overview" or headings with that exact text, then climbs exactly three ancestors to reach the card container and hides it. A MutationObserver debounced at 200ms re-applies on Google's incremental rerenders.
The udm=14 redirect, without loops
The cleaner alternative (and the default) skips DOM surgery entirely: Google's udm=14 parameter requests the text-only "Web" results layout — no AI Overview is ever rendered or even generated. A DNR redirect rule appends it to search URLs.
A naive redirect rule loops forever, though: the redirected URL is itself a Google search URL that matches the rule. And a too-aggressive rule breaks Images and Shopping, which use their own udm values. The fix is a two-rule hierarchy:
- Allow rule, priority 2 — if the URL already contains any
udm=parameter, pass through untouched. - Redirect rule, priority 1 — otherwise, transform the query to add
udm=14.
Rule 1 simultaneously prevents the loop (the redirected request carries udm=14) and preserves the other verticals (they carry their own udm). Two declarative rules replace what would otherwise be a pile of imperative URL parsing.
Install-time trust
Every host permission is declared under optional_host_permissions, so the install dialog shows no scary warnings — the extension requests access only when you actually toggle a feature, and the permission grant maps one-to-one to the thing you just asked for. For a privacy-positioned extension, asking for <all_urls> upfront would undercut the entire premise.
The whole thing ships with no analytics, no remote config, no accounts — settings live in chrome.storage.sync. Grab it from the Chrome Web Store or read the source on GitHub. A Safari port is in progress.