Skip to main content
Every extension dev session writes state to disk: machine contracts, logs, a control channel, and a managed browser profile. Knowing where each file lives tells you what survives a dist/ wipe, what is safe to parse, and what must never reach a commit.

The two-root contract

Session state splits across two roots with different lifetimes: The split is deliberate. A browser profile can outlive dist/, and the extension cached inside it remembers the control coordinates it was given. When those coordinates lived under dist/, wiping the folder stranded the profile’s cached service worker, which kept dialing a dead port. Anything a profile may have baked in now lives in .extension-js/, where a dist/ wipe cannot touch it. The control token files are written with 0600 permissions. They authorize state-changing verbs such as extension eval, so only your user can read them.

Per-browser keying

Every artifact name embeds the browser: control-token-chrome, dist/extension-js/firefox/ready.json. A single per-project slot would break the moment a second browser session starts on the same project. The second session would overwrite the first session’s token, and either shutdown would delete it for both. Because every slot is keyed, you can run extension dev --browser chrome and extension dev --browser firefox on one project at the same time. Each session keeps its own profile, its own debug port, and its own control channel. Inside dist/extension-js/, each browser gets its own artifact folder:

Debug port derivation

Each session derives its own DevTools debug port instead of sharing one:
  1. Start from the base: your --port value plus a fixed offset of 100, or the default 9222 when no valid base exists.
  2. Add a per-instance offset derived from the first 8 hex characters of the instance id, modulo 1000.
--port 0 means an OS-assigned dev server port, so Extension.js refuses to derive a debug port from it. Deriving from zero would yield an unbindable privileged port, so the default applies instead.

The instance registry

Tooling that attaches to a session resolves ports through an instance registry keyed by exact instance id. An exact id wins. A known instance with no registered port returns the caller’s own fallback, never another instance’s port. When no instance id is given and no fallback exists, the lookup throws AmbiguousInstanceError instead of guessing. Falling back to the most recently launched browser would cross instance streams, so the registry refuses.

Teardown

When a session ends, Extension.js tears the browser down in stages:
  • On the signal path, the browser child gets SIGTERM, then SIGKILL after a 5 second grace window.
  • On process exit, the handler gets one synchronous slice, so the child is force-killed synchronously with SIGKILL.
  • On Windows, taskkill /PID <pid> /T /F kills the whole process tree in both paths.
Socket errors with the codes ECONNRESET, EPIPE, ECONNABORTED, or ENOTCONN during shutdown are treated as benign. They come from a socket the browser is closing, so a graceful shutdown stays graceful instead of exiting with code 1.

The auto-written gitignore

The managed profile holds real browsing data: cookies, history, and any logins you performed during a dev session. Never commit it and never ship it.
Extension.js writes a .gitignore containing * into dist/extension-js/ so the whole session root stays out of commits. It also appends .extension-js to your project’s root .gitignore when one already exists, because the live control token must never land in a commit either. Both writes are hygiene guards: they never overwrite existing content and never fail a build.

build-summary.json

Hosts that shell out to extension build get a structured result channel instead of scraping stdout: The file is not deleted between runs, so consumers must guard against stale files. Compare the file’s mtime against the time you started the build before trusting it.

hot/ pruning

During dev, hot-update chunks are fetched from disk inside the extension origin, so stale generations would accumulate in what ships. After each compile, Extension.js prunes hot/ down to the current generation plus the previous one. The previous generation survives one round so in-flight fetches still resolve.

Next steps