Skip to main content
A browser extension is hard to debug because the interesting state lives in places you can’t easily reach: an MV3 service worker that goes idle, an isolated content-script world, a popup that closes the moment it loses focus. Extension.js opens a small local control channel into your running dev session so you (or an AI agent, or a CI job) can reach those contexts directly — read what they logged, inspect what they rendered, call into them, and fire the events a user would. It’s local, it needs no account, and observation is always free. Anything that changes state is opt-in per session.

Turn it on

Start a dev session with the control flags you need:
# Read-only is always on. Add control to act on the extension:
pnpm extension dev --allow-control

# Add eval to also run arbitrary expressions in a context:
pnpm extension dev --allow-control --allow-eval
Every operation below targets that session and addresses a context the same way, whether you’re reading or acting.

What you can do

CommandWhat it doesGate
extension logsStream logs from every context (SW, content, popup, options, sidebar, devtools) in one ordered timeline
extension inspectRead the live, post-injection DOM of a surface
extension storage get | setRead or write your extension’s chrome.storage--allow-control
extension reloadReload the extension or a tab--allow-control
extension open <popup|options|sidebar>Open an extension surface--allow-control
extension open actionTrigger the toolbar action — opens its popup, or replays onClicked--allow-control
extension open command --name <cmd>Replay a keyboard-shortcut command--allow-control
extension eval "<expr>" --context <c>Evaluate an expression in a context and return the value--allow-eval

Address a context

Reading and acting share one vocabulary — you name the surface, Extension.js resolves it against the session it’s already tracking.
AddressMeans
--context backgroundthe service worker (or MV2 background page)
--context popup / options / sidebarthat extension surface, if it’s open
--context content --url "https://shop.example/*"the isolated content world on matching pages
--context content --tab 7the content world in a specific tab

Example: did my background handler actually run?

pnpm extension eval "globalThis.__lastSyncAt" --context background --allow-eval
{ "ok": true, "value": 1718000000000 }
Any console.log your expression triggers flows out through extension logs at the same time, correlated by sequence — so you see the return value and the side effects.

Cross-browser support

Extension.js debugs through an in-browser companion, not the Chrome DevTools Protocol, so the core loop reaches your own surfaces on both Chrome and Firefox — the old “Firefox uses RDP, not supported” wall is gone for these tools.
CapabilityChromiumFirefox
logs, inspect, eval, storage, reload, open (incl. action/command)✓ (verified)
inspect --deep-dom (closed shadow roots)— (uses CDP)
extension_list_extensions (MCP tool)— (uses CDP)
(extension_list_extensions is an MCP tool rather than a CLI verb — it connects over the DevTools Protocol, so it’s Chromium-only.)

Safety

The gates are intentional, not bureaucratic:
  • Observation needs nothing. Reading logs and DOM is always available.
  • Bounded operations need --allow-control. storage, reload, and open change state, so you opt in per session.
  • eval needs --allow-eval and a per-session token. The token is written to a 0600 file outside dist/ so it never ships in a build — a random local process can’t quietly drive your service worker.
  • Nothing reaches production. The control channel exists only during dev/preview; it’s gated on a port that isn’t present in a built bundle.

With an AI agent

The same operations are exposed as MCP tools through @extension.dev/mcp (extension_logs, extension_eval, extension_storage, extension_reload, extension_open, extension_list_extensions). The gates are identical — an assistant observes freely but only acts when you’ve enabled it for the session.

Next steps