How to Run Puppeteer Inside Chrome for Hybrid Browser Automation
Run Puppeteer in a Chrome extension with ExtensionTransport, or use Node.js to control Chrome. Learn the setup, limits, troubleshooting, and when to use each.
Direct answer: If “Puppeteer inside Chrome” means automation code running in a Chrome extension, bundle Puppeteer’s browser-compatible puppeteer-core entry point and connect it to a tab with ExtensionTransport. That connection controls one tab and requires the extension’s debugger permission; this support is experimental. If you mean Puppeteer controlling Chrome from a Node.js process, use the standard Puppeteer launch or connect APIs instead. These are different architectures with different permissions and scope.
Choose the right architecture
| Approach | Where Puppeteer runs | Scope | Use it for |
|---|---|---|---|
| Extension-side Puppeteer | Extension-compatible JavaScript | One tab per connection, through chrome.debugger |
Automation initiated by the extension itself |
| Node.js Puppeteer | Node.js process | Normal browser launch or connection workflow | Scripts, test runners, or remote browser automation |
| Node.js testing an extension | Node.js process | Chrome plus extension targets | End-to-end testing of extension behavior |
Do not treat an extension transport as a full-browser session. The extension workflow attaches to a particular tab; use Chrome’s tabs API to create additional tabs and establish a separate connection for each. Chrome exposes DevTools Protocol access to extensions through chrome.debugger, which is a restricted transport and does not expose every protocol domain. See the [Puppeteer extension guide](https://pptr.dev/guides/running-puppeteer-in-chrome-extensions) and [Chrome debugger API](https://developer.chrome.com/docs/extensions/reference/api/debugger).
Run Puppeteer from a Chrome extension
1. Add the debugger permission
Declare debugger in the extension manifest. Chrome identifies this as a permission that triggers a warning, so account for the user-facing permission prompt and the trust implications of your extension.
{
"manifest_version": 3,
"name": "Puppeteer tab automation",
"version": "1.0.0",
"permissions": ["debugger", "tabs"],
"background": {
"service_worker": "background.js",
"type": "module"
}
}
The tabs permission is included here for an extension that needs access to tab details beyond the limited information available without it. If the extension only creates a tab and uses its returned ID, review Chrome’s [tabs permission documentation](https://developer.chrome.com/docs/extensions/reference/api/tabs) and request only the access its design needs. Host permissions may also be needed for extension operations that directly access particular sites; they do not replace the debugger permission.
2. Bundle the browser-compatible Puppeteer entry point
Puppeteer’s documented extension workflow uses a bundler such as Rollup or webpack and imports from puppeteer-core/lib/puppeteer/puppeteer-core-browser.js. This is an extension bundle entry point, not a Node.js import recipe. Install puppeteer-core in your development project and configure your bundler to emit a module usable by the extension service worker.
npm install puppeteer-core
3. Connect to a tab and automate it
For example, in an extension service worker bundled with the browser entry point:
import {
connect,
ExtensionTransport,
} from 'puppeteer-core/lib/puppeteer/puppeteer-core-browser.js';
async function inspectExampleTab() {
const tab = await chrome.tabs.create({ url: 'https://example.com' });
if (tab.id === undefined) {
throw new Error('Chrome did not return a tab ID');
}
const transport = await ExtensionTransport.connectTab(tab.id);
const browser = await connect({ transport });
try {
const [page] = await browser.pages();
if (!page) throw new Error('No page is attached to the tab');
await page.locator('body').wait();
console.log('Attached to the example tab');
} finally {
await browser.disconnect();
}
}
inspectExampleTab().catch((error) => {
console.error('Tab automation failed:', error);
});
The finally block disconnects Puppeteer’s client after the task. It does not close the Chrome tab. The example is a setup pattern; adapt tab selection, lifecycle, and error reporting to your extension. See the official [guide to running Puppeteer in Chrome extensions](https://pptr.dev/guides/running-puppeteer-in-chrome-extensions) for the supported API and current limitations.
Automate more than one tab
The connected Puppeteer browser object corresponds to the attached tab, and this connection cannot create additional pages through Puppeteer. Create another tab with chrome.tabs, then connect to its ID separately:
async function connectToNewTab(url) {
const tab = await chrome.tabs.create({ url });
if (tab.id === undefined) throw new Error('Tab ID unavailable');
const transport = await ExtensionTransport.connectTab(tab.id);
return connect({ transport });
}
const reportsTab = await connectToNewTab('https://example.com/reports');
const settingsTab = await connectToNewTab('https://example.com/settings');
try {
const [reportsPage] = await reportsTab.pages();
const [settingsPage] = await settingsTab.pages();
// Automate each attached tab through its own page and connection.
} finally {
await Promise.all([reportsTab.disconnect(), settingsTab.disconnect()]);
}
Track each connection against its tab and disconnect it when the task ends or the tab is removed. Extension service worker suspension and restart are part of the lifecycle you need to handle; do not assume an in-memory Puppeteer connection will survive them.
Use Node.js to launch or connect to Chrome
For conventional automation, Puppeteer in Node.js is generally the simpler choice. The puppeteer package downloads a compatible Chrome for Testing build. puppeteer-core does not download a browser and is intended when you manage the browser installation yourself or connect to a remote browser. The [Puppeteer getting started guide](https://pptr.dev/guides/getting-started) and [supported browsers guide](https://pptr.dev/supported-browsers) describe the current setup.
Runnable Node.js launch example
npm install puppeteer
// save as capture.mjs
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'example.png', fullPage: true });
} finally {
await browser.close();
}
node capture.mjs
Choose a navigation readiness condition deliberately. networkidle2 can be useful for pages that make a small number of continuing requests, but analytics, streaming, and long polling can prevent network idle from becoming a useful signal. For dynamic pages, wait for a selector that represents the content you need, with a timeout, rather than assuming navigation completion means the page is ready.
Connect to a separately managed Chrome
With puppeteer-core, provide the executable path for a local browser or the WebSocket endpoint provided by a remote browser service. Do not assume arbitrary local Chrome versions match your Puppeteer release; consult the supported version mapping.
npm install puppeteer-core
// save as connect.mjs
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
executablePath: process.env.CHROME_PATH,
headless: true,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
await browser.close();
}
For a remote browser, use the endpoint and authentication method specified by that browser’s operator, then connect with Puppeteer’s documented connect API. Keep credentials out of source code and logs. Runtime requirements change; the current Puppeteer documentation lists Node.js 22.12 or newer, so verify the [system requirements](https://pptr.dev/guides/system-requirements) when you set up or upgrade a project.
Test a Chrome extension with Puppeteer
This is a third workflow: Puppeteer runs in Node.js and launches Chrome with an extension enabled. Puppeteer documents enableExtensions and ways to inspect extension targets such as a Manifest V3 service worker, Manifest V2 background page, popup, or content-script realm. In this setup Puppeteer itself is not executing inside the extension.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
enableExtensions: ['/absolute/path/to/extension'],
});
try {
const targets = browser.targets();
const extensionTargets = targets.filter((target) =>
target.type() === 'service_worker' &&
target.url().startsWith('chrome-extension://')
);
console.log(extensionTargets.map((target) => target.url()));
} finally {
await browser.close();
}
Extension launch and target APIs are version-sensitive. Follow the current [Puppeteer Chrome Extensions guide](https://pptr.dev/guides/chrome-extensions) and ensure the extension directory and browser mode match its instructions.
Configuration and practical limits
- Browser version: use Puppeteer’s documented Chrome for Testing pairing. The package version and browser version matter, especially with
puppeteer-coreand a separately installed Chrome. - Headless mode: current Puppeteer uses the Chrome for Testing code path for headless and headful operation; the older
chrome-headless-shellis a separate legacy implementation. Confirm which mode your project needs. - Extension permissions: extension-side control requires
debugger. Expect Chrome’s permission warning and design for the constrained CDP access. - Connection scope: extension transport attaches to one tab. Open tabs through Chrome APIs and make a separate connection per tab.
- Environment: extension browser code and Node.js packages run in different environments. A successful Node.js script does not prove the bundled extension build or its lifecycle will work.
- Readiness: use a selector or explicit application condition where possible. Fixed sleeps are easy to implement but can be slow on fast pages and insufficient on slow ones.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| “Cannot find module” for the browser entry point | The extension bundle is importing the Node entry point, or the package/bundler does not include the browser entry point. | Use the documented puppeteer-core/lib/puppeteer/puppeteer-core-browser.js import and bundle it for the extension environment. |
| Debugger permission error or connection rejected | The manifest lacks debugger, Chrome has not granted it, the tab ID is invalid, or the tab has gone away. |
Declare and grant the permission, check that the tab still exists, and handle tab removal or connection failure. |
| Expected additional page is missing | The extension transport represents one tab and does not provide normal page creation. | Create tabs with chrome.tabs.create, then call ExtensionTransport.connectTab for each tab ID. |
| Some Puppeteer/CDP operation is unsupported | chrome.debugger exposes a restricted set of protocol domains; extension support is experimental. |
Check the Chrome debugger API and Puppeteer extension guide for support. Move the operation to Node.js Puppeteer if it needs normal browser-level control. |
| Local Chrome fails to launch or behaves unexpectedly | The browser version may not match Puppeteer, the executable path may be wrong, or system requirements may be unmet. | Prefer puppeteer with its compatible downloaded browser, or verify the executable path and documented version/platform requirements for puppeteer-core. |
| Navigation succeeds but screenshot or selector is empty | The page may render content asynchronously, require authentication, or never reach the selected network-idle condition. | Wait for a meaningful content selector, inspect navigation errors and page state, and set a bounded timeout. |
| Connection disappears after extension background work pauses | A Manifest V3 service worker can be stopped and restarted by Chrome. | Design the operation around extension lifecycle events, reconnect when needed, and avoid relying on in-memory state across worker restarts. |
Performance, reliability, and cost
Extension-side automation avoids handing the task to a separate Node.js controller, but it also inherits extension lifecycle, permission, and single-tab transport constraints. Node.js automation gives the script direct launch and connection control and is easier to use for multi-page jobs; browser startup and page rendering still consume local or hosted compute. Reuse a browser for a batch of pages when the process model permits it, close pages and browsers when finished, and put timeouts around navigation and waits so one stalled site does not hold a job indefinitely.
There are no general performance figures that apply across sites and machines. Rendering cost depends on the target page, viewport, assets, and wait condition. If you need a screenshot without maintaining browser binaries, extension permissions, and capture code, ScreenshotNeo provides a screenshot API and MCP server. Its per-request cost depends on the plan; the listed plans include 1,000 shots per month free, then Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. See the [ScreenshotNeo documentation](https://screenshotneo.com/docs/) for request options.
Or skip the browser setup
For a direct website screenshot, call the ScreenshotNeo API with a URL. Full details and parameters are in the [ScreenshotNeo API docs](https://screenshotneo.com/docs/).
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
Frequently asked questions
Can Puppeteer run directly in a Chrome extension?
Yes, through Puppeteer’s experimental browser-compatible build and ExtensionTransport, with the extension’s debugger permission. The connection is scoped to one tab.
Can extension-side Puppeteer create a new page?
Not through that connection. Create another tab with chrome.tabs and connect to it separately.
Should I use puppeteer or puppeteer-core?
Use puppeteer when you want Puppeteer to download its compatible Chrome for Testing browser. Use puppeteer-core when you manage the browser or connect to a remote one, and for the extension browser entry point.
Does launching Chrome with an extension mean Puppeteer runs inside that extension?
No. In the extension testing workflow, Puppeteer runs in Node.js and controls Chrome, while the extension runs in Chrome.


