Skip to content

FlagLint Blog

LaunchDarkly Feature Flag Cleanup: Audit, Rewrite, and Enforce in TypeScript

Your codebase has been accumulating direct LaunchDarkly SDK calls for years. The team knows a cleanup is overdue, but nobody has a clear picture of how many flag keys exist, which ones are safely automatable, and which ones will bite you if touched carelessly. LaunchDarkly’s built-in cleanup tooling — Vega — requires an Enterprise subscription. Grep finds string literals but misses dynamic keys, detail evaluations, and bulk calls. You end up either doing nothing or hand-editing files one at a time and hoping the argument order is correct.

FlagLint is a free, open-source CLI that automates LaunchDarkly feature flag cleanup in TypeScript and JavaScript codebases using AST-based static analysis — not regex — to classify every direct LaunchDarkly SDK call by risk, generate a readable flag debt inventory, rewrite safe call sites to the OpenFeature standard, and enforce the resulting boundary in CI. No LaunchDarkly API key required.

This guide walks through a complete cleanup cycle: audit → rewrite → enforce.

The first step of any LaunchDarkly feature flag cleanup is understanding what you are dealing with. Run flaglint audit against your source directory:

Terminal window
npx flaglint audit ./src

Here is the real output for a two-file Node.js service with seven flag evaluations across checkout and pricing modules:

- Auditing ./src...
# FlagLint Audit Report
**Scanned at:** 2026-07-06T04:47:44.977Z
**Scan root:** ./src
**Files scanned:** 2
**Duration:** 35ms
## Summary
| Total Flags | High Risk | Medium Risk | Total Usages |
|-------------|-----------|-------------|--------------|
| 7 | 2 | 5 | 7 |
| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review |
|--------------|--------------|------------|---------------|-------------------|---------------|
| 1 | 1 | 0 | 0 | 5 | 2 |
> **Staleness:** No staleness signals detected. Heuristics checked: keyword match
> (flag key contains old/deprecated/legacy/temp/tmp/test/demo), path pattern
> (test/spec/mock files, deprecated/old/legacy directories), and minFileCount
> threshold. Git-history-based staleness (last evaluation date) requires git
> metadata and is not available in a pure static scan.
## Migration Readiness
Migration readiness: **71/100** · moderate
[██████████████████░░░░░░░] 71%
5 safely automatable · 2 require manual review
## Flag Debt Inventory
| Flag Key | Risk | Usages | Files | Call Types | Reasons |
|----------|------|--------|-------|------------|---------|
| `<dynamic key>` | 🔴 High | 1 | 1 | numberVariation | dynamic key |
| `beta-pricing` | 🔴 High | 1 | 1 | boolVariationDetail | detail evaluation |
| `checkout-v2` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
| `discount-percentage` | 🟢 Automatable | 1 | 1 | numberVariation | safely automatable |
| `ui-theme` | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable |
| `checkout-config` | 🟡 Medium | 1 | 1 | jsonVariation | safely automatable, json variation |
| `promo-banner` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
## Next Steps
- Run `flaglint migrate --dry-run` to preview safe OpenFeature rewrites
- Run `flaglint validate --no-direct-launchdarkly` to enforce OF boundary in CI
- Review HIGH risk flags manually before any automated migration
✓ Audit complete: 7 flags — 2 high risk, 5 medium risk (35ms, 2 files)
Migration readiness: 71/100 · moderate
[██████████████████░░░░░░░] 71%
5 safely automatable · 2 require manual review

The readiness score of 71/100 means 5 of 7 call sites can be rewritten automatically. The two high-risk entries need manual attention before anything else moves.

Add --format html --output flag-debt.html to produce a shareable report to attach to a migration planning ticket. The flag debt blog post covers the full range of audit options including effort estimates.

What the flag debt inventory is telling you

Section titled “What the flag debt inventory is telling you”

Two call types are classified as high risk:

Dynamic key (<dynamic key>) — the flag key is constructed at runtime from a variable or template literal (e.g., const key = `pricing-${plan}`). FlagLint cannot resolve which flag is actually evaluated at any given call site. Any automated rewrite here would silently touch the wrong call. These require a human decision: extract the dynamic key into a lookup table, split into separate static flag keys, or handle manually per call type.

Detail evaluation (beta-pricing via boolVariationDetail) — variationDetail returns a reason object with no direct OpenFeature equivalent. FlagLint skips these by design. You need to decide whether that reason metadata is still needed after migration, and if so, which OpenFeature detail API maps to your use case.

The five remaining call sites — boolVariation, numberVariation, stringVariation, jsonVariation, and a second boolVariation — are all safely automatable. FlagLint can rewrite every one of them without you touching a line.

Run flaglint migrate with --dry-run to see exactly what changes before any file is modified:

Terminal window
npx flaglint migrate ./src --dry-run

Real output (provider setup guidance section omitted; covered in Step 3 below):

- Scanning ./src...
LaunchDarkly usages found: 7
Safely automatable: 5 · Manual review: 2
Reviewable diffs: 5
Diffs requiring provider setup: 5
Skipped usages: 2
## Diffs
diff --git a/checkout.ts b/checkout.ts
--- a/checkout.ts
+++ b/checkout.ts
@@ -8,1 +8,1 @@
- const newCheckoutEnabled = await ldClient.boolVariation("checkout-v2", ctx, false);
+ const newCheckoutEnabled = await openFeatureClient.getBooleanValue("checkout-v2", false, ctx);
@@ -9,1 +9,1 @@
- const discountPct = await ldClient.numberVariation("discount-percentage", ctx, 0);
+ const discountPct = await openFeatureClient.getNumberValue("discount-percentage", 0, ctx);
@@ -10,1 +10,1 @@
- const theme = await ldClient.stringVariation("ui-theme", ctx, "default");
+ const theme = await openFeatureClient.getStringValue("ui-theme", "default", ctx);
@@ -11,1 +11,1 @@
- const config = await ldClient.jsonVariation("checkout-config", ctx, {});
+ const config = await openFeatureClient.getObjectValue("checkout-config", {}, ctx);
diff --git a/pricing.ts b/pricing.ts
--- a/pricing.ts
+++ b/pricing.ts
@@ -10,1 +10,1 @@
- const promoEnabled = await ldClient.boolVariation("promo-banner", ctx, false);
+ const promoEnabled = await openFeatureClient.getBooleanValue("promo-banner", false, ctx);
## Skipped Usages
- pricing.ts:9:26 — `dynamicKey` via `numberVariation`: dynamic key requires manual review
- pricing.ts:11:23 — `beta-pricing` via `boolVariationDetail`: detail methods skipped:
OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature detail result parity requires
manual review

Notice the argument order flip: boolVariation("checkout-v2", ctx, false) becomes getBooleanValue("checkout-v2", false, ctx). The LaunchDarkly SDK puts context second and default last; OpenFeature reverses that. This reversed argument order is the most common source of silent production bugs in manual migrations — FlagLint handles it correctly for every safe call type.

The jsonVariationgetObjectValue rewrite is flagged json variation in the audit because OpenFeature’s JSON type is object. If your LaunchDarkly flag ever returns a primitive JSON value (number, string, boolean, null), call semantics differ. Review before applying.

The two skipped usages are left exactly as-is in source.

The dry-run output marks all five diffs as requiring provider setup. The LaunchDarkly SDK stays as your evaluation backend — you are changing the API your application code calls, not where flags are stored or evaluated.

Install once:

Terminal window
npm install @openfeature/server-sdk \
@launchdarkly/node-server-sdk \
@launchdarkly/openfeature-node-server

Add a bootstrap file at application startup. Do not remove existing LaunchDarkly packages — the OpenFeature provider depends on them at runtime:

import { OpenFeature } from "@openfeature/server-sdk";
import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);
await OpenFeature.setProviderAndWait(ldProvider);
export const openFeatureClient = OpenFeature.getClient();

Import openFeatureClient in every module that has call sites in the migration plan, or configure openFeatureClientBindings in your .flaglintrc so FlagLint locates the client binding automatically. The add OpenFeature provider tutorial covers both approaches with full examples.

Once the OpenFeature provider is wired:

Terminal window
npx flaglint migrate ./src --apply

FlagLint rewrites only the five safely automatable call sites and leaves the two high-risk ones untouched. What you get is an ordinary git diff: five function-call replacements across two files, reviewable like any other PR. The dynamic key and detail evaluation remain as direct LaunchDarkly SDK calls until you handle them manually.

After --apply and manual resolution of the remaining two call sites, lock the boundary so no new direct LaunchDarkly SDK calls can reach main:

Terminal window
npx flaglint validate ./src --no-direct-launchdarkly

If you are mid-cleanup and cannot enforce a hard block yet, use baseline mode: it freezes existing flag debt and fails on any net-new addition.

Terminal window
# Write current findings as the accepted baseline
npx flaglint audit ./src --write-baseline .flaglint-baseline.json
# In CI: fail only on findings not present in the baseline
npx flaglint validate ./src \
--no-direct-launchdarkly \
--baseline .flaglint-baseline.json \
--fail-on-new

Commit .flaglint-baseline.json to source control. Each time you resolve a flag through migrate --apply or a manual cleanup, re-run --write-baseline to shrink the accepted set. The GitHub Actions integration guide shows the full CI step configuration, including SARIF upload for GitHub Code Scanning annotations.

If your LaunchDarkly SDK calls are spread across multiple packages, run the three commands per package rather than at the repo root. Each package can have its own .flaglintrc pointing to its local OpenFeature client binding. The monorepo guide covers per-package configuration and how to sequence the cleanup when the same flag key is evaluated in shared libraries and consumer apps simultaneously.

LaunchDarkly feature flag cleanup at the code level breaks into four repeatable steps:

  1. flaglint audit ./src — inventory your flag debt and get a readiness score
  2. flaglint migrate ./src --dry-run — review the migration plan before touching files
  3. flaglint migrate ./src --apply — apply safe rewrites; fix the remaining two manually
  4. flaglint validate ./src --no-direct-launchdarkly — gate the boundary in CI

No Enterprise subscription, no API key, no manual grep. The complete six-step migration walkthrough picks up from here if you want to see the full picture across a production Node.js service.

FlagLint Is Now Installable via Homebrew

Starting with v1.1.0, FlagLint is installable via Homebrew — no Node.js required.

Terminal window
brew tap flaglint/tap
brew install flaglint

That’s it. The full CLI is available immediately, including audit, scan, migrate, and validate.

Until now, the only install paths were npm install -g flaglint or npx flaglint@latest. Both work fine — but both require Node.js 20 or newer. That’s a reasonable assumption for application developers, but it’s a friction point in a few common situations.

DevOps and platform engineers often run tooling audits across repos without a Node.js environment set up. Installing Node just to run one CLI is the kind of thing that gets a tool skipped in favour of whatever’s already available.

Docker-based CI is the bigger one. A lot of teams run their CI in minimal images — no Node, no npm. Adding a Node install step purely to run npx flaglint adds 30-60 seconds to every pipeline run and pulls in a dependency that has nothing to do with the actual application. With Homebrew, a Linux CI job can install FlagLint in a single brew install call with no Node dependency.

Mac developers who use Homebrew for CLI tools now get brew upgrade flaglint like any other tool, without thinking about npm.

The tap lives at github.com/flaglint/homebrew-tap. The formula fetches the published npm tarball directly from the npm registry and wires up the CLI binary.

The formula updates automatically on every release — a GitHub Actions job in the main repo runs after each npm publish, computes the new tarball SHA256, and commits an updated formula to the tap. So brew upgrade flaglint will always pull the current release without any manual intervention.

Terminal window
brew tap flaglint/tap
brew install flaglint
flaglint --version

Or if you already have Node.js, npx flaglint@latest still works exactly as before. The Homebrew path is an addition, not a replacement.

Once installed, the quickstart walks through the full audit → preview → apply workflow.

FlagLint Is Now Listed on the OpenFeature Ecosystem

FlagLint is now listed in the OpenFeature ecosystem directory as one of two integrations in the JavaScript/Server category.

I want to be straightforward about what that means — and what it doesn’t.

The OpenFeature ecosystem is a directory maintained by the OpenFeature project (a CNCF incubating standard) where providers, SDKs, hooks, and integrations can be listed. It is not an award or a certification. It is a discovery page. Teams that are already evaluating OpenFeature — researching providers, looking for tooling, trying to understand the ecosystem — land there.

Being listed means that those teams will now find FlagLint when they filter for integrations. That matters because the people who need FlagLint most are exactly the people who are actively thinking about OpenFeature.

The OpenFeature standard is the reason FlagLint exists. The whole problem FlagLint solves — the argument-order inversion between LaunchDarkly’s boolVariation(key, ctx, default) and OpenFeature’s getBooleanValue(key, default, ctx) — only surfaces when you are trying to move to OpenFeature. If teams weren’t adopting OpenFeature, there would be no migration to get wrong.

So it made sense to be in the directory where those teams are looking. Not to market FlagLint as a product, but to be findable at the point in the journey where someone is asking “what tooling exists around OpenFeature for LaunchDarkly migration?”

What FlagLint does in the context of OpenFeature

Section titled “What FlagLint does in the context of OpenFeature”

FlagLint is not an OpenFeature SDK or provider. It doesn’t evaluate flags. What it does is sit at the boundary between your existing LaunchDarkly codebase and the OpenFeature world you’re moving toward.

Specifically:

  • Audit — inventory every direct LaunchDarkly SDK call, classify each one by migration risk, produce a readiness score
  • Migrate — preview and apply proven-safe call-site rewrites that transpose arguments correctly and rename methods atomically
  • Validate — enforce in CI that no new direct LaunchDarkly calls land once you’ve drawn the boundary

None of that requires a network connection, an API key, or access to your LaunchDarkly environment. It’s all static analysis on your source code, running locally.

If you’re in the process of moving to OpenFeature and want to understand your current exposure before touching any code, the audit command is the right starting point.

Terminal window
npx flaglint@latest audit ./src

It runs in under a minute on most codebases and doesn’t touch any files.

Flipt's LaunchDarkly Migration Guide Recommends FlagLint

Flipt is an open-source feature flag platform — one of the most popular self-hosted alternatives to LaunchDarkly. Their official documentation includes a guide for migrating a Node.js application from the LaunchDarkly SDK to OpenFeature, and that guide now recommends FlagLint for the codebase analysis step.

Here’s what that looks like in practice and why it makes sense.

Flipt supports OpenFeature through its official provider — so a team moving away from LaunchDarkly can route flag evaluation through the OpenFeature API to Flipt’s provider instead. The flag evaluation backend changes, but the application code uses the same vendor-neutral API regardless of which provider is behind it.

The challenge isn’t the provider setup. It’s the application code. A typical Node.js service has dozens of direct LaunchDarkly SDK calls — boolVariation, stringVariation, jsonVariation — all using LaunchDarkly’s argument order. OpenFeature’s equivalent methods take arguments in a different order, and a naive find-and-replace will silently swap your fallback values and context in every call site.

That’s the problem FlagLint solves before you write a single line of migration code.

Flipt’s guide recommends running three FlagLint commands as an optional but practical first step for larger codebases:

Terminal window
# See every direct LaunchDarkly call site in your codebase
npx flaglint scan ./src
# Get a migration readiness score and per-flag risk classification
npx flaglint audit ./src
# Preview the exact rewrites FlagLint will make, without touching any files
npx flaglint migrate ./src --dry-run

The dry-run output shows you exactly which call sites will be rewritten and what the rewritten code looks like — before anything changes. Call sites that FlagLint cannot safely rewrite (dynamic flag keys, detail methods, bulk evaluation) are reported for manual review and left untouched.

After reviewing the diff, --apply writes the changes. Then you wire in the Flipt OpenFeature provider, run your tests, and enforce the boundary in CI with flaglint validate.

FlagLint and Flipt don’t overlap. FlagLint analyzes and rewrites your application’s call sites. Flipt evaluates your flags at runtime through an OpenFeature provider. They’re doing completely different jobs at different layers of the stack.

What they share is a user: someone who has decided to move away from vendor lock-in and is doing the actual migration work. FlagLint handles the static analysis half; Flipt handles the runtime half.

If you’re in that position — evaluating Flipt as a LaunchDarkly replacement and trying to understand the migration scope — flaglint audit is a good first step. It gives you a concrete inventory and readiness score before you commit to any approach.

Terminal window
npx flaglint@latest audit ./src

No API key, no source upload, runs locally in under a minute. See the full migration guide for everything that comes after the audit.

How to Migrate from LaunchDarkly to OpenFeature in Node.js

Most LaunchDarkly to OpenFeature migrations start the same way: someone opens a PR with a find-and-replace across the codebase, the tests pass, the PR merges, and two days later a subset of users hits the wrong feature state in production.

The root cause is almost always the same one-line trap — and this guide shows you how to avoid it entirely, using a workflow that audits first, rewrites only what it can prove is safe, and enforces the boundary in CI when you’re done.