Skip to main content
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.

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.

Dev command capabilities

Usage

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 for the rest.

Arguments and flags

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 runtime is ready
  • status: "error" for startup/compile failures
  • 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, so the file doubles as a build receipt after the terminal scrollback is gone
  • port is the dev server port; host is the connectable host clients dial (see bind host vs. connectable host)
  • controlPort / instanceId locate the control bridge used by extension logs and the act verbs
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: it walks the contract, control channel, token, executor, and browser in order and names the first failing leg with a fix.

--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. If you pass both --wait and --no-browser in the same command invocation, --wait takes precedence. The command runs in wait-only mode.

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

Logging flags

Shared global options

Also supports 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:
When exactly one extension package is found, Extension.js resolves it and prints:
When several candidates exist, it lists them so you can point at the one you mean:

Examples

Running a local extension

Running a remote extension from GitHub

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

Running in Firefox

Running in multiple browsers in sequence

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:
Combine with --port 0 to let the OS choose an available port automatically:

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

Running in Brave as a custom binary

Best practices

  • Browser compatibility: Test your extension in different browsers to verify it works on every target.
  • Polyfilling: If Firefox or a Gecko-based browser is also a target, use --polyfill. This flag enables browser.* API compatibility in Chromium-based browsers.
  • Automation reliability: Treat dev as the watch-mode companion (--no-browser + dev --wait). Treat start as the production companion (--no-browser + start --wait). Use --wait-format=json for scripts and CI automation.

Next steps