A Chrome extension + lightweight SDK plugin for overriding LaunchDarkly feature flags during development, QA, and production validation — without writing anything to the host page's localStorage, without requiring a LaunchDarkly login, and without rendering any UI into the host application.
Status: v0. Functional but minimal. See Roadmap for what's intentionally out of scope right now.
LaunchDarkly Labs: This repository is maintained as a Labs project. It is not officially supported by LaunchDarkly. For officially supported tooling, see the LaunchDarkly Dev Toolbar.
The official @launchdarkly/toolbar is the right tool for most teams — it's actively developed by LaunchDarkly, ships a rich UI directly into the app, integrates with the LaunchDarkly product, and covers a lot more than just flag overrides (contexts, event interception, observability, session replay). If you don't have any specific constraints below, use the official toolbar.
This project exists for a narrower set of scenarios where the official toolbar's design choices don't fit:
- Apps that can't write to
localStorage/sessionStorage/ cookies on their own origin — security policies, compliance posture, or embedded environments that lock down browser storage. The official toolbar's override mechanism islocalStorage-backed (correctly — that's the right primitive for an in-page tool), but it means the override path is unavailable when those storage APIs are blocked. - Workflows where requiring a LaunchDarkly login adds friction — for example, a QA engineer who needs to force a flag variation but doesn't have a seat in the project, or a contractor working in a sandbox environment. The official toolbar's full UI is gated behind LD authentication for good reasons (it shows project state); this extension trades that integration for a no-auth override-only experience.
- Validating flag behavior in production without any UI visible to end users. The official toolbar's floating button is the right default — it's how authenticated devs find their tools. For the specific case of "force a flag in prod for 30 seconds to confirm an incident workaround, then revert," moving the UI into Chrome DevTools keeps the host app pixel-identical to what users see.
This project does much less than the official toolbar by design: it focuses tightly on flag overrides and the supporting workflow (persistence, share links, flag discovery), and skips the rest. The two coexist — you can register both plugins in the same SDK config if you want both surfaces available.
The system has two cooperating halves:
Chrome Extension (this repo) Host App (any LD-SDK app)
───────────────────────────── ─────────────────────────
DevTools Panel (React)
│ chrome.runtime.Port
▼
Background Service Worker
│ chrome.tabs.sendMessage
▼
Content Scripts @launchdarkly/toolbar-
- ISOLATED world (RPC) ──▶ extension-bridge
- MAIN world (window hook) - SDK plugin
- In-memory override map
- Uses LDDebugOverride
▼
LD JS SDK serves the
overridden value
Two things make this work:
- The bridge plugin (
@launchdarkly/toolbar-extension-bridge) implements LaunchDarkly's officialLDPlugininterface and applies overrides viaLDDebugOverride.setOverride(...)— the same sanctioned SDK hook the official toolbar uses. No monkey-patching, no fragility. Overrides live in memory only. - The extension stores override state in
chrome.storage.local(the extension's own sandbox, completely separate from the page's storage). The page never sees this; CDW-style restrictions onwindow.localStoragedon't apply.
You need to do two things: install the Chrome extension, and add the bridge plugin to your app's LD SDK config.
This repo ships the extension as source; the v0 distribution path is sideloading.
git clone https://github.com/launchdarkly-labs/ld-toolbar-extension.git
cd ld-toolbar-extension
corepack pnpm install
corepack pnpm --filter ./packages/extension run buildThen in Chrome:
- Open
chrome://extensions/ - Toggle Developer mode on (top right)
- Click Load unpacked
- Select the
packages/extension/distdirectory
You should see LaunchDarkly Toolbar Extension in the list. (Chrome Web Store distribution is a future option — see Roadmap.)
The bridge plugin works with any browser-side LaunchDarkly SDK that supports the plugins config option (JS Client v3.6.0+ — which includes React, Vue, and Angular wrappers).
Installation:
@launchdarkly/toolbar-extension-bridgeis not yet published to npm. You can still install it directly from this repository today using one of the two methods below. If/when it lands on npm later, the integration code (theimportlines and the SDK config) won't change.Option A — local file path (best for development / iteration): Clone this repo somewhere on your machine, then add the dep in your app's
package.json:"dependencies": { "@launchdarkly/toolbar-extension-bridge": "file:../relative/path/to/ld-toolbar-extension/packages/bridge-plugin" }Then run
npm install(orpnpm install/yarn install) in your app. Whenever the bridge plugin source changes, runcorepack pnpm --filter @launchdarkly/toolbar-extension-bridge run buildin the cloned repo to refresh the builtdist/your app consumes.Option B — tarball install (best for hand-off to another team): In a checkout of this repo:
corepack pnpm install corepack pnpm --filter @launchdarkly/toolbar-extension-bridge run build cd packages/bridge-plugin npm packThat produces
launchdarkly-toolbar-extension-bridge-0.0.1.tgz. Hand the tarball to whoever needs the package and have them install it in their app:npm install /path/to/launchdarkly-toolbar-extension-bridge-0.0.1.tgzOnce installed via either method, the rest of the integration is identical to what's shown below.
Vanilla JS:
import { initialize } from "launchdarkly-js-client-sdk";
import { ExtensionBridgePlugin } from "@launchdarkly/toolbar-extension-bridge";
const bridge = new ExtensionBridgePlugin();
const client = initialize("YOUR_CLIENT_SIDE_ID", context, {
plugins: [bridge],
});
await client.waitForInitialization();React SDK:
import { asyncWithLDProvider } from "launchdarkly-react-client-sdk";
import { ExtensionBridgePlugin } from "@launchdarkly/toolbar-extension-bridge";
const bridge = new ExtensionBridgePlugin();
const LDProvider = await asyncWithLDProvider({
clientSideID: "YOUR_CLIENT_SIDE_ID",
context: { kind: "user", key: "user-key", anonymous: true },
options: {
plugins: [bridge],
},
});
ReactDOM.createRoot(document.getElementById("root")).render(
<LDProvider>
<App />
</LDProvider>,
);That's the entire integration. The plugin auto-detects whether the Chrome extension is installed; if it's not present, it does nothing (no network calls, no listeners, no overhead). If it is present, it announces itself and starts receiving override commands.
- Open your app in Chrome with the extension installed.
- Open Chrome DevTools (F12 or Cmd+Opt+I).
- Click the LaunchDarkly tab in the DevTools tab strip (it may be behind the
»overflow if you have many panels). - The panel shows the SDK status, current overrides, and an "Add override" form.
- Type a flag key, type a value (JSON-parsed:
true,false,42,"string", or{"foo":1}), click Add. The flag flips immediately on the page. - Share a configuration: once you've set overrides, click Copy share link in the panel. Send the resulting URL to a teammate. When they open it (with the extension installed), the overrides apply to their page automatically. The URL parameter is read by the extension and never by the host page, so it works in environments that block
localStorage.
The bridge plugin exposes a small API on the instance you create. Useful for Playwright/Cypress tests, console debugging, or driving overrides from your own code:
import { initialize } from "launchdarkly-js-client-sdk";
import { ExtensionBridgePlugin } from "@launchdarkly/toolbar-extension-bridge";
const bridge = new ExtensionBridgePlugin();
// In your SDK config:
const client = initialize(clientSideId, context, { plugins: [bridge] });
// Then from app code, tests, or the console:
bridge.setOverride("my-flag", true);
bridge.setOverride("greeting", "hello");
bridge.setOverride("config", { theme: "dark", limit: 50 });
bridge.removeOverride("my-flag");
bridge.clearAllOverrides();
bridge.getAllOverrides(); // → Record<flagKey, value>
bridge.getClient(); // → LDClient (or null before SDK register())By default the instance is also exposed at window.__ldBridge for convenient console access. Disable this in production builds by passing new ExtensionBridgePlugin({ exposeOnWindow: false }).
The programmatic API works with or without the extension installed. The extension just gives you a UI on top of the same operations.
These are different tools with overlapping but distinct surface areas. Pick based on what your environment can support and which features you need; the table below is a quick orientation, not a scorecard.
| Official Dev Toolbar | This (Extension Edition) | |
|---|---|---|
| Flag overrides | Yes | Yes |
| Override UI surface | In-app floating button | Chrome DevTools panel |
| Override storage backend | Browser localStorage on the page origin |
In-memory page-side + chrome.storage.local in the extension sandbox |
| LaunchDarkly login | Required for full functionality (project integration, share state, etc.) | Not used |
| UI rendered into host app DOM | Yes (floating button) | No |
| SDK plugin interface | LDPlugin |
LDPlugin (identical contract) |
| Framework support | JS / React / Vue / Angular | JS / React / Vue / Angular |
| Context switching | Yes | Not in v0 |
| Event interception / evaluation log | Yes | Not in v0 |
| Share state via URL | Yes (writes to localStorage on receive) |
Yes (writes to chrome.storage.local; both parties need the extension) |
| Observability / session replay integration | Yes | No |
| Distribution | Published npm package + CDN bundle | Sideloaded unpacked Chrome extension (Web Store TBD) |
Use the official toolbar if you have a LaunchDarkly account for everyone who'll use it, your app can write to localStorage, and you want the broader feature set. Use this extension if any of those don't hold and you just need flag overrides.
The two implementations are independent — this project doesn't depend on @launchdarkly/toolbar — and they happily coexist in the same SDK config if you want both surfaces available at once.
If you're integrating, debugging, or extending the project, this section covers the message flow.
Page load sequence (assuming the extension is installed):
- Page navigation begins.
- At
document_start, before any page script runs, Chrome injects two content scripts:injected.tsruns in the page's MAIN world and setswindow.__LD_DEVTOOLS_HOOK__(the same pattern React DevTools uses with__REACT_DEVTOOLS_GLOBAL_HOOK__).content-script.tsruns in the ISOLATED world and bridgeswindow.postMessage↔chrome.runtime.
- Your page loads, runs, and initializes the LD SDK with the bridge plugin in
plugins. - The SDK calls
bridge.register(client). The plugin seeswindow.__LD_DEVTOOLS_HOOK__, announces itself viahook.onSdkReady(...)(which postMessages out to the ISOLATED content script → background SW), and subscribes to override commands viahook.subscribeToOverrides(...). - The background SW now knows the tab has an active SDK.
Setting an override from the DevTools panel:
- User types a flag key + value in the panel and clicks Add.
- Panel sends
{ type: "set-overrides", overrides: { ... } }over itschrome.runtime.Portto the background SW. - Background SW forwards via
chrome.tabs.sendMessage(tabId, msg)to the ISOLATED content script. - ISOLATED content script rebroadcasts to MAIN world via
window.postMessage. - MAIN-world hook fans out to subscribed listeners (i.e., the bridge plugin).
- Bridge plugin calls
debugOverride.setOverride(flagKey, value)on the SDK. - Next
variation()call returns the override.
No localStorage write anywhere on the page side. Each change is also persisted into chrome.storage.local keyed by the page's origin, so overrides survive page reloads: when the SDK announces ready on the next load, the background SW looks up the saved overrides for that origin and pushes them back down through the same chain. Sending a share link uses the same persistence path on the recipient's end — see Quick start, step 3 point 6.
ld-toolbar-extension/
├── README.md
├── CLAUDE.md Notes for AI-assisted development
├── LICENSE Apache 2.0
├── package.json pnpm workspace root
├── pnpm-workspace.yaml
└── packages/
├── bridge-plugin/ npm package: @launchdarkly/toolbar-extension-bridge
│ ├── src/index.ts LDPlugin implementation
│ ├── tsup.config.ts ESM + CJS + .d.ts output
│ └── package.json
└── extension/ Chrome extension (MV3)
├── manifest.config.ts crxjs manifest (typed)
├── src/
│ ├── injected.ts MAIN-world script: sets the hook
│ ├── content-script.ts ISOLATED-world script: postMessage ↔ runtime bridge
│ ├── background.ts Service worker: tab registry + RPC + storage
│ ├── devtools/
│ │ ├── devtools.html Hidden DevTools entry point
│ │ ├── devtools.ts Calls chrome.devtools.panels.create
│ │ ├── panel.html The visible panel iframe
│ │ ├── panel.tsx React entry
│ │ ├── PanelApp.tsx Panel UI
│ │ ├── usePanelRpc.ts Port-based RPC hook
│ │ └── panel.css
│ └── shared/
│ └── shareState.ts Base64 codec + URL helpers used by both
│ the panel and the content script
├── vite.config.ts
└── package.json
- Node.js 20+ (works on 23+)
- pnpm 10+ — the repo pins
pnpm@10.14.0viapackageManagerinpackage.json. Usecorepack pnpm <cmd>to avoid global installs. - Chrome (or any Chromium browser supporting Manifest V3)
corepack pnpm installBridge plugin only:
corepack pnpm --filter @launchdarkly/toolbar-extension-bridge run buildExtension only:
corepack pnpm --filter ./packages/extension run buildEverything:
corepack pnpm -r buildThe bridge plugin supports watch mode via tsup:
corepack pnpm --filter @launchdarkly/toolbar-extension-bridge run devThe extension uses Vite, which can also watch:
corepack pnpm --filter ./packages/extension run devAfter rebuilds, you'll need to hit the reload button (↻) on the extension in chrome://extensions/ to pick up changes. Then close and reopen DevTools for the panel to update.
While iterating, point your app's package.json at the local source instead of an npm install:
{
"dependencies": {
"@launchdarkly/toolbar-extension-bridge": "file:../path/to/ld-toolbar-extension/packages/bridge-plugin"
}
}Run npm install (or your equivalent) in the host app. After rebuilding the bridge plugin, the host app picks up the new dist/ automatically.
What's intentionally not in v0, in rough priority order:
- Per-context overrides. Maintain different override sets for different LD contexts (currently scoped per-origin only).
- Chrome Web Store / Edge Add-ons / Firefox. Currently sideload-only.
- Browser action popup as an alternative entry point (not just the DevTools panel).
- Event interception view for parity with the official toolbar's event log.
- Automated tests. Unit coverage on the bridge plugin (plugin lifecycle, override precedence, hook detection, message handling) and Playwright end-to-end coverage of the panel ↔ background ↔ bridge round-trip.
- Light visual polish on the DevTools panel — tighter spacing, keyboard shortcuts, empty-state copy.
If you need any of these and want to contribute, open an issue first to discuss the approach.
If you're feeding this README to Claude (or another assistant) to help set up the integration in a specific codebase, here's what it needs to know:
- The integration point is the LD SDK's
pluginsarray. Find wherelaunchdarkly-*-client-sdkis initialized (look forinitialize,asyncWithLDProvider,LDProvider,useLDClient, or imports fromlaunchdarkly-*-client-sdk). - Add the bridge plugin to that array. Import
ExtensionBridgePluginfrom@launchdarkly/toolbar-extension-bridge, instantiate once, pass it in the SDK options as{ plugins: [bridge] }. - Don't remove other plugins. If
@launchdarkly/toolbar'sFlagOverridePluginorEventInterceptionPluginis already there, keep them. The bridge plugin coexists. - No localStorage anywhere. If your task is to make an existing flag-override pattern compliant with a no-
localStoragepolicy, the bridge plugin is the answer. Replace any localStorage-backed override logic; do not add new localStorage writes. - The Chrome extension must be installed separately by the developer. It is not a code dependency — it's a browser tool. Tell the user to follow the Install the Chrome extension section to load it unpacked.
- For Playwright/Cypress tests, drive overrides directly via the programmatic API (
bridge.setOverride('flag', value)). The extension is not required for tests — only the bridge plugin needs to be in the SDK config.
A minimal integration diff in a React app looks like this:
import { asyncWithLDProvider } from "launchdarkly-react-client-sdk";
+import { ExtensionBridgePlugin } from "@launchdarkly/toolbar-extension-bridge";
+const bridge = new ExtensionBridgePlugin();
+
const LDProvider = await asyncWithLDProvider({
clientSideID: clientSideId,
context: loggedOutContext,
options: {
evaluationReasons: true,
+ plugins: [bridge],
},
});Apache License 2.0 — see LICENSE.