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

# Dev command for watch mode and live reload

> Develop browser extensions with watch mode, hot module replacement, automatic browser launch, and context-aware reload via the Extension.js dev command.

Use `dev` for day-to-day browser extension development with watch mode, browser launch, and context-aware update behavior.

`dev` runs the development pipeline and watches your project files. It applies update strategies based on what changed: hot module replacement (HMR), hard reload, or a full restart when the change requires it.

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

## When to use `dev`

* Building features and validating changes in real time.
* Debugging extension behavior in one or more browser targets.

Use `build` for production artifacts, `start` for production build + launch, and `preview` to run existing build output only.

If your extension lives inside a monorepo/submodule, review how `extension.config.*` loads env files (including workspace-root fallback): [Environment variables](/docs/features/environment-variables#how-it-works).

## Dev command capabilities

| Capability | What it gives you |
| - | - |
| Watch mode iteration | Tight edit → rebuild → validate loop while coding |
| Browser-target control | Explicit cross-browser validation per command |
| Profile-aware runs | Reliable fresh or persisted profiles by workflow need |

## Usage

<CodeGroup>
  ```bash npm theme={null}
  extension dev [path-or-url] [options]
  ```

  ```bash pnpm theme={null}
  extension dev [path-or-url] [options]
  ```

  ```bash yarn theme={null}
  extension dev [path-or-url] [options]
  ```

  ```bash bun theme={null}
  extension dev [path-or-url] [options]
  ```

  ```bash deno theme={null}
  extension dev [path-or-url] [options]
  ```
</CodeGroup>

If you omit the path, Extension.js uses the current working folder. You can also pass a **GitHub tree URL** (for example, `https://github.com/user/repo/tree/main/path`). Extension.js downloads the repository and runs development mode on the local copy.

## Most-used flags

These cover the 80% case. Skip to the [full reference](#arguments-and-flags) for the rest.

| Flag | What it does | Default |
| - | - | - |
| `--browser <target>` | Target Chrome, Edge, Firefox, or comma-separated list. | `chromium` |
| `--polyfill` | Bridge `browser.*` API to Chromium targets for Firefox-flavored sources. | `true` |
| `--port <port>` | Dev server port. Use `0` for OS-assigned. | `8080` |
| `--starting-url <url>` | Open this URL when the browser launches. | unset |
| `--no-reload` | Skip auto-reload. Use when you want a clean dev bundle. | reload on |

## Arguments and flags

| Flag | Alias | What it does | Default |
| - | - | - | - |
| `[path or url]` | - | Extension path or remote URL. | `process.cwd()` |
| `--browser <browser>` | `-b` | Browser/engine target (`chromium`, `chrome`, `edge`, `firefox`, named forks like `brave`/`waterfox`, engine aliases, or comma-separated values). | `chromium` |
| `--profile <path\|boolean>` | - | Browser profile path or boolean profile mode. | fresh profile |
| `--chromium-binary <path>` | - | [Custom Chromium-family binary path](/docs/browsers/running-other-browsers). | system default |
| `--gecko-binary <path>` | `--firefox-binary` | [Custom Gecko-family binary path](/docs/browsers/running-other-browsers). | system default |
| `--polyfill [boolean]` | - | Enable `browser.*` API compatibility polyfill for Chromium targets. | `true` |
| `--no-polyfill` | - | Disable the cross-browser polyfill. | polyfill enabled |
| `--starting-url <url>` | - | Starting URL in launched browser. | unset |
| `--port <port>` | - | Requested dev server port. Use `0` for an OS-assigned port. See [how the port resolves](#how-the-port-resolves). | `8080` |
| `--host <host>` | - | Host to bind the dev server to. Use `0.0.0.0` for Docker/dev containers. | `127.0.0.1` |
| `--public-host <host>` | - | Connectable host the browser dials for HMR and the reload bridge, when it differs from the bind `--host` (remote/dev container). | bind host (`127.0.0.1` when bound to `0.0.0.0`) |
| `--allowed-hosts <list>` | - | Comma-separated host names the dev server answers besides localhost, IP addresses and `--public-host` (docker service names, tunnels, `*.local`). A leading dot allows every subdomain. Also `commands.dev.allowedHosts` in `extension.config.js`. See [bind host vs. connectable host](#bind-host-vs-connectable-host). | none |
| `--no-open` | - | Launch the browser, but do not open a tab for your extension. The browser still starts and loads it. See [stop the browser from launching](#stop-the-browser-from-launching). | a tab opens on launch |
| `--no-browser` | - | Stop the browser from launching. The dev server still starts and keeps rebuilding. See [stop the browser from launching](#stop-the-browser-from-launching). | browser launch enabled |
| `--no-reload` | - | Skip the content-script reload runtime and on-rebuild reload dispatch. Reload tabs manually to see changes. | reload runtime enabled |
| `--wait [boolean]` | - | Wait for `dist/extension-js/<browser>/ready.json` and exit. Requires a local project path. | disabled |
| `--wait-timeout <ms>` | - | Timeout for `--wait` mode. | `60000` |
| `--output <pretty\|json>` | - | Result format. `json` prints a schema-1 envelope on stdout. | `pretty` |
| `--extensions <list>` | - | Comma-separated companion extensions or store URLs. | unset |
| `--install [boolean]` | - | Internal flag. Install project dependencies when missing. Lifecycle scripts stay disabled unless `EXTENSION_ALLOW_INSTALL_SCRIPTS=true`. | command behavior default |
| `--allow-control` | - | Enable the agent-bridge control channel for the bounded act verbs (`reload`, `storage`, `open`). | disabled |
| `--allow-eval` | - | Also enable `extension eval`. Implies `--allow-control` and writes a `0600` session token. | disabled |
| `--parent-pid <pid>` | - | Exit the dev server as soon as the given process ID dies. See [parent watchdog](#parent-watchdog). | unset |
| `--debug` | - | Enable maintainer diagnostics. | disabled |

Two deprecated aliases are hidden from `--help` but still work:

* `--wait-format <pretty|json>` maps onto `--output` and warns once on stderr. Migrate scripts to `--output`.
* `--author` and `--author-mode` map onto `--debug`.

### Safari flags

These flags apply to `safari` and `webkit-based` targets only. Passing any of them with another target exits with `E_INVALID_OPTION`, so a typo never no-ops silently.

| Flag | What it does | Default |
| - | - | - |
| `--safari-binary <path>` | Safari binary to open after packaging. | system Safari |
| `--app-name <name>` | Override the Safari app name. | the manifest `name` |
| `--bundle-id <id>` | User-owned bundle identifier in reverse-DNS form. A malformed value fails before any build. | a generated `dev.extensionjs.*` id |
| `--macos-only [boolean]` | Generate a macOS-only Xcode project. Pass `false` for a universal macOS + iOS project. | `true` |
| `--force-regenerate` | Regenerate the Safari Xcode project even when up to date. | disabled |

For Safari targets, `dev` also runs a toolchain preflight before the first bundle. A missing Xcode fails fast with `E_SAFARI_TOOLCHAIN`.

From 4.1.20, a Safari session is a full dev session. Once you enable the extension in Safari Settings, `extension logs` reads it, the control bridge attaches, and every save reloads the extension in Safari. See [Building Safari extensions](/docs/browsers/safari).

### Parent watchdog

`--parent-pid` is for harnesses and agents that spawn `dev`, so a crashed owner cannot leak a server. The value must be a positive integer, anything else exits with `E_INVALID_OPTION`. The watchdog polls the parent process every 2 seconds. When the parent is gone, the dev server shuts down via `SIGTERM`, with a 5 second hard-exit backstop if cleanup wedges.

### How the port resolves

`--port` is a request, not a guarantee. When the requested port is busy, the dev server walks upward to the nearest free port. `--port 0` asks the OS for any free port. Read the bound port from `ready.json`, not from the flag you passed.

## Automation metadata (recommended for scripts/agents)

When `dev` runs, Extension.js emits machine-readable metadata under:

* `dist/extension-js/<browser>/ready.json`
* `dist/extension-js/<browser>/events.ndjson` (newline-delimited JSON)

For automation (Playwright, continuous integration (CI), AI agents), prefer these files over terminal log parsing.

Treat `ready.json` as the readiness contract:

* `status: "starting"` while booting
* `status: "ready"` when the build compiled
* `status: "error"` for startup/compile failures, and for a browser that never started (`code: "browser_launch_failed"`) or exited before the extension loaded (`code: "browser_exited"`)
* `status: "stopped"` after the session shut down, so a dead session never advertises `ready`
* `runtime: "attached"` (with `executorAttachedAt`) once the service worker connected. Act verbs should wait for this, not for `ready`
* `runtime: "detached"` (with `executorDetachedAt`) from 4.1.20, once the last connected extension context disconnects. `executorAttachedAt` stays as provenance, and the value goes back to `"attached"` on reconnect
* `runId` uniquely identifies a runtime session
* `startedAt` marks the runtime session start timestamp
* `command` names the producing command (`dev`, `start`, `preview`, or `build`)
* `toolchainVersion`, `extensionName`, and `extensionVersion` record which Extension.js version produced the tree, for which extension. The file doubles as a build receipt after the terminal scrollback is gone
* `port` is the bound dev server port, and `host` is the connectable host clients dial (see [bind host vs. connectable host](#bind-host-vs-connectable-host))
* `controlPort` / `instanceId` locate the control bridge used by `extension logs` and the act verbs
* `cdpPort` (Chromium) and `rdpPort` (Gecko) expose the browser debugging ports
* `profilePath`, `browserPid`, and `extensionId` are stamped by the browser launcher after launch

The full schema, error states, and two-phase readiness rules live in [the ready.json contract](/docs/contracts/ready-json).

`events.ndjson` is scoped to the current run: starting a new run resets the file, and every entry is stamped with the run's `runId` (matching `ready.json`), so consumers never see events from a previous session interleaved with the live one.

When a session misbehaves, run [`extension doctor`](/docs/commands/doctor): it walks the contract, control channel, token, executor, and browser in order and names the first failing leg with a fix.

### Stop the browser from launching

Two flags sound alike and do different things:

| You want | Flag | What happens |
| - | - | - |
| No browser at all | `--no-browser` | No browser process starts. The dev server runs and keeps rebuilding on save. |
| A browser, but no tab opened for your extension | `--no-open` | The browser starts and loads your extension. No tab is opened for it. |

```bash theme={null}
# Stop the browser launch. The dev server keeps running.
extension dev --no-browser

# Launch the browser without opening a tab for the extension.
extension dev --no-open
```

`--no-browser` is the run mode for headless, CI, and remote work, and it is the flag the [Playwright end-to-end workflow](/docs/workflows/playwright-e2e) is built on. It also has a config form, `commands.dev.noBrowser: true`, and an environment form, `EXTENSION_CLI_NO_BROWSER=1`.

`--no-open` is a launch detail. Reach for it when you want the browser open on whatever it already shows, without Extension.js opening a tab for your extension over it. `dev`, `start`, and `preview` all accept both flags.

### `--no-browser` and readiness synchronization

`--no-browser` disables browser launch but keeps the full dev loop. The dev server still watches your files, and on each rebuild it broadcasts a reload over the control bridge to the extension's service worker so your changes apply without a launched browser driving them:

* A **content-script** change is re-injected into the already-open matching tabs in place (the service worker runs `chrome.scripting.executeScript` with the fresh build), so the page updates on save without a manual refresh. Tabs opened afterward get the new build too, because the service worker re-registers the content scripts dynamically (`chrome.scripting.registerContentScripts`).
* A **service-worker / manifest** change restarts the extension.

So `--no-browser` behaves like a normal `dev` session for headless, continuous integration (CI), and remote/dev-container workflows: load the built `dist/<browser>` into any browser you control and it keeps updating on save. (Use `--no-reload` for a static dev bundle that never reloads. See below.)

`--no-browser` does not block external runners until the compile finishes.

For Playwright/CI/AI workflows:

1. Run `extension dev --no-browser` as a long-lived process.
2. Run `extension dev --wait --browser=<browser>` as the readiness gate.
3. Launch external browser automation only after `status: "ready"`.

`--wait` targets a second process (or CI step) and exits non-zero on `error`/timeout.
When `--wait` sees a stale `ready.json` from a dead process (`pid` no longer alive), it keeps waiting for a live producer.
`--wait` requires a local project path. Passing a remote URL exits with `E_ARGS`.
Passing both `--wait` and `--no-browser` in the same command invocation exits with `E_INVALID_OPTION`, because `--wait` only reads a contract that another process writes. Run them as two processes, as in the steps above.

From 4.1.21, a `--wait` that reads `ready.json` while the producer is still writing it treats the half-written file as one more transient state and keeps polling. Up to 4.1.20, one torn read failed the wait with a JSON parse error. A file that never parses still ends in `E_READY_TIMEOUT`, and the timeout message names the cause: `The last read of the file failed to parse as JSON`, followed by the parser's own error.

### Machine output with `--output json`

`--output json` prints a schema-1 envelope on stdout:

* A plain `dev` run prints one `status: "started"` frame as the first stdout line, once the session exists and right before the `starting` frame. It carries the project path, browser list, requested port, and the dev server `pid`. `dev` never terminates on its own, so no result frame follows. The [lifecycle frames](/docs/contracts/lifecycle-stream) (`starting`, `compiled`, `ready`, and the ones after) follow on stdout, and human progress lines go to stderr, so every stdout line parses as JSON. Read `ready.json` for the live state.
* A run refused before the session exists prints one `ok: false` frame and nothing else, with no `started` frame before it. That covers a missing manifest (`E_MANIFEST_NOT_FOUND`), a manifest that does not parse (`E_MANIFEST_INVALID`), a config file that fails to load (`E_CONFIG_LOAD`), and a remote URL that gives no usable archive (`E_REMOTE_ZIP_INVALID`, `E_REMOTE_FETCH_TIMEOUT`, or `E_REMOTE_DOWNLOAD`).
* A bad `--chromium-binary` or `--gecko-binary` pin does not stop the server. That covers a path that does not exist, a file that is not executable, and a binary that does not answer its version probe within 10 seconds. The contract goes to `error` with code `browser_launch_failed` and `browserLaunchFailedCode: "E_BROWSER_BINARY_INVALID"`. The failure frame carries that `readyCode` with `status: "usage"` and `error.code: "E_BROWSER_BINARY_INVALID"`, and a `dev --wait` against the session answers with the same status and code.
* A pinned binary that is executable but that the system cannot start also keeps the server up. Its failure frame has `status: "failed"`, `error.code: "E_READY_ERROR_STATUS"`, and the `browser_launch_failed` `readyCode`.
* A `dev --wait` run prints one `status: "ready"` frame on success. Its `value.results` array carries the full ready contract per browser.
* Failures print one `ok: false` frame with an `error.code` (for example `E_READY_TIMEOUT`) before the process exits `1`. A contract in `error` status names its cause: `E_BROWSER_BINARY_INVALID` with `status: "usage"` when the pinned binary was refused, `E_BROWSER_LAUNCH` when the browser never started, `E_BROWSER_EXITED` when it exited before the extension loaded, and `E_COMPILE` for a compile error. See [the ready.json contract](/docs/contracts/ready-json) for the full list.

### `--no-reload` for a clean dev bundle

`--no-reload` skips the content-script reinjection wrapper and the on-rebuild reload dispatch. The dev `dist` stays close to a production bundle and an open tab is not disturbed when files change. Reload the extension or page yourself to pick up changes.

`--no-reload` is only supported on `extension dev`. Passing it to `start`, `preview`, or `build` exits with an error. Internally it sets `EXTENSION_NO_RELOAD=true` so the develop process can read it from outside the CLI argv.

Dev builds emit `cheap-module-source-map` files that describe your source: the original TypeScript and the exact lines, for content scripts, classic multi-file groups, the background, and pages on both manifest versions. No `eval` variant is used, so the bundle runs under your own CSP.

## Logging flags

These flags are experimental and may change between minor releases.

| Flag | What it does | Default |
| - | - | - |
| `--logs <off\|error\|warn\|info\|debug\|trace\|all>` | Minimum log level. | `off` |
| `--log-context <list\|all>` | Context filter (`background`, `content`, `page`, `popup`, `options`, `sidebar`, `devtools`, `newtab`, `history`, `bookmarks`). | `all` |
| `--log-format <pretty\|json\|ndjson>` | Logger output format. | `pretty` |
| `--no-log-timestamps` | Disable timestamps in pretty mode. | timestamps enabled |
| `--no-log-color` | Disable color in pretty mode. | color enabled |
| `--log-url <pattern>` | Filter log events by URL substring/regex. | unset |
| `--log-tab <id>` | Filter log events by tab ID. | unset |

## Shared global options

Also supports [Global flags](/docs/workflows/global-flags).

## Monorepo and workspace roots

You can point `dev` (and `build`) at the **root of a monorepo** instead of the extension package itself. Extension.js detects the workspace root and auto-resolves the extension package inside it:

```bash theme={null}
extension dev .            # run from the monorepo root
```

When exactly one extension package is found, Extension.js resolves it and prints:

```text theme={null}
Workspace root detected — resolved extension package: packages/my-extension
```

When several candidates exist, it lists them so you can point at the one you mean:

```bash theme={null}
extension dev packages/my-extension
```

### pnpm workspace members install from the root

From 4.1.18, when the project is a member of a pnpm workspace and its dependencies are missing, the automatic install runs from the workspace root instead of the package folder. The install is filtered to the member and its workspace dependencies:

```bash theme={null}
pnpm install --filter {packages/my-extension}...
```

The lockfile and the linker layout stay the workspace's, so the result matches a `pnpm install` at the root. The session prints one info line before the install runs:

```plaintext theme={null}
This project is a pnpm workspace member, installing from the workspace root at /home/dev/monorepo.
```

## Examples

### Running a local extension

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

<CodeGroup>
  ```bash npm theme={null}
  extension dev ./my-extension
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension
  ```

  ```bash bun theme={null}
  extension dev ./my-extension
  ```

  ```bash deno theme={null}
  extension dev ./my-extension
  ```
</CodeGroup>

### Running a remote extension from GitHub

Pass a GitHub tree URL as the argument to develop a remote extension locally:

<CodeGroup>
  ```bash npm theme={null}
  extension dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder
  ```

  ```bash pnpm theme={null}
  extension dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder
  ```

  ```bash yarn theme={null}
  extension dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder
  ```

  ```bash bun theme={null}
  extension dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder
  ```

  ```bash deno theme={null}
  extension dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder
  ```
</CodeGroup>

### Running in Firefox

<CodeGroup>
  ```bash npm theme={null}
  extension dev ./my-extension --browser firefox
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension --browser firefox
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension --browser firefox
  ```

  ```bash bun theme={null}
  extension dev ./my-extension --browser firefox
  ```

  ```bash deno theme={null}
  extension dev ./my-extension --browser firefox
  ```
</CodeGroup>

### Running in multiple browsers in sequence

<CodeGroup>
  ```bash npm theme={null}
  extension dev ./my-extension --browser=chrome,firefox
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension --browser=chrome,firefox
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension --browser=chrome,firefox
  ```

  ```bash bun theme={null}
  extension dev ./my-extension --browser=chrome,firefox
  ```

  ```bash deno theme={null}
  extension dev ./my-extension --browser=chrome,firefox
  ```
</CodeGroup>

### Running inside Docker or a dev container

When you run inside Docker, dev containers, or GitHub Codespaces, bind the dev server to `0.0.0.0` so the host machine can reach it:

<CodeGroup>
  ```bash npm theme={null}
  extension dev ./my-extension --host 0.0.0.0
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension --host 0.0.0.0
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension --host 0.0.0.0
  ```

  ```bash bun theme={null}
  extension dev ./my-extension --host 0.0.0.0
  ```

  ```bash deno theme={null}
  extension dev ./my-extension --host 0.0.0.0
  ```
</CodeGroup>

Combine with `--port 0` to let the OS choose an available port automatically:

<CodeGroup>
  ```bash npm theme={null}
  extension dev ./my-extension --host 0.0.0.0 --port 0
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension --host 0.0.0.0 --port 0
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension --host 0.0.0.0 --port 0
  ```

  ```bash bun theme={null}
  extension dev ./my-extension --host 0.0.0.0 --port 0
  ```

  ```bash deno theme={null}
  extension dev ./my-extension --host 0.0.0.0 --port 0
  ```
</CodeGroup>

#### Bind host vs. connectable host

`--host` is the address the dev server **binds** to. The browser (the HMR client and the reload bridge) needs an address it can actually **connect** to, which is not always the same value:

* `--host 0.0.0.0` binds every interface, but `0.0.0.0` is not a connectable address. Extension.js automatically advertises `127.0.0.1` to the browser instead, the right target for the common port-forwarded Docker/dev-container/Codespaces setup, where the browser runs on the host and the port is forwarded to the container.
* For a true **remote** setup (the browser runs on a different machine than the dev server), pass `--public-host` with the address the browser can reach (an LAN IP or hostname). It is propagated to the HMR client URL, `ready.json`, and the reload bridge baked into the extension.

<CodeGroup>
  ```bash npm theme={null}
  # Browser on another machine reaches the dev server at devbox.local
  extension dev ./my-extension --host 0.0.0.0 --public-host devbox.local
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension --host 0.0.0.0 --public-host devbox.local
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension --host 0.0.0.0 --public-host devbox.local
  ```

  ```bash bun theme={null}
  extension dev ./my-extension --host 0.0.0.0 --public-host devbox.local
  ```
</CodeGroup>

When `--host` is a concrete address already (for example `--host 192.168.1.50`), that value is connectable as-is and is used directly. `--public-host` is only needed when the bind host and the browser-facing host differ.

The dev server checks the `Host` header of every request, as a defense against DNS rebinding. It answers `localhost`, IP addresses, the bind host and `--public-host`. A request under any other name gets `403` with a plain-text body that names the refused host and the fix, and the terminal prints one line per refused host. Allow extra names with `--allowed-hosts`, a comma-separated list where a leading dot allows every subdomain, or with `commands.dev.allowedHosts` in `extension.config.js` (an array or a comma string). Use it when the dev server is reached under a docker service name, a tunnel hostname or an mDNS name, without changing what the browser dials.

```bash theme={null}
extension dev ./my-extension --host 0.0.0.0 --allowed-hosts web,.ngrok.app
```

`--host 0.0.0.0` alone does not allow every name. The bind address and the allowed names are separate settings.

### Running in Brave as a custom binary

<CodeGroup>
  ```bash npm theme={null}
  extension dev ./my-extension --chromium-binary /path/to/brave
  ```

  ```bash pnpm theme={null}
  extension dev ./my-extension --chromium-binary /path/to/brave
  ```

  ```bash yarn theme={null}
  extension dev ./my-extension --chromium-binary /path/to/brave
  ```

  ```bash bun theme={null}
  extension dev ./my-extension --chromium-binary /path/to/brave
  ```

  ```bash deno theme={null}
  extension dev ./my-extension --chromium-binary /path/to/brave
  ```
</CodeGroup>

## Best practices

* **Browser compatibility:** Test your extension in different browsers to verify it works on every target.
* **Polyfilling:** The polyfill is on by default in `dev`, so `browser.*` calls work in Chromium-based browsers. Pass `--no-polyfill` when you want the raw bundle.
* **Automation reliability:** Treat `dev` as the watch-mode companion (`--no-browser` + `dev --wait`). Treat `start` as the production companion (`--no-browser` + `start --wait`). Use `--output=json` for scripts and CI automation.

## Next steps

* Build production artifacts with [`build`](/docs/commands/build).
* Validate production launch flow with [`start`](/docs/commands/start).
* Review browser targeting with [Browser-specific manifest fields](/docs/features/browser-specific-fields).
* Configure shared defaults in [`extension.config.js`](/docs/features/extension-configuration).
* Review configuration env loading behavior in [Environment variables](/docs/features/environment-variables#how-it-works).


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