Chrome extensions
Load unpacked extensions at launch with extensions. Manifest V2 and V3 both run, headed and headless, on Windows and Linux.
Load one
Pass a list of unpacked extension directories — each a folder containing a manifest.json. Extensions require a persistent context, because an extension needs a profile to live in.
from clearcote import launch_persistent_context
ctx = launch_persistent_context(
"./profile",
extensions=["./ext/ublock", "./ext/my-helper"], # unpacked directories
)
page = ctx.pages()[0] if ctx.pages() else ctx.new_page()
page.goto("https://example.com")import { launchPersistentContext } from "clearcote";
const ctx = await launchPersistentContext("./profile", {
extensions: ["./ext/ublock", "./ext/my-helper"],
});The SDK sets both --load-extension and --disable-extensions-except for you. That pairing matters: on its own, --load-extension is ignored under automation, which is the usual reason an extension silently fails to appear.
Getting an unpacked directory
Clearcote loads folders, not .crx files. A .crx is a ZIP with a signature header, so unzipping it gives you a loadable directory:
unzip extension.crx -d ./ext/extensionThe Chrome Web Store is not reachable from Clearcote — the upstream privacy patches remove the Web Store integration, so there is no in-browser install flow. Ship the unpacked directory with your automation instead. This is the same model Playwright and Puppeteer use.
Manifest V2 still works
Stock Chrome 151 has finished disabling Manifest V2. Clearcote still runs MV2 extensions, because the upstream privacy patch set restores that support — so MV2-only content blockers and older in-house tooling keep working here after they have stopped working in Chrome.
# both of these load and run
ctx = launch_persistent_context("./profile", extensions=["./ext/mv2-tool", "./ext/mv3-tool"])Headless
Extensions load in headless mode as well as headed. No extra flags, and no need to run a display server just to get an extension working.
ctx = launch_persistent_context("./profile", extensions=["./ext/my-helper"], headless=True)Fingerprint considerations
An extension is your code running in your profile, and Clearcote does not hide it. Two things follow, and both are worth deciding deliberately rather than discovering later.
- Extensions are observable. A content script that rewrites the DOM, injects styles, or blocks requests changes what a page measures. A site comparing its own markup against what it served can tell something modified the page. That is true of every browser with extensions and is not a Clearcote-specific signal, but it is a signal.
- Web-accessible resources are probeable. If an extension declares
web_accessible_resources, any page can attempt to fetchchrome-extension://<id>/<file>and learn the extension is present. Prefer extensions that declare none, and keepuse_dynamic_urlin mind for those that must.
If your goal is blocking requests rather than shipping UI, consider doing it in the automation layer — route interception costs no extension surface at all. See Recommended settings for the general principle: only spoof what you need, and only add surface that earns its place.
Troubleshooting
- Nothing happens. Check the path points at the folder holding
manifest.json, not at its parent and not at a.crx. - The content script never runs. Confirm the
matchespattern covers the URL you are visiting —http://localhost/*does not matchhttp://127.0.0.1:8080/. - It works headed but not headless. That should not happen on this build; both are tested. If you hit it, please open an issue with the manifest.