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.
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.
LaunchDarkly flag debt accumulates quietly. A team ships a feature behind a flag, verifies the rollout, and moves on. The flag stays. Six months later it is still being evaluated on every request — carrying the original business logic, an implicit dependency on the LaunchDarkly SDK, and a refactoring cost that compounds with every passing sprint.
At scale, the problem becomes a planning question as much as a technical one. Grepping for ldClient gives you a count, but it misses wrappers, misclassifies risk levels, and gives no indication of how long cleanup will actually take. Before you can schedule the work or make the case to your engineering manager, you need a measurement: how many direct LaunchDarkly SDK calls exist, which ones are safe to automate, and how many engineer-hours does this represent?
FlagLint produces that measurement from static source analysis alone. No LaunchDarkly API key required.
The readiness score answers the foundational question before any migration begins: what fraction of your direct LaunchDarkly SDK call sites can a tool rewrite automatically? A score of 50 — moderate — means exactly half require a human to review before any automated step runs. A score below 50 is graded complex; 80 or above is ready, meaning the migration can proceed with minimal manual effort.
The stale signals column surfaces flag keys that carry staleness signal — flag keys containing keywords like old, deprecated, legacy, or tmp. Zero here means no flag key names carry obvious staleness signal at the source level. Git-based staleness detection, which checks last-evaluation date against git history, is outside the scope of a static scan.
Step 2: Measure the LaunchDarkly flag debt in engineer-hours
A risk count tells you what you have. It does not tell you what it will cost to fix. Add --effort-estimate to get a directional planning number:
Terminal window
npxflaglintaudit./src--effort-estimate
Real output:
## Estimated Migration Effort
| | Low | High |
|---|---|---|
| Automatable calls (10 calls) | 2.5h | 3.8h |
| Manual review calls (10 calls) | 15h | 30h |
| Validation & testing | 5.3h | 10.1h |
| **Total** | **22.8h** | **43.9h** |
> Estimates are directional planning guides based on call-site complexity. Actual effort
> depends on test coverage, team familiarity, and provider setup. FlagLint does not access
> runtime data or LaunchDarkly billing.
Migration readiness: 50/100 · moderate
[█████████████░░░░░░░░░░░░] 50%
10 safely automatable · 10 require manual review
Estimated migration effort: 22.8h – 43.9h
Estimates are directional. See the report for assumptions.
The estimate breaks into three phases. Automation covers running flaglint migrate --apply, reviewing the generated diffs, and merging — roughly 0.25 engineer-hours per automatable call site. Manual review is where the range widens: each call site that requires human inspection is estimated at 1.5–3h, because the effort depends on what the surrounding code does with the evaluated value and how complex the flag key resolution is. Validation adds 30% of the combined automation and manual total for test runs, CI, and integration checks.
Supplying --hourly-rate converts the estimate to an engineering cost range:
This appends Estimated cost: $3,420 – $6,585 to the summary output. It is a planning heuristic calibrated to call-site complexity, not a billing projection.
Three call types drive the high-risk category in this service:
Dynamic flag key (8 usages across 3 files) — the flag key is a variable or template literal rather than a string literal. In this service, flags-wrapper.ts is the source: it accepts flagKey as a parameter and calls the LaunchDarkly SDK internally. FlagLint classifies it as a wrapper and marks every call through it as high risk because it cannot statically determine which flag is being evaluated, verify the call type, or confirm the return type. The resolution is to extract each dynamic key path to a named constant per call site so subsequent flaglint audit runs can classify them as automatable.
Detail evaluation (1 usage) — boolVariationDetail returns an evaluation reason object alongside the flag value. OpenFeature has a getBooleanDetails equivalent, but the reason vocabulary differs from the LaunchDarkly SDK (TARGETING_MATCH vs RULE_MATCH). Code that inspects reason.kind or reason.ruleId must be updated alongside the call site. FlagLint cannot safely generate that transformation.
Bulk call (1 usage) — allFlagsState has no OpenFeature provider equivalent. This call type requires an architecture decision before the migration can proceed: enumerate the specific flag keys needed explicitly, or retain the LaunchDarkly SDK client for the bootstrap path while migrating all other call sites to OpenFeature.
The call-site difference between a high-risk and an automatable entry is visible in source:
// High risk — dynamic flag key, cannot be rewritten automatically
const result = await ldClient.boolVariation(flagKey, ctx, false);
// Automatable — static flag key, safely rewritable
The only structural difference in the automatable rewrite is argument order: the OpenFeature provider convention places the fallback value at position two and the evaluation context at position three. The flag key is preserved exactly. No flag evaluation logic at LaunchDarkly changes.
For teams that need to share the findings with engineering leads or allocate sprint capacity, --format html produces a self-contained file with no external dependencies:
The file includes the summary card row, the effort estimate table, and the sortable flag debt inventory. It can be attached to a JIRA ticket, linked in a PR description, or opened locally. No LaunchDarkly credentials appear in the output — the report contains only what the static scan detected.
These files must migrate to OpenFeature before this rule passes.
Run `flaglint migrate --dry-run` to review the migration plan.
Add this command to your CI pipeline. flaglint validate --no-direct-launchdarkly exits non-zero when any direct LaunchDarkly SDK call is detected, blocking regressions as the migration lands across multiple PRs. The validate gate is the mechanism that turns a migration plan into a contract.
As you resolve manual-review call sites — extracting dynamic flag keys to named constants, migrating detail evaluations by hand, replacing bulk calls with enumerated evaluations — re-run the audit to watch the readiness score climb. At 80 or above, flaglint migrate --apply can handle the remaining LaunchDarkly flag debt in a single automated pass, and the CI validate gate will confirm the boundary is clean.
Run flaglint migrate ./src --dry-run and you will see two kinds of results: call
sites with a generated diff and call sites marked skip — manual review required.
The skipped calls are not bugs in the tool. They are patterns where a mechanical
rewrite would change runtime behavior in ways the tool cannot prove are safe.
This article covers the five patterns that produce skips and what you need to do for each.
A provider outage can expose how deeply application code depends on a single
feature-flag SDK. OpenFeature creates a neutral application boundary without
forcing teams to abandon LaunchDarkly.
This article walks through the local audit, migration preview, and CI enforcement
path that lets teams add that boundary incrementally.
LaunchDarkly and OpenFeature both evaluate flags with three arguments, but the
fallback and context positions are reversed. A naive codemod can produce
valid-looking code that silently changes runtime behavior.
This article shows the argument-order trap and why FlagLint uses AST analysis
before rewriting any call site.