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

# Browser profiles for isolated dev runs

> Manage browser state isolation during extension development with managed, persistent, or custom profile paths for Chrome and Firefox.

Control browser state isolation during development with managed, persistent, or custom profiles.

Keep browser sessions isolated or persistent based on your workflow. Extension.js launches browsers with profile-aware defaults. Choose clean runs, reusable state, or an explicit local profile path.

## How it works

Extension.js chooses the profile mode in this order:

1. System profile mode when `profile: false` or `EXTENSION_USE_SYSTEM_PROFILE=true`
2. Explicit `profile` path (if provided)
3. Managed profile mode (default)
   * temporary (ephemeral) by default
   * persistent when `persistProfile: true` or `keepProfileChanges: true`

### How `--profile` values are read

The CLI delivers flag values as strings, so Extension.js normalizes them:

* `--profile=false` (or `profile: false` in config) means the browser's own default profile.
* `--profile=true` means the managed default, the same as leaving the option unset.
* Any other string is an explicit profile path.

A relative `--profile` path resolves against the project (compilation context), not your shell's working directory. This keeps sequential runs of different projects from collapsing onto one shared profile.

### System profile mode on Firefox and other Gecko browsers

<Warning>
  Extension.js refuses to launch Firefox or any other Gecko-based browser in
  system profile mode. Use it with Chromium-family browsers only.
</Warning>

Extension.js installs the add-on in a Gecko browser over remote debugging, and a browser's own profile keeps remote debugging off. So with `--profile=false`, `profile: false`, or `EXTENSION_USE_SYSTEM_PROFILE=true`, `dev`, `start`, and `preview` stop before the browser launches. Nothing opens on your everyday profile. This applies to every Gecko target the CLI launches: `firefox`, `waterfox`, `librewolf`, `zen`, `floorp`, and `gecko-based`.

All three commands print a block that names the source of the setting and the two ways out. The browser name follows your target, and the `SET BY` line names every source that is active (`--profile=false`, `profile: false in extension.config.js`, or `EXTENSION_USE_SYSTEM_PROFILE=true`):

```text theme={null}
Firefox can't load the add-on in its own profile, so it was not launched.
SET BY --profile=false
Remote debugging is off in a browser's own profile, and the add-on can only be installed over it.
To fix, drop --profile=false, then let Extension.js manage the profile or pass --profile=<path> to use a profile of your own.
```

`extension dev` keeps the dev server running and watching, with `ready.json` in `error` (code `browser_launch_failed`, `browserLaunchFailedCode: "E_FLAG_NOT_SUPPORTED_HERE"`). `extension start` and `extension preview` exit with code 1. With `--output json`, every command fails with `E_FLAG_NOT_SUPPORTED_HERE` and `status: "usage"`.

Earlier versions launched the browser on your own profile with nothing loaded in it. To get a working session, take one of the two ways out the block names:

* Drop `--profile=false` (and `profile: false` or `EXTENSION_USE_SYSTEM_PROFILE`) so Extension.js manages the profile and installs the add-on. Add `persistProfile: true` to keep logins and state between runs, or seed the managed profile from an existing one with [`copyFromProfile`](#seeding-with-copyfromprofile).
* Pass `--profile=<path>` to launch with a profile directory of your own. Extension.js installs the add-on into it.

## Profile capabilities

| Config / option | What it does |
| - | - |
| `browser.<target>.profile` | Uses an explicit profile folder for that browser target. |
| `commands.dev.profile` | Uses an explicit profile only for `dev`. |
| `commands.start.profile` | Uses an explicit profile only for `start`. |
| `commands.preview.profile` | Uses an explicit profile only for `preview`. |
| `browser.<target>.persistProfile` | Reuses managed profile state between runs for a target. |
| `commands.<name>.persistProfile` | Reuses managed profile state in a command-specific context. |
| `keepProfileChanges` | Keeps the managed profile and its changes across runs (same effect as `persistProfile`). |
| `copyFromProfile` | Seeds the managed profile as a copy of an existing profile directory. |
| `--profile=/abs/path` | CLI override for explicit profile path. |
| `--profile=false` | Launches the browser's own default profile (system mode). Chromium family only, see above. |
| `EXTENSION_USE_SYSTEM_PROFILE=true` | Uses the OS/browser system profile instead of managed profiles. |
| `EXTJS_USE_SYSTEM_PROFILE=true` | Alias of `EXTENSION_USE_SYSTEM_PROFILE`. |

### Seeding with `copyFromProfile`

`copyFromProfile` copies the source directory into the managed profile before launch. The copy happens only when the target is fresh, meaning it does not exist or is empty. A persisted profile therefore seeds once, and your later changes survive every run.

## Profile modes

| Mode | How to enable | Typical use |
| - | - | - |
| Managed ephemeral | default | clean runs with isolated state |
| Managed persistent | `persistProfile: true` or `keepProfileChanges: true` | iterative debugging with stable browser state |
| Explicit custom profile | `profile: "/abs/path"` or `--profile=/abs/path` | reuse an existing profile |
| System profile | `--profile=false` or `EXTENSION_USE_SYSTEM_PROFILE=true` | launch with OS/browser default profile (Chromium family only) |

Extension.js creates managed profiles under:

* `dist/extension-js/profiles/<browser>-profile/<...>`

Persistent mode uses:

* `dist/extension-js/profiles/<browser>-profile/dev`

Each ephemeral run gets a generated three-word leaf name in adjective-color-animal form, for example `brave-magenta-heron`. The name is random per run, so do not hardcode it. Read the `profilePath` field in the session's `ready.json` to find the profile a run is using.

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

## Configure in `extension.config.*`

```js theme={null}
export default {
  browser: {
    chrome: {
      // Use your own profile folder:
      profile: "path/to/custom/profile",
    },
    firefox: {
      // Keep a stable managed profile for repeated sessions:
      persistProfile: true,
    },
    edge: {
      // Keep changes across runs and seed once from an existing profile:
      keepProfileChanges: true,
      copyFromProfile: "/path/to/existing/profile",
    },
  },
};
```

You can also scope profile defaults by command:

```js theme={null}
export default {
  commands: {
    dev: {
      persistProfile: true,
    },
  },
};
```

## CLI usage

Use an explicit profile path directly:

```bash theme={null}
extension dev --browser=chrome --profile=/path/to/custom/profile
```

Works similarly with `start` and `preview`.

## Lifecycle notes

* Extension.js creates ephemeral managed profiles for each run.
* Extension.js reuses the persistent managed profile (`dev`) across runs.
* Each ephemeral profile carries a `.extension-js-managed-profile` marker file. On browser exit, Extension.js removes only directories that carry the marker, so kept and explicit profiles are never reclaimed.
* Extension.js also sweeps stale marked profiles on the next launch. Set `EXTENSION_TMP_PROFILE_MAX_AGE_HOURS` to control the maximum age (default 12 hours).
* On every Firefox launch, Extension.js deletes the profile's `startupCache` directory. A pinned or persisted profile can otherwise serve stale extension code across a full dev restart.

## Locked Chromium profiles

<Warning>
  If another live browser process on the same machine owns the profile,
  Extension.js refuses to launch instead of corrupting it. Close that browser or
  choose a different profile first.
</Warning>

Before a Chromium launch, Extension.js reads the profile's `SingletonLock` artifact. A lock that names a dead process or another host is stale. Extension.js removes the stale `SingletonLock`, `SingletonSocket`, and `SingletonCookie` files and launches normally. When the owning process is still alive on this host, the launch aborts instead. The session's `ready.json` is stamped with the `profile_locked` error code, so machine consumers never parse the error sentence.

## Privacy

A managed profile is a full browser profile. It holds cookies, history, and login data from anything you do in that browser session. Extension.js writes a `.gitignore` with a `*` rule into `dist/extension-js` once, so profiles and session state never reach git. Do not commit or ship this directory.

## Best practices

* **Use managed ephemeral profiles for baseline testing**: Reduces hidden state and flaky reproductions.
* **Use `persistProfile` for long-lived debug sessions**: Keep auth/session/devtools state between runs.
* **Keep custom profiles per browser family**: Avoid cross-browser contamination.
* **Use system profile mode intentionally**: Useful for reproduction on Chromium-family browsers, but less isolated than managed profiles. Extension.js refuses to launch a Gecko browser in this mode.

## Next steps

* Learn more about [Browser preferences](/docs/browsers/browser-preferences).
* Learn more about [Browser flags](/docs/browsers/browser-flags).


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