Most teams don't know how many feature flags are in their codebase.
They know they have some. They think they cleaned up most. They're not sure about the rest.
One command changes that:
npx flaglint audit ./src
No API key. No credentials. No dashboard to sign up for. Just your source code and an honest answer.
The Problem Nobody Talks About
According to LaunchDarkly's best practices, most release flags should live for only days to weeks — yet many remain in codebases for months or years.
That's not a LaunchDarkly problem. It's a universal one.
Flags accumulate because adding one is fast and removing one is work. You ship the feature, move on, and the flag stays. Six months later a new engineer asks "is this safe to delete?" and nobody knows. So it stays another six months.
Stale flags make code "more complex and harder to maintain," as developers spend extra time navigating obsolete conditionals. Unused toggles may degrade performance or even inadvertently expose features or data.
And here's the part that stings: developers spend 33–42% of their time dealing with technical debt and maintenance. Feature flag debt is a quiet contributor to that number.
What "Flag Debt" Actually Looks Like
Here's a real checkout service. Nothing exotic — a Node.js backend with LaunchDarkly calls spread across five files.
// checkout.ts
export async function isCheckoutV2Enabled(user: User): Promise<boolean> {
const ctx = { targetingKey: user.id, email: user.email, plan: user.plan };
return ldClient.boolVariation("checkout-v2", ctx, false);
}
// discounts.ts
const flagKey = `discount-${experimentName}`;
const enabled = await ldClient.boolVariation(flagKey, ctx, false);
// analytics.ts
const state = await ldClient.allFlagsState(ctx);
Three different patterns. Three very different levels of risk.
The first one is fine — static key, known type, safely removable.
The second one is a problem — the key is a template literal. You can't statically know which flag it evaluates at runtime.
The third one is a migration blocker — allFlagsState has no OpenFeature equivalent. It requires an architecture decision before you touch it.
Most teams treat all three the same. They shouldn't.
The Audit Command
FlagLint v0.6.0 ships with a new command: flaglint audit.
It scans your codebase, classifies every flag call by risk level, and tells you exactly what you're dealing with — before you touch anything.
npx flaglint@latest audit ./src
Running it against the enterprise checkout service above produces:
✓ Audit complete: 13 flags — 3 high risk, 10 medium risk, 0 low risk
With the full table:
| Flag Key | Risk | Usages | Reasons |
|---|---|---|---|
dynamic |
🔴 High | 7 | dynamic key |
checkout-experiment |
🔴 High | 1 | detail evaluation |
* |
🔴 High | 1 | bulk call |
checkout-v2 |
🟡 Medium | 1 | safely automatable |
payment-provider |
🟡 Medium | 1 | safely automatable |
discount-config |
🟡 Medium | 1 | safely automatable, json variation |
| ... |
High risk means the call needs manual review before anything happens:
- Dynamic key — the flag key is a variable or template literal. FlagLint can't tell which flag you're evaluating.
-
Detail evaluation —
boolVariationDetailreturns metadata with no direct OpenFeature equivalent. -
Bulk call —
allFlagsStateis an architecture decision, not a line-by-line migration.
Medium risk means FlagLint can automate the migration safely, but the flag is still a direct vendor call that will need to move eventually.
Low risk means the flag is already migrated or has no debt signals.
Three Output Formats
# Markdown — good for PRs and docs
npx flaglint audit ./src --format markdown
# JSON — good for CI pipelines and dashboards
npx flaglint audit ./src --format json --output audit.json
# HTML — shareable report for engineering reviews
npx flaglint audit ./src --format html --output flag-debt.html
The HTML report is the one worth sharing with your team or your manager. It's a single self-contained file — no server needed, just open it in a browser. Drop it in a PR, a Jira ticket, or an email. It shows exactly which flags need attention and why.
From Audit to Action
The audit is informational. It doesn't touch your code.
Once you've seen your report, FlagLint has two more commands for when you're ready to act:
Preview the migration:
npx flaglint migrate ./src --dry-run
This shows before/after diffs for every safely automatable call — the Medium risk flags from your audit. Nothing is written to disk.
--- checkout.ts
+++ checkout.ts
- return ldClient.boolVariation("checkout-v2", ctx, false);
+ return openFeatureClient.getBooleanValue("checkout-v2", false, ctx);
Note the argument order. LaunchDarkly is (flagKey, context, fallback). OpenFeature is (flagKey, fallback, context). A manual find-and-replace migration silently swaps them — that's a production bug waiting to happen. FlagLint gets it right because it uses AST analysis, not text matching.
Apply the migration:
npx flaglint migrate ./src --apply
Rewrites only the calls that are proven safe. Dynamic keys, detail methods, and bulk calls are skipped and reported for manual review. Won't run on a dirty git tree. Idempotent — safe to run twice.
Lock the boundary in CI:
npx flaglint validate ./src --no-direct-launchdarkly
Exits 1 if any direct LaunchDarkly evaluation call appears in your codebase. Add this to your GitHub Actions workflow and the migration can't silently reverse.
Why No API Key?
Every major feature flag platform has a tool that connects to their API to show you flag health. Those tools are useful, but they only see flags that exist in their own system. They won't show you a LaunchDarkly flag if you're asking a Statsig dashboard.
More importantly — they're built to keep you in their platform. No vendor builds the exit ramp for their own tool.
FlagLint only looks at your source code. It doesn't know what's in your LaunchDarkly dashboard. It doesn't need to. The question it answers is: what is your code actually doing right now?
That's a different question, and it has a different answer.
The Full Workflow
# Step 1: Understand what you have
npx flaglint audit ./src --format html --output flag-debt.html
# Step 2: Preview safe migrations
npx flaglint migrate ./src --dry-run
# Step 3: Apply the safe ones
npx flaglint migrate ./src --apply
# Step 4: Lock it in CI
npx flaglint validate ./src --no-direct-launchdarkly
Four commands. Covers the full lifecycle from "what do we have?" to "this can never regress."
Try It
npx flaglint@latest audit ./src
No install required. Works on any Node.js 20+ project using the LaunchDarkly Node.js server SDK (both launchdarkly-node-server-sdk and @launchdarkly/node-server-sdk).
→ GitHub — MIT licensed, open source
→ npm
→ Docs
FlagLint is a vendor-neutral CLI for LaunchDarkly → OpenFeature migrations. v0.6.0 adds flag debt auditing. Read the changelog →
Tags: node devops javascript typescript openfeature launchdarkly featureflags technicaldebt opensource featureflags
Top comments (0)