Launch Chrome with an unpacked extension and test its UI via CDP. Auto-installs Chrome for Testing if needed. Loads the extension, opens sidepanel/popup/options page, and hands off to cdp-connect for interaction (click, type, screenshot, ax-tree). Handles Chrome 137+ branded build restrictions (Extensions.loadUnpacked via pipe), sidepanel user gesture requirements, and React input quirks. Use when you need to test a Chrome extension's UI, automate extension interactions, or validate extension behavior on a target page. Triggers on: chrome extension test, test extension, load unpacked extension, extension sidepanel, extension popup, test chrome extension, extension testing, chrome extension automation, ext pilot, cdp extension.
100
100%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Launch Chrome with an unpacked extension, open its UI, interact via CDP.
Composes on cdp-connect — load that skill first for cdp.js commands.
# Locate cdp-ext-pilot.mjs
if [[ -n "${CLAUDE_SKILL_DIR:-}" ]]; then
EXT_PILOT="${CLAUDE_SKILL_DIR}/scripts/cdp-ext-pilot.mjs"
else
EXT_PILOT="$(command -v cdp-ext-pilot.mjs 2>/dev/null || \
find ~/.claude -path "*/cdp-ext-pilot/scripts/cdp-ext-pilot.mjs" -type f 2>/dev/null | head -1)"
fi
# Locate cdp.js (from cdp-connect skill)
if [[ -n "${CLAUDE_SKILL_DIR:-}" ]]; then
CDP_JS="$(find "$(dirname "${CLAUDE_SKILL_DIR}")" -path "*/cdp-connect/scripts/cdp.js" -type f 2>/dev/null | head -1)"
fi
CDP_JS="${CDP_JS:-$(command -v cdp.js 2>/dev/null || \
find ~/.claude -path "*/cdp-connect/scripts/cdp.js" -type f 2>/dev/null | head -1)}"node "$EXT_PILOT" launch <path-to-extension-dist> [--port 9222]Returns JSON with extensionId, port, chromeVariant. Auto-installs
Chrome for Testing if no suitable Chrome is found.
Verify: Confirm extensionId is non-null. If null: check the extension
path has a valid manifest.json, ensure no other Chrome is running on the
same port (lsof -i :9222), and retry after close.
node "$EXT_PILOT" open sidepanel [--port 9222] # Opens sidepanel, returns target ID
node "$EXT_PILOT" open popup [--port 9222] # Opens popup as tab
node "$EXT_PILOT" open options [--port 9222] # Opens options page as tabFor sidepanel: navigates to a page first if no page target exists.
Use cdp-connect commands with --id <target-id> from Phase 2:
node "$CDP_JS" ax-tree --id <target-id> # Understand the UI
node "$CDP_JS" screenshot /tmp/ext.png --id <tid> # Visual check
node "$CDP_JS" click "button" --id <tid> # Click elements
node "$CDP_JS" type "input" "text" --id <tid> # Type into fields
node "$CDP_JS" eval "expression" --id <tid> # Run JSnode "$EXT_PILOT" status [--port 9222] # Check session state
node "$EXT_PILOT" close [--port 9222] # Kill Chrome, remove profilecdp.js type sets DOM .value which does not trigger
React state updates. For React-controlled inputs, focus the element first
with cdp.js eval "document.querySelector('input').focus()", then use
a CDP Input.insertText call via eval to type character by character.page context,
not popup. Extension code using chrome.extension.getViews({type: "popup"}) will see different results.open),
not the page target — they are separate CDP targets.cdp-connect on the page
target. Use Runtime.enable to find the extension's execution context.page-prep skill to dismiss overlays before
testing extension behavior on a page.launch fails with a connection error, another
Chrome is running on that port. Run close first, or use --port <other>
to pick a different port.manifest.json (not a parent dir). Check status output for
chromeVariant — branded Chrome 137+ uses the pipe dance which requires
--enable-unsafe-extension-debugging (handled automatically).