Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Load an unpacked extension when you launch Chromium, and explicitly remove Pyppeteer’s default --disable-extensions flag. Use absolute paths and headed Chromium for the most predictable results:
from pathlib import Path
import asyncio
from pyppeteer import launch
async def main():
extension_path = str((Path(__file__).parent / "my-extension").resolve())
browser = await launch({
"headless": False,
"ignoreDefaultArgs": ["--disable-extensions"],
"args": [
f"--disable-extensions-except={extension_path}",
f"--load-extension={extension_path}",
],
})
try:
await asyncio.sleep(2)
finally:
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
The extension directory must be unpacked and contain a valid manifest.json. For Manifest V2, wait for a background_page target; for Manifest V3, wait for a service_worker target.
What you need before launching
- Python and an installed Pyppeteer package.
- A Chromium-compatible browser. Pyppeteer works best with its bundled Chromium; a separately managed executable is not guaranteed to behave identically.
- An unpacked extension directory containing
manifest.jsonand every referenced script, HTML file, icon and resource. - An absolute filesystem path to that directory.
Pyppeteer’s default launch mode is headless. Because extension support differs by Chromium version and headless implementation, start with headless: False while developing and diagnosing the extension.
Prepare an unpacked extension
Point Chromium at the directory itself, not at a ZIP file or the manifest file. A typical layout is:
Recommended Free Tools
#1 Best Overall
my-extension/
├── manifest.json
├── background.js # MV2, or a service-worker script for MV3
├── content.js
├── popup.html
└── popup.js
The manifest determines whether the extension creates a persistent background page (Manifest V2) or a service worker (Manifest V3). Keep the manifest and all referenced files inside the directory you pass to Chromium.
Launch Pyppeteer with the extension enabled
Why ignoreDefaultArgs is required
Pyppeteer’s launcher adds --disable-extensions to its default Chromium arguments. If that argument remains, Chromium can ignore the extension even when --load-extension is present. Set ignoreDefaultArgs to remove that one conflicting argument, then add the two extension flags yourself:
--disable-extensions-except=/absolute/pathlimits enabled extensions to the directory you specify.--load-extension=/absolute/pathloads that unpacked directory.
Complete asynchronous example
from pathlib import Path
import asyncio
from pyppeteer import launch
async def main():
extension_path = str((Path(__file__).parent / "my-extension").resolve())
browser = await launch({
"headless": False,
"ignoreDefaultArgs": ["--disable-extensions"],
"args": [
f"--disable-extensions-except={extension_path}",
f"--load-extension={extension_path}",
],
})
try:
page = await browser.newPage()
await page.goto("https://example.com", {"waitUntil": "networkidle2"})
# Exercise the extension or inspect its targets here.
await asyncio.sleep(2)
finally:
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
Using Path.resolve() avoids failures caused by launching the script from a different working directory. Keep the extension path free of accidental trailing characters and verify that the resolved directory exists before calling launch().
Verify that Chromium actually loaded the extension
Manifest V3: find the service worker
async def extension_service_worker(browser):
target = await browser.waitForTarget(
lambda t: t.type == "service_worker"
and "chrome-extension://" in t.url
)
print("Service-worker target:", target.url)
return await target.worker()
Call this after launch. The returned worker object lets you evaluate extension-side code. If several workers exist, match a distinctive part of the target URL, such as the extension ID or background script path.
Free tools Windows power users keep installed
One-click scans. No signup required.
Manifest V2: find the background page
async def extension_background_page(browser):
target = await browser.waitForTarget(
lambda t: t.type == "background_page"
and "chrome-extension://" in t.url
)
print("Background-page target:", target.url)
return await target.page()
Use the target’s type and url as diagnostics. A normal tab, a popup, and an extension worker can all appear as separate targets, so checking both prevents you from attaching to the wrong context.
Wait instead of assuming startup timing
Extension targets may appear after the first page opens. browser.waitForTarget() is preferable to a fixed sleep when your test depends on the worker or background page. A short sleep can still be useful for a visual smoke test, but it should not be your only readiness check.
Headed Chromium, headless modes and browser choice
| Choice | What to expect | Recommended use |
|---|---|---|
Headed Chromium (headless: False) |
The most conservative and observable option for extension testing. | Development, debugging and CI failures that need visual confirmation. |
| Experimental headless Chrome | Extension behavior depends on the Chromium version and headless implementation. | Use only after validating the exact Pyppeteer/Chromium pair. |
| Pyppeteer’s bundled Chromium | Pyppeteer is designed around this browser build. | Start here for reproducible setup. |
Separate executablePath |
Pyppeteer documents the option, but another executable is not guaranteed to be compatible. | Use when your project pins a browser, and verify loading and target behavior explicitly. |
Pyppeteer is an unofficial Python port and is currently unmaintained. Pin your Pyppeteer and Chromium versions in the project environment, then run a smoke test that confirms the expected target type before relying on extension behavior in a larger suite.
Testing extension pages and content scripts
Open an extension page directly
Once you know the extension ID from a target URL, an extension page can be opened with a chrome-extension:// URL:
Rank #3
extension_id = "YOUR_EXTENSION_ID"
extension_page = await browser.newPage()
await extension_page.goto(
f"chrome-extension://{extension_id}/popup.html",
{"waitUntil": "networkidle0"},
)
This is useful for testing popup markup and scripts independently of a toolbar click. The exact path must match the file declared by your manifest.
Exercise a content script on a normal site
Navigate a regular page after launch, then assert the DOM changes or messages your content script is expected to produce:
page = await browser.newPage()
await page.goto("https://example.com", {"waitUntil": "networkidle2"})
result = await page.evaluate("""() => ({
title: document.title,
marker: document.querySelector('[data-extension-marker]')?.textContent || null
})""")
print(result)
If your content script runs at document start, wait for the navigation and then check its marker or other observable effect. Keep extension-context assertions on the worker or background page; keep site-DOM assertions on the tab.
Common failures and precise fixes
No extension target appears
- Cause: The path is relative, misspelled or points to a ZIP file. Fix: resolve it with
Path.resolve(), print it, and confirm thatmanifest.jsonexists in that directory. - Cause: Pyppeteer still passed
--disable-extensions. Fix: include"ignoreDefaultArgs": ["--disable-extensions"]exactly in the launch options. - Cause: The manifest is invalid or incompatible with the browser. Fix: load the same directory through Chromium’s developer-mode “Load unpacked extension” flow and correct manifest errors before rerunning Pyppeteer.
The browser opens, but the extension does nothing
- Confirm that the target type matches the manifest:
background_pagefor MV2 orservice_workerfor MV3. - Print the target URL; it should begin with
chrome-extension://. - Use
dumpio: Trueinlaunch()to forward browser stderr while troubleshooting. - Check that every script path in the manifest exists and that your test is observing the correct tab or extension page.
The test hangs while waiting
A wait predicate that only checks service_worker will never finish for an MV2 extension, and the reverse is also true. Select the predicate from the manifest version, and include the chrome-extension:// URL check so unrelated browser targets do not satisfy it.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Headless mode fails but headed mode works
Treat this as a compatibility issue, not proof that the extension is malformed. Reproduce in headed Chromium, record the Pyppeteer and browser versions, and only move to headless after confirming that your chosen Chromium version supports the extension behavior you need.
Loading an unpacked extension is a local browser operation; Pyppeteer does not charge for it. Your practical costs are the browser startup time and whatever infrastructure runs the tests. If your goal is simply to obtain clean website screenshots rather than test an extension’s runtime behavior, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing status. See the full parameter list in the ScreenshotNeo documentation. A cURL request is: The equivalent Python call is: And Node.js: ScreenshotNeo also has an MCP server with The procedure described here is for an unpacked directory. Extract the extension so Chromium can read its Use the version your extension requires. The Pyppeteer verification step differs because MV2 exposes a background-page target while MV3 exposes a service-worker target. This workflow loads the extension at browser startup with Chromium flags. Runtime installation is a separate capability associated with newer Puppeteer APIs, not the launch-time Pyppeteer method shown here. Pyppeteer adds –disable-extensions by default. Remove that argument with ignoreDefaultArgs before supplying the load and disable-extensions-except flags. Inspect manifest.json: MV2 uses a background page, while MV3 declares a background service worker. Match your waitForTarget predicate to that target type. No, but it is the most conservative choice. Headless extension support varies by Chromium version, so validate the exact combination before using it in production tests. Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
finally block so failed tests do not leave Chromium processes running.Or skip the browser setup
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webpimport requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. Every feature is included on every plan: 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Sign up for the free ScreenshotNeo plan.Frequently asked questions
Can I load a packed CRX file?
manifest.json and referenced files directly.Should I use Manifest V2 or Manifest V3?
Can I install the extension after launch?
Frequently Asked Questions
Why does adding only –load-extension fail in Pyppeteer?
How do I know whether my extension is MV2 or MV3?
Is headed mode mandatory?
Quick Recap

