flag-prune does one job: the mechanical, deterministic part of removing a
feature flag. It is most effective as one step in a short pipeline that ends
with your project’s own checks and, optionally, a judgment-based cleanup pass by
an AI agent.
The guiding split is:
Keeping these separate makes large cleanups faster, cheaper, and easier to review than asking an agent to do everything.
Run these steps in order and commit between them so each diff stays small.
flag-prune - replace the flag and remove the code that can no longer
run. Preview first, review the diff, then write.
npx flag-prune --set 'useFlag("new-checkout")=true' src
npx flag-prune --set 'useFlag("new-checkout")=true' --write src
By default it also de-scopes the blocks it can safely flatten; pass
--no-flatten-blocks to keep them. See
Safety guarantees.
tsc --noEmitflag-prune reprints a moved statement it may add a semicolon or switch a
quote style; the formatter (Prettier, Biome, or ESLint) normalizes this, so
run it before review.flag-prune intentionally does not delete unused
files, exports, or imports that are still referenced elsewhere. Run a
dedicated tool such as Fallow or Knip
to remove what is now unreachable across the project.flag-prune is deliberately conservative. When it removes a branch, it keeps
enough structure to guarantee safety, which can leave code that is correct but
not yet idiomatic. Typical leftovers:
let declared for both branches that could now be a single const.These are judgment calls, so they are a good fit for an LLM rather than a codemod.
Considering initial code:
export async function getSettings() {
let user: Awaited<UserType>
if (flagValue) {
user = await getUser()
} else {
user = await getLegacyUser()
}
return verifyAccess(user)
}
After flag-prune resolves flagValue to true, you might have:
export async function getSettings() {
let user: Awaited<UserType>
user = await getUser()
return verifyAccess(user)
}
An LLM can finish the cleanup into idiomatic code, which a deterministic tool will not attempt:
export async function getSettings() {
const user = await getUser()
return verifyAccess(user)
}
Give the agent the changed files and a clear, bounded instruction. A prompt that works well:
Clean up code left by an automated feature-flag removal. Improve readability only; do not change runtime behavior.
Safe refactors: merge a `let` that is now assigned exactly once into a `const`; inline a variable that is read once; collapse a trivially nested conditional.
Preserve evaluation order and every side effect (calls, awaits, getters). Do not change behavior, types, or exports. If a change is not clearly safe, leave it. Return only the edited files.