The idea
A command-line tool that reads every feature flag reference in your codebase, asks your flag provider how each one has actually evaluated over the last 30 days, and classifies them: fully rolled out, fully off, still split, or orphaned. For flags in the first two buckets it rewrites the code — inlining the winning branch, deleting the dead one — and opens a pull request per flag. For orphans (keys in the provider that no code reads any more) it offers to archive them.
Why build this
Every team that adopts flags accumulates them. The rollout finishes, the flag stays at 100%, and the if stays in the code forever because deleting it is nobody's ticket and touching a conditional that guards a payments path feels risky at 4pm on a Thursday. Two years in, a mid-size codebase carries hundreds of permanently-true flags, each one a branch that tests still have to cover and readers still have to reason about.
The providers know this is a problem and mostly punt on it. LaunchDarkly, Unleash, PostHog and Flagsmith all surface some version of a "stale flags" list, but they stop at telling you. The gap is the removal: the mechanical, boring, error-prone part where someone has to work out whether !isEnabled('new-checkout') means the code below is dead or alive. That is exactly the kind of transformation an AST tool does reliably and a human does at 2am with a typo.
What makes it tractable now is that flag SDKs have converged on a tiny, greppable call surface — one function, a string key, a default — and that every major provider exposes evaluation counts through a REST API. You do not need to understand the program to find the call sites; you need to understand one function signature per language.
Stack sketch
- Go for the CLI, distributed as a single static binary plus a GitHub Action wrapper.
- Tree-sitter for parsing, with per-language queries matching the SDK call shapes — TypeScript/JS, Python, Go, Java, Ruby to start. Tree-sitter over regex because you need the enclosing
ifnode, not just the string. - ts-morph for the TypeScript rewrite (it preserves formatting and comments far better than a raw AST print) and
libcstfor Python; for Go,dstrather thango/astso comments survive. - Provider adapters behind one interface: LaunchDarkly's flag status API, Unleash's metrics endpoint, PostHog's
/api/feature_flag/:id/, Flagsmith's analytics. Each returns{key, lastRequested, variantShare}. - go-github for PR creation, one branch per flag, with the provider's 30-day evaluation chart pasted into the description as evidence.
- SQLite for local state: which flags you have already proposed, which PRs were rejected, so reruns do not re-open the same PR weekly.
Scope for v1
In: TypeScript and Python. LaunchDarkly and Unleash adapters. Boolean flags only. A scan command that prints the classification table, and a clean --flag <key> command that opens exactly one PR. Conservative refactor rules — only rewrite when the flag call is the entire if condition, or directly negated. Anything more complex (flag && user.isAdmin, flags stored in a variable, flags read in a template) gets reported with a file:line list and left alone.
Deliberately out: automatic merging, non-boolean/multivariate flags, config-file-based flag systems, and any attempt to resolve flags behind indirection. Those are where wrong answers ship bugs.
Where it could go
The obvious next step is prevention rather than cleanup: a CI check that fails when a new flag lands without an expiry date in a comment or in the provider's description field, so the tool is enforcing a policy rather than fighting a backlog. That is a much easier sell to a platform team than a one-off cleanup script, and it turns the tool into something that runs on every PR.
From there the same scan produces a genuinely useful artifact: a flag inventory tied to owners, age, and evaluation volume, which is the input to questions like "which of our flags guard code paths no customer has hit in a year". Multivariate flags and experiment cleanup are the natural third step, though they need real judgement about which variant won and are better left semi-automatic.
Watch out for
The refactor has to be boringly correct — one bad rewrite of a billing conditional and nobody trusts the tool again, so bias hard toward reporting instead of rewriting when the pattern is not exactly what you expect. Watch provider rate limits too: naive per-flag API calls will hit LaunchDarkly's limits on a repo with 400 flags, so batch and cache aggressively.