<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: M.Bilal Khan</title>
    <description>The latest articles on DEV Community by M.Bilal Khan (@mbilalkhan192003).</description>
    <link>https://dev.to/mbilalkhan192003</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1716950%2Fd394b897-07c3-4b11-a635-fc908095c26c.jpeg</url>
      <title>DEV Community: M.Bilal Khan</title>
      <link>https://dev.to/mbilalkhan192003</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mbilalkhan192003"/>
    <language>en</language>
    <item>
      <title>Two Ways to Write an Article in 2026 (And Why the Best Ones Use Both)</title>
      <dc:creator>M.Bilal Khan</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:11:01 +0000</pubDate>
      <link>https://dev.to/mbilalkhan192003/two-ways-to-write-an-article-in-2026-and-why-the-best-ones-use-both-5mb</link>
      <guid>https://dev.to/mbilalkhan192003/two-ways-to-write-an-article-in-2026-and-why-the-best-ones-use-both-5mb</guid>
      <description>&lt;p&gt;It's almost &lt;strong&gt;Midnight&lt;/strong&gt;. Somewhere, a developer opens a blank tab, types a prompt into an AI, and has a polished 1,200-word article in minutes. Somewhere else, another developer opens the same blank tab, stares at it for forty minutes, deletes three sentences, writes one true one, and finally starts typing for real.&lt;/p&gt;

&lt;p&gt;Both of them will hit publish tomorrow. Only one of them will get remembered.&lt;/p&gt;

&lt;p&gt;That's the tension I want to talk about, because I think it's the real question hiding under "how do I write a good article" in 2026: not &lt;em&gt;AI or heart&lt;/em&gt; — but how you let one serve the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way One: Writing With AI
&lt;/h2&gt;

&lt;p&gt;This is the fast lane, and it's genuinely useful. You bring the idea, the AI helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Breakdown a topic&lt;/li&gt;
&lt;li&gt;Draft an outline&lt;/li&gt;
&lt;li&gt;Punch up a clunky sentence&lt;/li&gt;
&lt;li&gt;Catch the boring paragraph you were too tired to notice&lt;/li&gt;
&lt;li&gt;Turn years of scattered learning into one clean explanation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Used well, this isn't cheating. It's what editors have always done for writers — except now everyone has one.&lt;/p&gt;

&lt;p&gt;Used badly, though, it produces a very specific kind of article: correct, competent, and completely forgettable. You've read a hundred of them. Perfect grammar. Zero pulse. The kind of post you skim, nod at, and never think about again. The tell is always the same — it sounds like &lt;em&gt;an&lt;/em&gt; article about the topic, not like &lt;em&gt;your&lt;/em&gt; article about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way Two: Writing With Heart
&lt;/h2&gt;

&lt;p&gt;This is the slow lane. It's you, the actual failure that taught you the lesson, the 2 AM bug that made you want to quit, the moment something finally clicked and you felt like a wizard for ten whole minutes.&lt;/p&gt;

&lt;p&gt;Heart-writing isn't about being emotional for its own sake. It's about specificity. It's the difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Debugging can be frustrating but rewarding."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I spent three hours debugging our API, convinced the logic was broken. It wasn't. I'd written a nested loop with i and j as counters, and with my eyesight, on a bad-contrast monitor, they were nearly indistinguishable — I kept reading i where I'd written j, and my brain corrected it every single time without telling me. It took a junior dev, glancing over my shoulder for thirty seconds, to spot it. I went home and renamed many variable I owned that night..based on True story😊"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second one isn't better because it's more honest in some abstract moral sense. It's better because it's &lt;em&gt;true in a way nobody else could have written&lt;/em&gt;. That's the whole game. AI can simulate insight. It cannot simulate the specific, embarrassing, real thing that happened to you.&lt;/p&gt;

&lt;p&gt;The cost of this path is that it's slower, scarier, and occasionally humbling — because writing with heart means admitting you didn't always know what you were doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Answer: They're Not Two Roads. They're Two Hands.
&lt;/h2&gt;

&lt;p&gt;Here's what actually works, and it's the framework underneath most articles you've bookmarked without noticing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Start with the true thing only you have.&lt;/strong&gt;&lt;br&gt;
Not a topic. A moment. The bug, the mistake, the surprising result, the question a junior dev asked you that you couldn't answer cleanly. This is the part AI cannot do for you, because it doesn't have your Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Let AI hold the structure while you hold the story.&lt;/strong&gt;&lt;br&gt;
Once you know &lt;em&gt;what happened&lt;/em&gt;, structure is a solved problem — and this is where AI genuinely shines. Ask it to help you outline, to suggest where the piece drags, to find the sentence buried on paragraph nine that should actually be your opening line. Let it be your scaffolding, not your voice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Write the ugly first draft yourself.&lt;/strong&gt;&lt;br&gt;
Even three rough paragraphs. Even bad ones. This is non-negotiable, and here's why: your voice lives in your &lt;em&gt;word choices under pressure&lt;/em&gt; — the slightly weird metaphor, the joke that only makes sense if you know you, the rhythm of how you explain things when you're not trying to sound impressive. AI can polish that voice. It can't originate it from nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Use AI as your harshest kind editor, not your ghostwriter.&lt;/strong&gt;&lt;br&gt;
"Where does this get boring?" "What's the weakest paragraph?" "Is my opening line actually earning the reader's next thirty seconds?" These are incredible questions to ask a model. "Write my article for me" is not — because you'll get back competence with the fingerprints sanded off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Read it out loud before you publish.&lt;/strong&gt;&lt;br&gt;
This one has nothing to do with AI. It's the oldest trick there is, and it still catches everything: the sentence that's too long, the paragraph that doesn't mean anything, the ending that fizzles instead of lands. If you'd feel embarrassed saying a sentence to a friend, don't publish it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Actually Matters
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you about writing on dev.to or Medium in 2026: readers have gotten &lt;em&gt;very&lt;/em&gt; good at smelling the difference between a piece written &lt;em&gt;through&lt;/em&gt; someone and a piece written &lt;em&gt;by&lt;/em&gt; something. Not because AI writing is bad — it isn't — but because readers aren't actually looking for information anymore. They can get information anywhere, instantly, for free. What they're looking for, every time they click on an article instead of asking a chatbot directly, is &lt;em&gt;you&lt;/em&gt;. Your particular scar tissue. Your particular way of seeing a problem. The proof that a real person went through something and came back with a flashlight for the rest of us.&lt;/p&gt;

&lt;p&gt;That's not a nostalgic argument for doing things the hard way. It's a practical one: the articles that get saved, shared, and remembered are the ones where a real experience got a little help getting dressed for the world — not the ones where an experience was replaced entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Start Tonight
&lt;/h2&gt;

&lt;p&gt;Don't open with a prompt. Open with a memory — the bug, the launch that went sideways, the comment on your PR that stung more than it should have, the tiny win nobody else would understand the weight of.&lt;/p&gt;

&lt;p&gt;Write that part yourself, badly, honestly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Then&lt;/em&gt; bring in the help.&lt;/p&gt;

&lt;p&gt;That's not two ways to write an article in 2026. That's one way, done right — with both hands.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>productivity</category>
      <category>contentwriting</category>
    </item>
    <item>
      <title>I built a Chrome extension, got 100 installs, and 90% of users left. Here's what I'm fixing.</title>
      <dc:creator>M.Bilal Khan</dc:creator>
      <pubDate>Tue, 11 Aug 2026 10:42:27 +0000</pubDate>
      <link>https://dev.to/mbilalkhan192003/i-built-a-chrome-extension-got-100-installs-and-90-of-users-left-heres-what-im-fixing-3gmi</link>
      <guid>https://dev.to/mbilalkhan192003/i-built-a-chrome-extension-got-100-installs-and-90-of-users-left-heres-what-im-fixing-3gmi</guid>
      <description>&lt;p&gt;Somewhere around install #80, I stopped checking my Chrome Web Store dashboard every day. Not because I'd lost interest — because the number that mattered wasn't installs anymore, it was this one: 10 active users.&lt;/p&gt;

&lt;p&gt;Out of 101 people who installed &lt;a href="https://chromewebstore.google.com/detail/ihclgidibhioanhjchbhcblpfhaipiok?utm_source=dev.to"&gt;Goat Alert&lt;/a&gt;, roughly 20 uninstalled it outright, and most of the rest just... quietly stopped using it. No uninstall, no feedback, no bug report. They just went dark.&lt;/p&gt;

&lt;p&gt;That's a rough number to sit with when you built the thing yourself. But it's also the most useful number I have, because it's telling me something installs never could: the idea works well enough to get a "yes," and not well enough to get a "keep."&lt;/p&gt;

&lt;p&gt;Here's what I think is actually happening.&lt;/p&gt;

&lt;p&gt;Goat Alert does one thing: it notifies you at a random interval, and if you ignore it for 60 seconds, a goat sound plays. That's funny for the first three, maybe five times. Then it becomes exactly what any repetitive notification becomes — noise you've learned to dismiss on reflex, or annoying enough that you just turn the extension off.&lt;/p&gt;

&lt;p&gt;I built a joke. I didn't build a habit. Those are different problems, and I only fully understood that after watching my own retention numbers.&lt;/p&gt;

&lt;p&gt;What I'm changing:&lt;/p&gt;

&lt;p&gt;Snooze, not just dismiss. Right now the only options are "deal with the notification" or "ignore it and get screamed at." There's no middle ground for "not now, but check on me in 5." Adding a snooze button on the notification itself — no popup, no extra click, just an action button.&lt;/p&gt;

&lt;p&gt;Rotating messages. Same text every time is a big part of why it becomes background noise so fast. Swapping in a small rotating set of messages so it doesn't read as identical each time.&lt;/p&gt;

&lt;p&gt;Streaks and a stats page. This is the one I think matters most. Right now there's zero feedback loop — you either get annoyed or you don't, and nothing sticks around to show you it's working. Adding a simple streak counter and a stats page (times dismissed, times the goat actually screamed, days active) gives people a reason to open the popup that isn't just "turn this off."&lt;/p&gt;

&lt;p&gt;A gentler alternative sound. Not everyone wants a goat scream as their daily interruption. Adding a soft chime mode as an option, so the "annoying by design" mechanic still works for people who want persistence without chaos.&lt;/p&gt;

&lt;p&gt;None of this requires new permissions, so the update ships silently to everyone already using it — no re-approval prompt, no disruption.&lt;/p&gt;

&lt;p&gt;The honest part&lt;/p&gt;

&lt;p&gt;I don't know yet if any of this actually fixes retention. It's a reasonable theory based on watching where people seem to drop off, not a guarantee. The only way I'll actually know is by shipping it and watching the same numbers again in a few weeks.&lt;/p&gt;

&lt;p&gt;If you've built something small that people install and then abandon, I'd genuinely like to hear what you found — whether it was a UX thing, a "this solved a one-time problem" thing, or something else entirely. Retention on tiny tools feels like an underdiscussed problem compared to how much airtime "getting the first users" gets.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sideprojects</category>
      <category>javascript</category>
      <category>extensions</category>
    </item>
    <item>
      <title>How MV3 Service Workers Made Me Use an Offscreen Document Just to Play a Goat Sound</title>
      <dc:creator>M.Bilal Khan</dc:creator>
      <pubDate>Tue, 21 Jul 2026 06:59:27 +0000</pubDate>
      <link>https://dev.to/mbilalkhan192003/how-mv3-service-workers-made-me-use-an-offscreen-document-just-to-play-a-goat-sound-dd3</link>
      <guid>https://dev.to/mbilalkhan192003/how-mv3-service-workers-made-me-use-an-offscreen-document-just-to-play-a-goat-sound-dd3</guid>
      <description>&lt;p&gt;So I built a Chrome extension that does one very stupid thing: it waits a random amount of time, then screams at you like a goat if you don't dismiss the notification fast enough.&lt;/p&gt;

&lt;p&gt;Simple idea. Should've been a simple build. It was not, because Manifest V3 said no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plan was easy, in my head&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what I thought I needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;chrome.alarms&lt;/code&gt; to fire at a random interval&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chrome.notifications&lt;/code&gt; to show a "you good?" popup&lt;/li&gt;
&lt;li&gt;If the user ignores it for 60 seconds, play a goat sound&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where things fell apart. Because in Manifest V3, your background script isn't a persistent background page anymore — it's a service worker. And service workers have the audio capabilities of a rock. No &lt;code&gt;Audio()&lt;/code&gt; object. No Web Audio API. Nothing. You can't just do &lt;code&gt;new Audio('goat.mp3').play()&lt;/code&gt; and call it a day, because there's no DOM for it to live in.&lt;/p&gt;

&lt;p&gt;I found this out the way everyone finds out things in MV3: by writing the obvious code, watching it silently fail, and then spending 20 minutes wondering if I'd misspelled "goat."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter: the offscreen document&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chrome's answer to "service workers can't do X" for a bunch of X's (audio playback included) is the &lt;strong&gt;offscreen document API&lt;/strong&gt;. It's exactly what it sounds like — a hidden HTML page that exists purely so your extension has something with a DOM, so it can do DOM things. In my case, playing an mp3 of a goat losing its mind.&lt;/p&gt;

&lt;p&gt;The flow ends up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service worker (background.js)
  → creates an offscreen document if one doesn't exist
  → sends it a message: "play the sound"
offscreen.html/offscreen.js
  → has an &amp;lt;audio&amp;gt; tag
  → actually plays the sound
  → closes itself when done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a little bit like hiring a stunt double because the main actor (your service worker) isn't allowed near water. The offscreen document does the wet work, then goes home.&lt;/p&gt;

&lt;p&gt;Rough version of what that looks like in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// background.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;playGoatSound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offscreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasDocument&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offscreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offscreen.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AUDIO_PLAYBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;justification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Playing a goat scream sound effect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playSound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// offscreen.js&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playSound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sounds/goat.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic. But it took me embarrassingly long to figure out that "reasons: ['AUDIO_PLAYBACK']" is a required field and not optional flavor text, and that you genuinely cannot skip the offscreen document if you want sound out of a service worker. There's no secret shortcut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other MV3 gotcha: your background script forgets everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Service workers also don't persist. Chrome can (and will) kill your background script whenever it feels like, which is a problem when your whole app is "wait N minutes, then do a thing." If your script gets shut down mid-wait, a regular &lt;code&gt;setTimeout&lt;/code&gt; just... doesn't fire. It's gone. Chrome ate it.&lt;/p&gt;

&lt;p&gt;This is what &lt;code&gt;chrome.alarms&lt;/code&gt; is actually for — it's not just a nice API, it's the only way to reliably schedule something in the future when your background context can disappear at any moment. So Goat Alert ends up using two alarms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One for "time to show the notification" (the random 5/10/15/20 min interval)&lt;/li&gt;
&lt;li&gt;One for "15 seconds have passed, did they dismiss it? No? Ok, scream."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both survive the service worker dying and waking back up, because &lt;code&gt;chrome.alarms&lt;/code&gt; is backed by the browser itself, not by whatever fragile JS context happened to be running when you set the timer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd tell past me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building this taught me that a huge chunk of "why is my simple Chrome extension so complicated" comes down to one thing: Manifest V3 assumes your background script is disposable, and designs everything around surviving that. Once you accept that and stop trying to keep long-lived state or objects (like an Audio instance) alive in the service worker, the actual APIs make sense. Offscreen documents for anything DOM-shaped, alarms for anything time-shaped.&lt;/p&gt;

&lt;p&gt;Anyway. 100+ installs, people are enjoying it, so all efforts are worth it.&lt;/p&gt;

&lt;p&gt;Give it a try, will put a smile on your face &lt;a href="https://chromewebstore.google.com/detail/ihclgidibhioanhjchbhcblpfhaipiok?utm_source=dev.to"&gt;Goat Alert&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I spent a week trying to intercept Slack push notifications from a Chrome extension. Here's why it's impossible.</title>
      <dc:creator>M.Bilal Khan</dc:creator>
      <pubDate>Wed, 15 Jul 2026 18:55:52 +0000</pubDate>
      <link>https://dev.to/mbilalkhan192003/i-spent-a-week-trying-to-intercept-slack-push-notifications-from-a-chrome-extension-heres-why-9mc</link>
      <guid>https://dev.to/mbilalkhan192003/i-spent-a-week-trying-to-intercept-slack-push-notifications-from-a-chrome-extension-heres-why-9mc</guid>
      <description>&lt;p&gt;After I published my last article about building a Chrome extension that speaks browser notifications aloud, a commenter asked a question I didn't have a good answer to.&lt;/p&gt;

&lt;p&gt;He pointed out that a lot of web apps — Slack, Gmail, most modern tools — fire their notifications from a service worker via &lt;code&gt;registration.showNotification()&lt;/code&gt;, not from the page's JavaScript context. My MAIN world override of &lt;code&gt;window.Notification&lt;/code&gt; would never reach those.&lt;/p&gt;

&lt;p&gt;He was right. And I told him I'd look into it.&lt;/p&gt;

&lt;p&gt;I spent a week researching whether there was any way to close that gap. There isn't. But the reason why is more interesting than a simple "no."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Two ways a website can show you a notification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a website sends you a browser notification, it can do it in one of two ways.&lt;/p&gt;

&lt;p&gt;The first is the constructor path. The page's own JavaScript calls &lt;code&gt;new Notification("You have a message")&lt;/code&gt; directly. This is common for in-tab alerts, real-time updates when you're actively on the site, or any notification triggered by something you just did.&lt;/p&gt;

&lt;p&gt;The second is the push path. The browser receives a push event from the website's server, wakes up the website's service worker in the background, and the service worker calls &lt;code&gt;self.registration.showNotification()&lt;/code&gt; from inside its own scope. This is what happens when Slack notifies you of a new message while the tab is closed or backgrounded. The page never runs. No page JavaScript ever fires.&lt;/p&gt;

&lt;p&gt;My extension catches the first path. The MAIN world content script overrides &lt;code&gt;window.Notification&lt;/code&gt; before any page code runs. But the service worker never touches the page's window. It has no &lt;code&gt;window&lt;/code&gt;. It runs in a completely isolated thread, completely separate from the page, and calls &lt;code&gt;showNotification&lt;/code&gt; on itself.&lt;/p&gt;

&lt;p&gt;The override is never reached.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Why can't the extension reach the service worker?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that took me a week to fully accept.&lt;/p&gt;

&lt;p&gt;Chrome extensions can inject content scripts into web pages. They can run code in the MAIN world or the ISOLATED world of a page. They can observe network requests, intercept navigations, and modify headers.&lt;/p&gt;

&lt;p&gt;What they cannot do is inject code into a third-party website's service worker.&lt;/p&gt;

&lt;p&gt;Service workers operate in a separate thread with no DOM, no window, and no connection to the page's JavaScript context. The Chrome Extensions API simply does not offer a mechanism to run extension code inside an arbitrary third-party service worker scope. It's a hard security boundary — and for good reasons. Allowing extensions to arbitrarily patch service workers for any website on the internet would be a significant attack surface.&lt;/p&gt;

&lt;p&gt;So: the extension lives on the page side. The push notification lives on the service worker side. There is a wall between them.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Three approaches I investigated&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I didn't accept "impossible" without looking for workarounds. Here's what I found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach 1: Override ServiceWorkerRegistration.prototype.showNotification in the MAIN world&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A page's JavaScript can call &lt;code&gt;navigator.serviceWorker.ready.then(reg =&amp;gt; reg.showNotification(...))&lt;/code&gt;. If you override &lt;code&gt;ServiceWorkerRegistration.prototype.showNotification&lt;/code&gt; in the MAIN world, you'd intercept that pattern.&lt;/p&gt;

&lt;p&gt;The problem: Slack's push notifications don't come from page JavaScript. They come from inside the service worker script itself, where it calls &lt;code&gt;self.registration.showNotification()&lt;/code&gt;. The service worker runs in its own scope. The MAIN world prototype override doesn't reach there.&lt;/p&gt;

&lt;p&gt;This approach catches one narrow pattern and misses the actual push notification path entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach 2: chrome.debugger API with CDP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the most discussed workaround in the Chromium Extensions community. The &lt;code&gt;chrome.debugger&lt;/code&gt; API gives an extension access to the Chrome DevTools Protocol. With CDP's &lt;code&gt;Fetch.fulfillRequest&lt;/code&gt; command, you can intercept a network response — including a service worker JavaScript file — and modify it before the browser processes it. Inject your code into the SW script, and it runs inside the service worker scope.&lt;/p&gt;

&lt;p&gt;This technically works. It is completely unusable for a consumer extension.&lt;/p&gt;

&lt;p&gt;When you attach the debugger to any tab, Chrome displays a persistent banner at the top of the browser: "Chrome is being controlled by automated test software." This banner appears on every tab. It cannot be dismissed. It cannot be hidden by the extension. The only way to suppress it is to launch Chrome with &lt;code&gt;--silent-debugger-extension-api&lt;/code&gt;, which is not something a real user would do.&lt;/p&gt;

&lt;p&gt;For a Chrome Web Store extension that people install on their own machine, this approach would immediately look like malware. Ruled out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach 3: declarativeNetRequest&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Manifest V3's &lt;code&gt;declarativeNetRequest&lt;/code&gt; API lets extensions block or redirect network requests. Could you redirect the Slack service worker script to a modified version?&lt;/p&gt;

&lt;p&gt;No. &lt;code&gt;declarativeNetRequest&lt;/code&gt; can block or redirect a request, but it cannot modify the response body. And even if it could redirect the service worker script URL, the replacement would need to be served from the same origin as the site — extensions can't serve content from &lt;code&gt;slack.com&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What Firefox can do that Chrome can't&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's where it gets interesting.&lt;/p&gt;

&lt;p&gt;Firefox has an API called &lt;code&gt;webRequest.filterResponseData()&lt;/code&gt;. It lets an extension intercept a network response and modify its body before the browser processes it. Applied to a service worker JavaScript file, you could inject code that runs inside the SW scope. That code could override &lt;code&gt;self.registration.showNotification&lt;/code&gt; and intercept push notifications.&lt;/p&gt;

&lt;p&gt;This capability exists in Firefox. It was advocated for by Giorgio Maone, the developer of NoScript, who needed exactly this kind of deep injection for a privacy tool.&lt;/p&gt;

&lt;p&gt;Chrome removed blocking &lt;code&gt;webRequest&lt;/code&gt; in MV3 and never added a &lt;code&gt;filterResponseData&lt;/code&gt; equivalent. This is one of the more significant functional divergences between the two extension models post-MV3. Extension developers have raised it on the Chromium mailing lists. For now, it's a genuine gap.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What this means for &lt;a href="https://chromewebstore.google.com/detail/hnaggblalhlbihfaegbknioadncpcged?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=article2-sw" rel="noopener noreferrer"&gt;Serious Notification&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The extension works well for the constructor path — notifications fired from page JavaScript when a tab is active. For many apps and many use cases, that's most of their notification volume.&lt;/p&gt;

&lt;p&gt;For pure push-via-service-worker notifications from backgrounded tabs — like Slack when you've minimised the window — the extension will miss them. I've added a Known Limitations section to the original article to reflect this honestly.&lt;/p&gt;

&lt;p&gt;What would need to change for this to be solvable on Chrome? Some form of extension access to service worker scope — whether through a filtered response body API like Firefox's, or through a purpose-built notification interception hook in the Chrome Extensions API. Neither exists today.&lt;/p&gt;

&lt;p&gt;If you want to see this change, the Chromium issue tracker is the place to make that case. I'm watching it.&lt;/p&gt;

&lt;p&gt;In the meantime, if you want to try what's possible now:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://chromewebstore.google.com/detail/hnaggblalhlbihfaegbknioadncpcged?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=article2-sw" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/hnaggblalhlbihfaegbknioadncpcged?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=article2-sw&lt;/a&gt;&lt;/p&gt;

</description>
      <category>extensions</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>How I built a Chrome extension that speaks your notifications aloud — and the two Manifest V3 problems nobody warned me about</title>
      <dc:creator>M.Bilal Khan</dc:creator>
      <pubDate>Tue, 07 Jul 2026 19:40:25 +0000</pubDate>
      <link>https://dev.to/mbilalkhan192003/how-i-built-a-chrome-extension-that-speaks-your-notifications-aloud-and-the-two-manifest-v3-3239</link>
      <guid>https://dev.to/mbilalkhan192003/how-i-built-a-chrome-extension-that-speaks-your-notifications-aloud-and-the-two-manifest-v3-3239</guid>
      <description>&lt;p&gt;I counted how many times I glanced at a notification during a coding session last week.&lt;/p&gt;

&lt;p&gt;Thirty-four times. Only four needed my attention.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://chromewebstore.google.com/detail/hnaggblalhlbihfaegbknioadncpcged?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=technical-article" rel="noopener noreferrer"&gt;Serious Notification&lt;/a&gt; — a Chrome extension that speaks browser notifications aloud, but only when they match keywords you set. Type "failed" and only hear build errors. Type "urgent" and only hear what actually matters. Everything else stays silent.&lt;/p&gt;

&lt;p&gt;It took a weekend to build the core. The two Manifest V3 architectural problems I hit along the way took longer to figure out. Neither of them is well documented. Here's both.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Problem 1: You can't intercept the Notification API in a regular content script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The plan seemed simple: listen for notifications, check them against keywords, speak the matching ones. A content script listening for notification events should work, right?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Chrome's &lt;code&gt;Notification&lt;/code&gt; constructor lives on &lt;code&gt;window&lt;/code&gt;. When a website calls &lt;code&gt;new Notification("You have a message")&lt;/code&gt;, it's using &lt;code&gt;window.Notification&lt;/code&gt; in the page's own JavaScript context.&lt;/p&gt;

&lt;p&gt;A standard Manifest V3 content script runs in what Chrome calls an &lt;strong&gt;ISOLATED world&lt;/strong&gt; — a sandboxed environment that shares the DOM with the page but has its own separate JavaScript context. Your content script can read the DOM, but it cannot touch &lt;code&gt;window.Notification&lt;/code&gt; on the actual page. By the time your script runs, the page's Notification constructor is completely separate from anything you can reach.&lt;/p&gt;

&lt;p&gt;You can listen for a notification after it fires. You cannot intercept it before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: MAIN world content scripts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Manifest V3 added support for content scripts that run in the MAIN world. Set &lt;code&gt;"world": "MAIN"&lt;/code&gt; in your manifest and your script runs directly in the page's JavaScript context — before any page code executes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content_scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;all_urls&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"interceptor.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MAIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"run_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document_start"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can override &lt;code&gt;window.Notification&lt;/code&gt; before the page has a chance to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OriginalNotification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Notification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__sn_notification__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OriginalNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The catch: MAIN world scripts have no Chrome extension APIs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the problem with MAIN world scripts. They run in the page context, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;chrome.storage&lt;/code&gt; — you cannot read the user's saved keywords&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;speechSynthesis&lt;/code&gt; — you cannot speak the notification aloud&lt;/li&gt;
&lt;li&gt;No extension APIs at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are in the page's world. Chrome's extension APIs are not available there.&lt;/p&gt;

&lt;p&gt;So you need two scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bridge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Script 1 (&lt;code&gt;interceptor.js&lt;/code&gt;) runs in the MAIN world. It overrides &lt;code&gt;window.Notification&lt;/code&gt; and fires a custom DOM event when a notification arrives.&lt;/p&gt;

&lt;p&gt;Script 2 (&lt;code&gt;content.js&lt;/code&gt;) runs in the ISOLATED world. It has full access to &lt;code&gt;chrome.storage&lt;/code&gt; and &lt;code&gt;speechSynthesis&lt;/code&gt;. It listens for the custom event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__sn_notification__&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keywords&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;speed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;utterance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SpeechSynthesisUtterance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;utterance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;speechSynthesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utterance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two scripts. One custom DOM event as the bridge. The MAIN world intercepts, the ISOLATED world decides.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Problem 2: Chrome showed a "read and change all your data on all websites" warning at install&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The extension needs to inject on every page — that's legitimate, since you don't know which tab will fire a notification. So &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; host access is genuinely required.&lt;/p&gt;

&lt;p&gt;But I was declaring it as a required permission in &lt;code&gt;manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"content_scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;all_urls&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This caused Chrome to show the scariest possible install warning before users had any reason to trust the extension. I was watching my analytics and seeing a 33% uninstall rate. Some of that was almost certainly people bouncing at the permission screen, before they ever heard the extension speak a single word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: optional host permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Manifest V3 supports &lt;code&gt;optional_host_permissions&lt;/code&gt;. Move &lt;code&gt;&amp;lt;all_urls&amp;gt;&lt;/code&gt; there instead of requiring it at install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scripting"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"optional_host_permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;all_urls&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then register content scripts dynamically at runtime using &lt;code&gt;chrome.scripting.registerContentScripts()&lt;/code&gt;, only after the user explicitly grants the permission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// background.js&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onAdded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerContentScripts&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interceptor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interceptor.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MAIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;runAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document_start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;runAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document_start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the popup, show a "Turn on to hear alerts" button that triggers the permission request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: the Chrome Web Store install dialog now shows &lt;strong&gt;Storage only&lt;/strong&gt;. No broad-access warning. The scary prompt only appears when the user explicitly clicks "Turn on" inside the popup — after they have already installed the extension and understand what it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One edge case: updating from a version that required the permission&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you update an extension and a previously required permission becomes optional, Chrome removes it from the user's granted set. Existing users lose host access silently on update — the extension just stops working with no explanation.&lt;/p&gt;

&lt;p&gt;The fix: fire a native notification immediately after update using the &lt;code&gt;notifications&lt;/code&gt; permission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onInstalled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update-notice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;basic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;iconUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;icon.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Serious Notification — One quick step needed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;We improved how permissions work. Open the extension and click "Turn on" to re-enable.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;requireInteraction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silent failure is the worst outcome for retention. If something breaks on update, tell the user immediately and tell them exactly what to do.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I built&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Serious Notification speaks Chrome notifications aloud when they match keywords you set. Free, no account, works offline. Just shipped v0.2.0 with the optional permissions model above.&lt;/p&gt;

&lt;p&gt;If you're building Chrome extensions and hit either of these problems — happy to answer questions in the comments. The MAIN world injection in particular has some edge cases around static property copying (&lt;code&gt;permission&lt;/code&gt;, &lt;code&gt;requestPermission&lt;/code&gt;) that took me a while to get right.&lt;/p&gt;

&lt;p&gt;🔗&lt;a href="https://chromewebstore.google.com/detail/hnaggblalhlbihfaegbknioadncpcged?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=technical-article" rel="noopener noreferrer"&gt;Serious Notification - Live Chrome WebStore&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Known Limitations
&lt;/h2&gt;

&lt;p&gt;This approach only intercepts notifications fired via the Notification constructor in the page's JavaScript context.&lt;/p&gt;

&lt;p&gt;Notifications fired from a service worker via registration.showNotification() — which is the path used by background push notifications in apps like Slack — bypass the MAIN world override entirely. The service worker runs in its own scope and never touches the page's window.&lt;/p&gt;

&lt;p&gt;Chrome's extension model does not currently offer a hook into arbitrary third-party service workers, so this gap cannot be closed with the current architecture.&lt;/p&gt;

&lt;p&gt;The extension works well for in-tab notifications and apps that call the constructor directly from page scripts. For pure push-via-service-worker apps when the tab is backgrounded, it will miss notifications.&lt;/p&gt;

</description>
      <category>extensions</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
