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.
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
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
Automation metadata (recommended for scripts/agents)
Whendev runs, Extension.js emits machine-readable metadata under:
dist/extension-js/<browser>/ready.jsondist/extension-js/<browser>/events.ndjson(newline-delimited JSON)
ready.json as the readiness contract:
status: "starting"while bootingstatus: "ready"when runtime is readystatus: "error"for startup/compile failuresrunIduniquely identifies a runtime sessionstartedAtmarks the runtime session start timestampcommandnames the producing command (dev,start,preview, orbuild)toolchainVersion,extensionName, andextensionVersionrecord which Extension.js version produced the tree, for which extension, so the file doubles as a build receipt after the terminal scrollback is goneportis the dev server port;hostis the connectable host clients dial (see bind host vs. connectable host)controlPort/instanceIdlocate the control bridge used byextension logsand 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.executeScriptwith 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.
--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:
- Run
extension dev --no-browseras a long-lived process. - Run
extension dev --wait --browser=<browser>as the readiness gate. - 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 pointdev (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:
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 to0.0.0.0 so the host machine can reach it:
--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.0binds every interface, but0.0.0.0is not a connectable address. Extension.js automatically advertises127.0.0.1to 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-hostwith 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.
--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 enablesbrowser.*API compatibility in Chromium-based browsers. - Automation reliability: Treat
devas the watch-mode companion (--no-browser+dev --wait). Treatstartas the production companion (--no-browser+start --wait). Use--wait-format=jsonfor scripts and CI automation.
Next steps
- Build production artifacts with
build. - Validate production launch flow with
start. - Review browser targeting with Browser-specific manifest fields.
- Configure shared defaults in
extension.config.js. - Review configuration env loading behavior in Environment variables.

