惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
博客园 - 叶小钗
博客园 - 聂微东
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
IT之家
IT之家
J
Java Code Geeks
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
爱范儿
爱范儿
Vercel News
Vercel News
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky
U
Unit 42
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
F
Fortinet All Blogs
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Chrome 126+ Broke My WXT Extension Dev Setup — Here's Wha...
Hiroshi Toyama · 2026-06-02 · via DEV Community

I spent a weekend debugging a Chrome extension dev environment that stopped working after a Chrome update. No error messages. The extension loaded — I could open its options page — but content scripts never ran, service workers never started, and the UI stayed unchanged.

This post is about three separate failure modes I hit, why each happens, and the minimal fix for each.

The Setup

The project uses WXT (a Chrome extension framework built on Vite) with a custom dev.sh script that:

  1. Starts the WXT dev server for hot reload
  2. Launches a dedicated Chrome instance with the extension loaded
  3. Exposes Chrome's remote debugging port for MCP/DevTools access

A clean dev-build → chrome-start → edit-reload loop. Or so it was.

Problem 1: --load-extension No Longer Starts Service Workers in Chrome 126+

What broke

Chrome has a flag --load-extension=/path/to/ext that loads an unpacked extension at startup. Before Chrome 126, this worked well for local development. After Chrome 126, the extension appears to load — chrome-extension://ID/options.html is accessible — but:

  • The background service worker never appears in Target.getTargets CDP results
  • Content scripts declared in manifest.content_scripts are not injected
  • The extension is not written to the Chrome profile's Secure Preferences

The last point is the key. With --load-extension, Chrome 126+ treats the extension as ephemeral. It's accessible as a filesystem resource but not actually "installed" in the profile, so Chrome's normal extension machinery (service worker lifecycle, content script injection) doesn't activate.

Why web-ext switched to Extensions.loadUnpacked

The web-ext project documented this in their issue tracker and switched away from --load-extension for Chrome 126+. Their new approach:

  1. Start Chrome with --remote-debugging-pipe and --enable-unsafe-extension-debugging
  2. Call the Extensions.loadUnpacked CDP command via the pipe

Extensions.loadUnpacked writes to Secure Preferences just like clicking "Load unpacked" in chrome://extensions/. Once written, the extension is a real installed extension.

Critical caveat: Extensions.loadUnpacked is only available via pipe-based CDP (--remote-debugging-pipe), not via WebSocket-based CDP (--remote-debugging-port). Connecting via port returns "Method not available." even with --enable-unsafe-extension-debugging.

The simpler fix

After trying the pipe approach, I discovered something: if you already have the extension registered in Secure Preferences from a previous pipe-based install, you can start Chrome normally (no --load-extension) with --enable-unsafe-extension-debugging and it loads from the profile automatically.

But there's an even simpler path: --load-extension + --enable-unsafe-extension-debugging together. Testing showed that when --enable-unsafe-extension-debugging is present, Chrome treats --load-extension extensions as real installs and injects content scripts:

"${CHROME_BINARY}" \
  --user-data-dir="${USER_DATA_DIR}" \
  --remote-debugging-port=9222 \
  --load-extension="${EXTENSION_DIR}" \
  --enable-unsafe-extension-debugging

That's the entire fix for service worker / content script injection.

Problem 2: WXT Dev Server Exits Immediately When Backgrounded

What broke

The dev script ran WXT in the background:

npm run dev &
WXT_PID=$!
# ... wait for build, start Chrome ...
wait "$WXT_PID"

WXT would build the extension, print "Load manually", and exit. The wait returned. The dev server was gone.

Why

WXT uses Node.js readline for its interactive keyboard shortcuts:

rl ??= readline.createInterface({
    input: process.stdin,
    // ...
})

When npm run dev & backgrounds the process, process.stdin is connected to /dev/null. readline immediately gets EOF, emits close, and WXT exits.

This only manifests when WXT is backgrounded — running it in the foreground is fine. But backgrounding is necessary because you need to start Chrome after the build completes.

The fix

Feed a non-closing stream to the process's stdin:

npm run dev < <(tail -f /dev/null) &

tail -f /dev/null follows an empty file indefinitely, never sending data and never closing. WXT's stdin stays open. readline never gets EOF. WXT keeps running.

Problem 3: web-ext Injects Flags That Break Google Login

What broke

WXT's runner (web-ext) was starting Chrome correctly — but Google account login was consistently logged out on every restart.

Why

web-ext uses chrome-launcher for Chrome startup. chrome-launcher's defaultFlags() includes:

'--disable-sync',
'--use-mock-keychain',

--use-mock-keychain is the destructive one. On macOS, Chrome encrypts cookies using the system keychain. --use-mock-keychain substitutes a fake keychain with a different encryption key. Cookies encrypted with the real keychain cannot be decrypted with the mock one and vice versa.

Once Chrome writes cookies with the mock keychain, subsequent Chrome starts (without the flag) cannot read them. Login state is destroyed.

web-ext excludes --disable-extensions, --mute-audio, and --disable-component-update from chrome-launcher's defaults — but not --disable-sync or --use-mock-keychain.

The fix

Disable WXT's web-ext runner entirely and launch Chrome manually:

// wxt.config.ts
export default defineConfig({
  webExt: {
    disabled: true,
  },
})

Then start Chrome from dev.sh with exactly the flags you need and nothing else.

Bonus: WXT Dev Mode Removes content_scripts from the Manifest

This one isn't a Chrome regression — it's WXT's intentional behavior that became a problem once service workers stopped starting.

In dev mode, WXT strips content_scripts from manifest.json and relies on the background service worker to register them dynamically via chrome.scripting.registerContentScripts(). The service worker connects to WXT's dev server and WXT sends reload commands.

When the service worker doesn't start (Problem 1), this entire chain breaks. Content scripts are never registered.

Fix: use WXT's build:manifestGenerated hook to add content_scripts back to the dev manifest:

hooks: {
  'build:manifestGenerated': (wxt, manifest) => {
    // Also strip the localhost CSP entry WXT adds for HMR — Chrome MV3
    // rejects http:// origins in extension_pages CSP.
    const csp = manifest.content_security_policy as Record<string, string> | undefined;
    if (csp?.extension_pages) {
      csp.extension_pages = csp.extension_pages
        .replace(/\s*http:\/\/localhost:[0-9]+/g, '')
        .trim();
    }

    if (wxt.config.command === 'serve' && !manifest.content_scripts?.length) {
      (manifest as Record<string, unknown>).content_scripts = [{
        matches: ['https://your-target-site.com/*'],
        run_at: 'document_end',
        js: ['content-scripts/content.js'],
      }];
    }
  },
},

This also strips http://localhost:3000 from the extension_pages CSP. Chrome MV3 forbids HTTP origins in that directive; WXT adds it for Vite HMR, but it may silently break extension loading.

The Full Picture

Three independent changes colliding:

Problem Root Cause Fix
Service worker / content scripts not running Chrome 126+ changed --load-extension to not register extensions in profile Add --enable-unsafe-extension-debugging
WXT dev server exits immediately readline on backgrounded stdin gets EOF npm run dev < <(tail -f /dev/null) &
Google login lost on restart web-ext injects --use-mock-keychain Disable WXT runner, launch Chrome manually
Content scripts not injected in dev WXT removes content_scripts from dev manifest Restore via build:manifestGenerated hook

None of these produce meaningful error messages. The extension "loads" in the sense that its static pages are accessible. Everything else silently fails. The debugging path was: CDP Target.getTargets to check for service workers, Secure Preferences inspection to check if the extension was actually installed, and process-level stdin inspection to find the WXT exit cause.

The startup order and flags are what matter:

  1. Start the dev server in the background, feeding stdin from tail -f /dev/null so it stays alive
  2. Wait for the build to finish
  3. Wait for the dev server to initialize before launching Chrome (so the service worker can connect)
  4. Launch Chrome with both --load-extension and --enable-unsafe-extension-debugging
npm run dev < <(tail -f /dev/null) &
WXT_PID=$!

# wait for build → wait for dev server → launch Chrome
"${CHROME_BINARY}" \
  --user-data-dir="${USER_DATA_DIR}" \
  --remote-debugging-port="${DEBUG_PORT}" \
  --load-extension="${EXTENSION_DIR}" \
  --enable-unsafe-extension-debugging &

wait "$WXT_PID"