How the Engine Works
End-to-end architecture — how flags are detected, how the inventory stays accurate, and how stale flags become merged cleanup PRs.
FlagShark's engine has two halves. The first half watches your code and keeps an inventory of every flag in the default branch. The second half decides which flags are stale, transforms the code to remove them, and opens a pull request you can review and merge. Both halves run continuously across every connected repository.
This page is the canonical architecture reference. If you only want to know which SDK patterns are detected, see Flag Detection. If you want to see the resulting cleanup PRs in the dashboard, see Cleanup PRs.
Detection: From Code to Inventory
Two-Phase Architecture
Detection runs in two distinct phases:
- PR Preview Phase — when you open or update a pull request, FlagShark analyzes the diff and posts a comment showing which flags are being added or removed. This is a preview — flags aren't persisted yet.
- Lifecycle Tracking Phase — when code reaches your default branch (via merge or direct push), FlagShark detects the actual flag changes and persists them to your inventory. This is the source of truth.
PR Preview
Show what flags would be tracked, without committing them.
Flags are NOT persisted to your inventory at this stage.
Lifecycle Tracking
Persist flag changes when code reaches the default branch.
Source of truth — every flag in your default branch lands here.
Why Two Phases?
Single source of truth. By tracking flags only when they reach the default branch, FlagShark avoids ghost entries from PRs that get closed without merging. Open a PR adding new-checkout-flag, change your mind, close the PR — and the flag never enters your inventory. The inventory always reflects what's actually in production.
Complete coverage. Not all code changes go through pull requests. Hotfixes get pushed directly to main during incidents, automated tools commit on their own, legacy workflows bypass PRs entirely. FlagShark catches all of these. Every push to the default branch is analyzed, whether it came from a merged PR or a direct commit.
Accurate state. When code lands on the default branch, FlagShark compares the before and after states. Flags added in the change are emitted as FLAG_ADDED, flags removed as FLAG_REMOVED, and the exact commit SHA and author are captured.
What Gets Tracked Where
| Event | PR Comment | Dashboard Inventory | Notes |
|---|---|---|---|
| PR opened/updated | Preview shown | Not yet | Comment shows what will be tracked |
| PR merged | — | Flags tracked | Linked to PR number |
| Direct push to default | — | Flags tracked | No PR association |
| Push to feature branch | — | Not tracked | Only the default branch matters |
| Repository scan | — | Flags tracked | One-time discovery, no PR |
Phase 1: PR Preview
When you open or update a pull request, FlagShark:
pull_request webhook from GitHub- 1GitHub PR
- 2FlagShark
- 3Parse Code
- 4Detect Flags
- 5PR Comment
The comment shows flags being added (new to the codebase), flags being removed (cleanup), file locations and line numbers, and the SDK method used.
Phase 2: Lifecycle Tracking
When code reaches your default branch, FlagShark:
push webhook from GitHubFLAG_ADDED events for new flagsFLAG_REMOVED events for removed flags- 1Code changePR merge or direct push
- 2Push Detector
- 3Compare Commits
- 4Track Changes
- 5Dashboard
Merged PRs. When a PR is merged, GitHub sends a push event for the merge commit. FlagShark detects the flag changes, links them to the PR that introduced them, and captures the PR author as the flag owner.
Direct pushes. When code is pushed directly to main/master, FlagShark records the commit author and tracks the flag without a PR association. Direct pushes appear in your dashboard with the commit SHA and author, but without a linked PR — you still see exactly when and by whom each flag was added.
Commit Status Checks
After processing a push event, FlagShark posts a commit status check to GitHub:
| Status | Meaning |
|---|---|
| Success — No changes | No feature flag changes detected |
| Success — X tracked | X flag changes detected and tracked |
| Success — All tracked | Changes detected, all flags already in inventory |
This gives you visibility into flag detection directly in GitHub's commit history.
Deduplication
FlagShark prevents duplicate tracking three different ways:
- Webhook deduplication — each GitHub webhook has a unique delivery ID. FlagShark tracks processed deliveries to prevent double-processing.
- Flag deduplication — before emitting a
FLAG_ADDEDevent, FlagShark checks if the flag is already active in that repository. - Event idempotency — the same flag change won't create multiple events even if webhooks are retried.
The Cleanup Pipeline at a Glance
Once a flag is in the inventory, it's a candidate for cleanup. The cleanup pipeline runs continuously and is event-driven — a flag turned off in LaunchDarkly, a push to your default branch, a conflict on an open PR all flow through the rest of the pipeline without anyone clicking a button.
- 1
Scan & Index
tree-sitter ASTWalk the default branch, parse every supported file with tree-sitter, build the flag inventory.
- 2
Stale Decision
age + policyPick candidates by age threshold and per-workspace policy. Skip permanent flags and anything with an open PR.
- 3
Provider Sync
LaunchDarkly APICross-reference each candidate against your flag provider. Only archived flags survive to the next stage.
- 4
Transform
Polyglot PiranhaPolyglot Piranha rewrites the AST: removes the flag check, the dead branch, dead helpers, and stale imports.
- 5
Open Cleanup PR
Git Trees APIAtomic multi-file commit via GitHub Git Trees API. Body explains the flag, the provider state, and the diff.
- 6
Regenerate on Drift
auto re-runWatches the PR. On base-branch push, conflict, or manual click, re-runs the pipeline and force-pushes the new commit.
loops back to step 5
1. Scan & Index
The first time you connect a repository, FlagShark walks the entire default branch and builds the inventory described above. After that, the lifecycle-tracking phase keeps the inventory in sync with every push.
For each flag reference, the engine records where it appears (file, line, surrounding context) and who introduced it (via batched git blame). That metadata is what powers ownership, age, and "is this still in use" decisions later in the pipeline.
2. Stale Decision
A flag is a candidate for removal when all of these are true:
The age threshold is the policy lever most teams care about. Some teams want aggressive cleanup (anything past two weeks at 100% rollout); others want a long tail, only after the flag is archived in the provider. Both work — the threshold is configurable per workspace. New workspaces default to 30 days; the CLI defaults to 30 days when run standalone.
staleFlagDays). The standalone CLI defaults to 30 days. You can tune the per-workspace value in your dashboard settings.
Candidates are scored, deduplicated against open work, and queued. They aren't removed yet — at this stage the engine is just deciding what to act on.
3. Provider Sync
Before transforming code, the engine cross-references your flag provider. If a flag is still rolled out to 50% of users, removing it from the codebase would break production. So we ask the provider: is this flag archived? Is it at 100%? Is it gone?
The sync runs on a schedule and pulls the current state of every flag the engine has seen in your code. Flags that are confirmed safe to remove — archived in LaunchDarkly, or matching a provider-state policy you've configured — are passed through to the transformation stage. Flags that are still in flight stay in the inventory but aren't queued for cleanup.
4. Transform (Piranha)
This is the stage that does the actual code surgery. FlagShark wraps Polyglot Piranha, the open-source AST transformation tool from Uber, and uses it to rewrite each file that references the flag.
Piranha is not a find-and-replace. It parses your code into an AST, applies a set of language-specific rewrite rules, and re-emits the result. That means it can:
- Remove the flag check and the dead branch underneath it
- Collapse
if (true)and ternaries that became trivial after the flag was inlined - Delete helper functions that only the dead branch was calling
- Drop
importstatements that no other line references - Preserve your formatting, indentation, and comments on the surviving code
The rules are per-language and per-provider — a TypeScript codebase using useFlags() from the LaunchDarkly React SDK gets different rules than a Go codebase calling ldClient.BoolVariation. Rules are templated with the flag name at transformation time, so the same engine handles every flag without any per-flag configuration from you.
// Input
import { useFlags } from 'launchdarkly-react-client-sdk'
import { NewCheckout } from './NewCheckout'
import { LegacyCheckout } from './LegacyCheckout'
function CheckoutPage() {
const { enableNewCheckout } = useFlags()
if (enableNewCheckout) {
return <NewCheckout />
}
return <LegacyCheckout />
}
// Output (after Piranha, keeping enabled path)
import { NewCheckout } from './NewCheckout'
function CheckoutPage() {
return <NewCheckout />
}
A post-processing pass then handles the cleanup that AST rewriting can leave behind: stray blank lines, trailing whitespace, and import groups that need recompacting. For Go, this work happens inside the Piranha rules themselves; for other languages, it's a text-level pass after the AST rewrite.
5. Open Cleanup PR
Once the transformed file set is ready, the engine opens a pull request against your repository. Branch creation and the commit itself go through GitHub's Git Trees API, which lets the engine commit a multi-file change atomically — no matter how many files reference the flag, you get one commit, one branch, one PR.
The PR body explains what was removed and why: the flag name, where it was last referenced, how many files changed, and the provider state that justified removal. You review it like any other PR — CI runs, codeowners get tagged, your team approves and merges.
6. Regenerate on Drift
Real PRs don't stay clean. Your default branch keeps moving, conflicts appear, the flag's provider state can change while the PR is open. The engine's last responsibility is keeping these PRs alive without forcing you to babysit them.
Whenever a relevant event fires — a push to the base branch, a merge conflict, a manual "regenerate" click in the dashboard — the engine re-runs the full transformation against the latest state of the codebase, force-pushes the new commit, and posts a comment explaining what changed and why it regenerated. If the flag is gone (e.g. you merged a manual cleanup yourself), the PR is closed automatically.
Each regeneration is tagged with its trigger:
| Trigger | Reason | What happens |
|---|---|---|
| Initial creation | initial | First version of the PR |
| Push to base branch | push_to_base | Re-run against latest main |
| Merge conflict detected | conflict | Reset branch, re-transform, force-push |
| User clicked Regenerate | manual | Re-run with current settings |
This is why a FlagShark PR can sit open for a week without rotting — every time the world moves, the PR catches up.
What's Outside the Engine
A few things deliberately don't live in the engine, and it's worth being clear about them:
- Test execution. FlagShark doesn't run your tests. CI does. The engine produces a diff; your CI tells you whether it's safe.
- Merging. Always a human action.
- Custom flag patterns. If your team uses a non-standard wrapper around your SDK, you can teach the scanner about it via Configuration, but the engine itself doesn't infer custom patterns automatically.
- Non-default branches. The engine only tracks and acts on the default branch. Feature branches are noise from a flag-lifecycle perspective.
Related Documentation
- Flag Detection — SDK patterns, what's detected, debugging
- Cleanup PRs — the user-facing cleanup feature in detail
- Supported Languages — language and SDK coverage
- LaunchDarkly Integration — how provider sync is configured
- Configuration — custom flag patterns and per-repo settings
- GitHub Comments — how PR comments work