> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Debug a browser extension from the terminal

> Drive a live extension dev session from the terminal: read logs, inspect the DOM, eval in your extension's contexts, read chrome.storage, and reload on Chrome or Firefox.

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.

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/A8SxcA1fuwU?rel=0" title="Extension.js: extension eval" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

## Turn it on

Start a dev session with the control flags you need:

```bash theme={null}
# 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

| Command | What it does | Gate |
| - | - | - |
| `extension logs` | Stream logs from every context (SW, content, popup, options, sidebar, devtools) in one ordered timeline | n/a |
| `extension inspect` | Read the live, post-injection DOM of a surface | n/a |
| `extension storage get \| set` | Read or write your extension's `chrome.storage` | `--allow-control` |
| `extension reload` | Reload the extension or a tab | `--allow-control` |
| `extension open <popup\|options\|sidebar>` | Open an extension surface | `--allow-control` |
| `extension open action` | [Trigger the toolbar action](/docs/debugging/trigger-actions-and-commands): opens its popup, or replays `onClicked` | `--allow-control` |
| `extension open command --name <cmd>` | [Replay a keyboard-shortcut command](/docs/debugging/trigger-actions-and-commands) | `--allow-control` |
| `extension eval "<expr>" --context <c>` | Evaluate an expression in a context and return the value | `--allow-eval` |

`extension logs` and `extension inspect` have their own reference pages: [logs](/docs/commands/logs) and [inspect](/docs/commands/inspect).

### Shared flags

Every acting command accepts the same trio:

* `--browser` chooses the session to target (default `chromium`).
* `--timeout <ms>` bounds the round trip (default 5000).
* `--output <pretty|json>` chooses the stdout dialect.

When a session lacks the required unlock, the refusal names the exact flag to restart with: `--allow-eval` for `eval`, `--allow-control` for everything else. You never have to guess which gate you missed.

### What lands on disk

Beyond the live channel, the session writes append-only records under `dist/extension-js/<browser>/`: `logs.ndjson` holds every captured log event, and `actions.ndjson` (written when the session has `--allow-control`) audits the control actions that ran. Both are readable after the session ends.

The log contract also reserves structured `dx.signal` entries, machine-readable diagnostics about the runtime's own health, with `extension logs --signals-only` as their filter. No emitter ships yet, so the filter currently returns nothing. The shape is documented so consumers can branch on it the day the first signal lands.

## Address a context

Reading and acting share one vocabulary: you name the surface, Extension.js resolves it against the session it's already tracking.

| Address | Means |
| - | - |
| `--context background` | the service worker (or MV2 background page) |
| `--context popup` / `options` / `sidebar` | that extension surface, if it's open |
| `--context content --url "https://shop.example/*"` | the isolated content world on matching pages |
| `--context content --tab 7` | the content world in a specific tab |

## Example: did my extension actually change the page?

This runs on the default template as-is. `--context page` evaluates in the active tab's MAIN world, so you can check what your content script really did to the page:

```bash theme={null}
pnpm extension eval "document.title" --context page
```

```text theme={null}
Example Domain
```

Default output is the value itself: a string prints raw, anything else prints as indented JSON. Add `--output json` when a script is reading, and you get the full envelope instead:

```json theme={null}
{
  "schema": 1,
  "ok": true,
  "command": "eval",
  "status": "ok",
  "value": "Example Domain",
  "error": null,
  "warnings": []
}
```

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.

To call into the background instead, target a Firefox or MV2 session, where the background is a page that evaluates normally:

```bash theme={null}
pnpm extension eval "globalThis.__lastSyncAt" --context background --browser firefox
```

On Chromium MV3 (what the default template builds for Chrome) the background is a service worker, and Chrome's extension CSP blocks eval there. `--context background` returns an explanatory error instead of a value on those builds. Use `--context page` or `--context content` on Chromium MV3. The extension.dev MCP server applies the same default to its eval tool on Chromium MV3 sessions.

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

| Capability | Chromium | Firefox |
| - | - | - |
| `logs`, `inspect`, `storage`, `reload`, `open` (incl. `action`/`command`) | ✓ | ✓ (verified) |
| `eval` in `page` / `content` contexts | ✓ | ✓ |
| `eval --context background` | MV2 builds only (see footnote) | ✓ (MV2 background page) |

(`eval --context background` on a Chromium MV3 build returns an explanatory error. The MV3 background is a service worker, and Chrome's extension CSP rejects `unsafe-eval` there on every MV3 build, not only in production. Evaluate in `page`/`content` on Chromium MV3, or target the background on a Firefox/MV2 build.)

## Where logs live in the browser

`extension logs` merges every context into one terminal timeline (see [logs](/docs/commands/logs)). When you want the browser's own console for a context instead, each one lives behind a different door:

| Context | Chrome / Chromium | Firefox |
| - | - | - |
| Background | `chrome://extensions`, your extension, the **service worker** link under "Inspect views" | `about:debugging#/runtime/this-firefox`, then **Inspect** on your extension |
| Content scripts | DevTools on the page that they run in | DevTools on the page that they run in |
| Popup, options, sidebar | Right-click inside the surface, then **Inspect** | The same extension toolbox from `about:debugging` |

Three details save time:

* On Chrome, the **service worker** link also revives an idle MV3 worker, so use it when the background seems dead.
* Content script logs never reach the extension's own inspector. In the page DevTools console, the context dropdown filters to your extension's isolated world.
* On Firefox, the `about:debugging` toolbox covers the background and extension pages. Content script output stays in the page's DevTools.

Everything above also flows through `extension logs`, tagged by context, without clicking through any of those doors.

## 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, and a random local process can't quietly drive your service worker.
* **Web pages can't connect.** The control socket refuses a handshake that carries a web page origin (`http://`, `https://`, or `null`), so a site open in the browser can't reach it.
* **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

extension.dev, the platform that sponsors Extension.js, ships an MCP server that exposes these operations to an agent as tools. The gates are identical: an assistant observes freely but only acts when you have enabled it for the session. See [extension.dev MCP server](/docs/integrations/extension-mcp).

## Next steps

* [Trigger actions and keyboard commands](/docs/debugging/trigger-actions-and-commands): test handlers without clicking, headless and in CI.
* [Manifest refusals](/docs/debugging/manifest-refusals): why Chromium refuses an extension before the session can attach.
* [Chrome DevTools MCP](/docs/integrations/chrome-devtools-mcp): Google's Chrome debugging server beside an Extension.js session.
* [CI templates](/docs/workflows/ci-templates): wire these into a pull-request gate.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.