Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.json and 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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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/path limits enabled extensions to the directory you specify.
  • --load-extension=/absolute/path loads 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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 that manifest.json exists 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_page for MV2 or service_worker for MV3.
  • Print the target URL; it should begin with chrome-extension://.
  • Use dumpio: True in launch() 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Headless 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.

Reliability and performance practices

  • Resolve and validate the extension path once, before launching the browser.
  • Use one deterministic browser configuration in local runs and CI; changing between bundled and system Chromium can change extension behavior.
  • Wait for the relevant target rather than inserting long global sleeps. This shortens successful runs and makes failures explicit.
  • Close the browser in a finally block so failed tests do not leave Chromium processes running.
  • Keep a small smoke test that launches Chromium, finds the expected target type, and evaluates a harmless expression in the worker or background page.

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.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

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:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The equivalent Python call is:

import 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)

And Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also has an MCP server with 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently asked questions

Can I load a packed CRX file?

The procedure described here is for an unpacked directory. Extract the extension so Chromium can read its manifest.json and referenced files directly.

Should I use Manifest V2 or Manifest V3?

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.

Can I install the extension after launch?

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.

Frequently Asked Questions

Why does adding only –load-extension fail in Pyppeteer?

Pyppeteer adds –disable-extensions by default. Remove that argument with ignoreDefaultArgs before supplying the load and disable-extensions-except flags.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I know whether my extension is MV2 or MV3?

Inspect manifest.json: MV2 uses a background page, while MV3 declares a background service worker. Match your waitForTarget predicate to that target type.

Is headed mode mandatory?

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.