Feature flag debt

Every flag is a decision you already made. The rollout finished, the losing branch deleted, and the flag stayed. It's still evaluating though, and it's cluttering your codebase. This scout finds the flags you forgot and opens the pull request that takes them out.

What the scout looks at

The scout looks for patterns like Fig. 1: a rollout hit 100% four months ago, and the evaluations never stopped. A decision you already made and moved on with is still being asked ~52,000 times a day.

FlagRolloutAgeIn codeVerdict
new-checkout-flow100%4 mo3 filesRemove
beta-nav0%5 mo1 fileRemove
legacy-export100%7 monot foundAsk first
Fig. 1What the scout saw: every flag still evaluating, and how long each decision has been finished.

If the rollout were still moving, this would be a live experiment, not debt. This is how the scout determines what to act on.

The report in your inbox

When your product emits signals that indicate flags have gone stale, a scout opens a report – and Fig. 2 breaks the card down part by part. Click Review and the report opens into Fig. 3: the full evidence, the raw signals, and the button that turns it into a pull request. Yours would name your flags, your repos, your call sites.

P33 flags at a terminal rollout for 4+ months, still evaluated new-checkout-flow and beta-nav finished months ago and are still evaluated – legacy-export is left as an open questionScout · flag debtReadyActionable1d ago
ArchiveReview
Fig. 2Once surfaced, your inbox surfaces this report card. The card includes its claim and triage, with the evidence sitting inside the report itself.Hover over the figure to learn about each element.
P33 flags at a terminal rollout for 4+ months, still evaluated ReadyActionableScout · flag debt· 1d ago
ArchiveCreate PR
Summary

Safe to clean up. new-checkout-flow has been at 100% for four months with no edits, and its key still appears in three files – all of them taking the same branch every time. beta-nav has been at 0% for five months, with its key in one file behind a conditional that never fires. Neither is experiment-linked, and no other flag depends on them.

Open question. legacy-export is at 100% and still evaluated ~11,000 times a day, but its key doesn't appear in any connected repo. 94% of those evaluations come from posthog-android versions older than the cleanup release, so this looks like shipped clients rather than live code – deleting it would flip them to the off path. Worth confirming before anything is removed.

3 flags · ~52,000 evaluations/day

Suggested fix: Remove the call sites for new-checkout-flow and beta-nav. Once that's deployed and evaluations stop, you can delete the flags themselves – that's the follow-up, not this diff.

Evidence2 sources
Your feature flagsWhich flags still evaluate, how often, at what rollout, and how long they've sat there.
Your codebaseYour repo is a signal source too. Whether the flag key still appears in code decides between cleanup and a question.
Fig. 3The report opened. From here, you can review the evidence and open a pull request.Hover over the figure to learn about each element.

The pull request you merge

When you're ready to open a pull request, the agent does it for you, attaching the evidence along the way. You review and merge it like any other – nothing merges without you.

Reports are free. Pull requests are $15 each, with your first three a month free. See self-driving Pricing.

The scout itself

Scouts are the heart of the self-driving system – they're instructions an agent follows on a schedule – Fig. 4 is the actual file for this guide's custom scout.

signals-scout-flag-debt/SKILL.md
---
name: signals-scout-flag-debt
description: >
Finds feature flags whose decision is over but which are still evaluated in production –
terminal rollouts left in place, or keys that no longer appear in any connected repo.
allowed_tools:
- emit_report
- edit_report
---
# Feature flag debt
Find flags whose decision is over while something is still evaluating them.
Narrower than `signals-scout-feature-flags`, which watches the whole flag surface – evaluation
cliffs, ghost keys, response-distribution shifts, flag debt – from PostHog data alone. This one
adds the half that scout cannot see: your repository. Run it if your flags routinely outlive
your cleanup PRs. If you want the analysis on demand rather than on a schedule, the
`cleaning-up-stale-feature-flags` and `finding-deleted-feature-flags` skills already do it, and
`auditing-experiments-flags` covers experiment-linked flags.
Weekly rather than the daily fleet default: flag debt accumulates over months, so a daily run
would re-read the same roster and write nothing six days out of seven.
**Term collision, worth knowing.** The canonical scout uses **ghost flag** for the opposite case
– code calling a key with no flag behind it, where the SDK silently returns `false`. That is a
different finding with a different fix. Don't reuse the term here.
## Discriminator
Emit a report when a flag is still emitting `$feature_flag_called` evaluations **and** either
- **(a)** it has been at 100% or 0% with no edits for 60+ days, or
- **(b)** its key does not appear in any connected repository.
Write nothing for a flag whose rollout percentage changed recently, or that was edited in the
last 30 days – that flag is mid-rollout and doing its job.
The two cases are not the same finding. **(a)** is cleanup you can do: the code is there and it
takes the same branch every time. **(b)** is a question – something is calling that key and it
isn't in the code you can see. Never treat (b) as a delete candidate on its own.
### Coverage caveat
`$feature_flag_called` fires on explicit evaluation calls. Server SDKs using local evaluation,
bootstrapped flags, and clients with flag-event capture disabled may not emit it at all, so
**absence of the event is weak evidence that a flag is unused**. Lean on PostHog's server-side
`STALE` status, which accounts for `last_called_at`, rather than on raw event absence.
## Explore
1. List flags with recent evaluations, plus their rollout percentage, tags, and last-modified
date.
2. Search every connected repository for each candidate key. Three outcomes, not two:
- **A string literal, or a constant that resolves to one** – a definite answer either way.
- **Dynamically constructed** – a key assembled at the call site from a variable. A repo
search can't resolve these, and they are the main source of false "the key is gone"
conclusions. Treat a nearby prefix or template match as uncertainty, never as a delete
recommendation.
- **Not found anywhere** – case (b). Go to step 3 before concluding anything.
3. For a case (b) flag, work out what is still calling it. The plausible answers are all things
this scout can't see: a repo that isn't connected, shipped mobile clients still running old
code, or a third-party or SDK consumer. Name the possibilities in the report, and check the
`$lib` and `$lib_version` spread on the evaluations – old client versions point at shipped
code you can't deploy over.
4. Before recommending any removal, confirm the flag isn't load-bearing:
- **Attached to an experiment**, with a non-empty `experiment_set` – leave it alone entirely;
deleting it breaks the experiment and its results.
- **Referenced by another flag's release conditions** – flag dependencies exist, and deleting
the parent changes what the dependent serves.
- **Consumed for its payload** – a flag can sit at 100% and be read only for its payload
value, so the key shows up in a config fetch rather than in a branch.
- **Tagged as excluded** – see the disqualifiers.
5. Quantify the reach: evaluations per day and distinct persons. That is what makes the report
worth someone's afternoon. It is **not** a billing argument – `$feature_flag_called` is
excluded from billable event volume – so don't put a dollar figure on it. The cost here is a
code path nobody can reason about, not an invoice.
6. Bundle the findings into one report rather than filing one per flag, and keep the two cases
visibly separate inside it.
## Suggested action
Deleting a flag is not a no-op. Once the flag is gone, evaluation returns `false` or `undefined`
– not the last value it served. Any live client still evaluating a deleted 100% flag flips to
the off path, which is a production behavior change.
So the order matters, and it isn't one pull request:
1. Remove the call sites, and deploy.
2. Confirm evaluations stop.
3. Then delete the flag.
Recommend step 1 – that's the PR an agent can open. Steps 2 and 3 are the follow-up you name in
the report, not something to bundle into the same diff.
## Disqualifiers
- The key appears in the repo, even only in tests or config.
- The key may be dynamically constructed and the search can't rule it out.
- The flag was edited recently, or its percentage is between 1 and 99.
- The flag is experiment-linked, has dependent flags, or is read for its payload.
- The flag carries an exclusion tag. Kill switches and ops toggles look exactly like debt and
must never be recommended for removal – but most of them carry no description, so don't try to
infer intent from prose. Ask the team to tag them (`keep`, `ops`, `kill-switch`) and read the
tag. Record anything you're told to leave alone in your scratchpad so the exclusion survives
to the next run. The durable way to stop a scout re-flagging something is to give it somewhere
to remember, not to hope for good metadata.
- Coverage is partial. If any repo that evaluates flags isn't connected, case (b) findings are
unsafe – report them as open questions only.
Fig. 4The scout itself – the file an agent follows on a schedule.

It reads more than one signal source, so the report can name a likely cause, not just a number that moved.

  • Your feature flags

    Which flags still evaluate, how often, at what rollout, and how long they've sat there.

  • Your codebase

    Your repo is a signal source too. Whether the flag key still appears in code decides between cleanup and a question.

The schedule it runs on

This custom scout runs daily by default. You can choose to run it every 30 minutes up to once a day. It only writes when something is wrong, so most days it's silent.

Add this to your scout troop

This is how the scout appears in PostHog once it's running – a scheduled agent that files a report when it finds something.

Preview

  • RequiredFeature flags in use
  • RequiredYour codebase connected via GitHub – every repo that evaluates flags, not just the main one. A repo the scout can't see looks exactly like code that no longer exists.
Add this scoutOpens PostHog with it prefilled. Review it, then hit Create.

Not set up yet? One command installs PostHog, connects GitHub, and turns on a default troop of scouts. That troop doesn't include this one – add it above once you're set up.

npx @posthog/wizard self-driving
signals-scout-flag-debtAdd this scout