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.
- Running source-inspection workflows with
--source.
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
| 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 |
| Source inspection mode | Structured output and diagnostics with source flags |
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.| 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. | false |
--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, 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. | system default |
--gecko-binary <path> | --firefox-binary | Custom Gecko-family binary path. | system default |
--polyfill [boolean] | - | Enable browser.* API compatibility polyfill for Chromium targets. | false |
--starting-url <url> | - | Starting URL in launched browser. | unset |
--port <port> | - | Dev server port. Use 0 for OS-assigned port. | 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) |
--no-open | - | Do not automatically launch browser. | browser launches by default |
--no-browser | - | Do not launch browser. | 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. | disabled |
--wait-timeout <ms> | - | Timeout for --wait mode. | 60000 |
--wait-format <pretty|json> | - | Output format for wait results (json is machine-readable). | pretty |
--extensions <list> | - | Comma-separated companion extensions or store URLs. | unset |
--install [boolean] | - | Install project dependencies when missing. | command behavior default |
--author | --author-mode | Enable maintainer diagnostics. | disabled |
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 timestampportis 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
--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 — 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.
Source inspection workflow
Use source options only withdev. Source inspection opens a target URL in the browser and prints the full, live HTML after your content scripts run. Use this to verify what your extension actually changes.
--source [url]to inspect a page (defaults to--starting-urlorhttps://example.com)--watch-source/--no-watch-sourceto re-print HTML on each rebuild
start and preview do not support source-inspection flags.
Source flags
| Flag | What it does | Default behavior |
|---|---|---|
--source [url] | Enable source inspection flow. | disabled |
--watch-source [boolean] | Re-print source output on watched updates. | true when --source is enabled |
--source-format <pretty|json|ndjson> | Output format for source inspection. | falls back to log format or json |
--source-summary [boolean] | Emit compact summary output. | disabled |
--source-meta [boolean] | Include page metadata. | enabled when source is on |
--source-probe <selectors> | Comma-separated CSS selectors to probe. | unset |
--source-tree <off|root-only> | Include compact extension tree details. | unset |
--source-console [boolean] | Include console summary. | disabled |
--source-dom [boolean] | Include DOM snapshots/diffs. | enabled when watch-source is on |
--source-max-bytes <bytes> | Cap output payload size. | unset |
--source-redact <off|safe|strict> | Redaction strategy for source output. | format-dependent |
--source-include-shadow <off|open-only|all> | Control Shadow DOM inclusion. | open-only when source is on |
--source-diff [boolean] | Include diff metadata on watch updates. | enabled when watch-source is on |
Logging flags
| 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, sidebar, popup, options, devtools). | 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.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:Inspecting page HTML with source mode
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. - Source inspection workflows: Use
dev(notstart/preview) when you need source inspection options. - 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.

