Automatically remove stale feature flags from your codebase using AI coding agents.
Every team accumulates feature flags that outlive their purpose — flags rolled out to 100% months ago, experiments that were killed but never cleaned up, toggles nobody remembers adding. They clutter your code, confuse new developers, and make refactoring harder.
bye-bye-flag connects to your feature flag provider (PostHog, with more coming), identifies stale flags, and dispatches AI coding agents to remove them in parallel — cleaning up conditionals, dead code paths, unused imports, and orphaned tests across multiple repositories. Point it at a backlog of 50 stale flags and walk away; come back to a stack of draft PRs ready for review.
We built this at Relevance AI and run it against our own production codebases. It's actively maintained and improving.
- Node.js 24+ (native TypeScript support)
- git
- Agent CLI: at least one coding-agent CLI in your
PATH- Built-in adapters: Claude Code CLI (
claude) and Codex CLI (codex) - Generic adapter: any CLI command configured via
agent.type/agent.command - macOS: if you installed the Codex app, the CLI binary is bundled at
/Applications/Codex.app/Contents/Resources/codex. To expose it on yourPATH:ln -s /Applications/Codex.app/Contents/Resources/codex ~/.local/bin/codex
- Built-in adapters: Claude Code CLI (
- GitHub CLI: Required for creating PRs (not needed for
--dry-run)
- Clone and install:
git clone https://github.com/RelevanceAI/bye-bye-flag.git
cd bye-bye-flag
nvm use
pnpm install- Set up your target repos directory:
mkdir .target-repos
cd .target-repos
git clone [email protected]:your-org/your-repo.git- Create a config file (
.target-repos/bye-bye-flag-config.json):
{
"agent": {
"type": "claude",
"timeoutMinutes": 60
},
"fetcher": {
"type": "posthog",
"projectIds": ["12345"]
},
"orchestrator": {
"concurrency": 2,
"maxPrs": 2
},
"repos": {
"your-repo": {
"baseBranch": "main",
"setup": ["pnpm install"]
}
}
}- Add your PostHog API key (see Environment Variables):
cp .env.example .env
# Edit .env with your POSTHOG_API_KEY- Run it:
pnpm start run --target-repos=./.target-reposThis will fetch stale flags from PostHog, spin up agents in parallel, and open draft PRs for review. See examples/ for more config options.
Set up your repos directory with the following structure:
my-repos/
bye-bye-flag-config.json # Config file (required)
CONTEXT.md # Optional context for the AI
repo1/ # Git repository
repo2/ # Git repository (can have 1 or more repos)
If you're running from source, replace bye-bye-flag with pnpm start.
Pass --target-repos to point at the directory that contains bye-bye-flag-config.json and all target repos.
# Run the orchestrator - fetches stale flags and processes them
bye-bye-flag run --target-repos=/path/to/target-repos
# Dry run to preview what would happen
bye-bye-flag run --target-repos=/path/to/target-repos --dry-run
# Find flags with no code references (quick wins to delete from PostHog)
# Set "orchestrator.maxPrs": 0 in config, then run:
bye-bye-flag run --target-repos=/path/to/target-repos
# Process flags from a custom JSON file
bye-bye-flag run --target-repos=/path/to/target-repos --input=my-flags.json
# Remove a single flag manually (for testing)
bye-bye-flag remove --target-repos=/path/to/target-repos --flag=enable-dashboard --keep=enabled| Option | Default | Description |
|---|---|---|
--target-repos=<path> |
(required) | Path to target repos root |
--input=<file> |
(fetcher) | Use a JSON file instead of fetcher |
--dry-run |
false | Run agents in dry-run mode (no PRs) |
| Option | Description |
|---|---|
--flag=<key> |
The feature flag key to remove (required) |
--keep=<branch> |
Which code path to keep: enabled or disabled (required) |
--target-repos=<path> |
Path to target repos root (required) |
--dry-run |
Preview changes without creating a PR |
--keep-worktree |
Keep the worktree after completion for manual inspection |
Test your bye-bye-flag-config.json setup commands without running the full orchestrator:
# Test all repos
pnpm test-setup --target-repos=/path/to/target-repos
# Test a specific repo
pnpm test-setup --target-repos=/path/to/target-repos --repo=my-api
# Test only worktree setup (skip mainSetup)
pnpm test-setup --target-repos=/path/to/target-repos --skip-main-setup
# Test only mainSetup (skip worktree creation)
pnpm test-setup --target-repos=/path/to/target-repos --skip-worktreeThis creates a temporary worktree, runs your setup commands, and cleans up. Useful for debugging setup failures.
- Scaffold: Creates git worktrees with a fresh branch (
remove-flag/<flag-key>) for each repo - Search: Finds all usages of the flag key (including variations like camelCase, SCREAMING_SNAKE_CASE)
- Remove: Removes flag conditionals, keeping the specified code path
- Clean up: Removes dead code, unused imports, and orphaned files
- Verify: Runs typecheck, lint, and tests
- PR: Commits changes and creates a draft pull request for each repo with changes
The agent runs in git worktrees, which isolates all file changes from your main repositories.
The tool is designed to be idempotent and safe to run multiple times:
- Open PRs block re-runs: If a PR already exists for a flag, the tool will refuse to run and point you to the existing PR.
- Closed/merged PRs allow re-runs: If a PR was merged or closed (without being declined), you can run the tool again to create a fresh PR.
- Declined PRs: If a PR is closed because the flag removal was rejected, add
[DECLINED]to the PR title. This prevents future removal attempts for that flag. - Worktrees are preserved: After creating a PR, the worktree is kept for resuming the agent session. Worktrees are automatically cleaned up when their PR is merged or closed.
If you need to make changes or continue working on a PR, the PR description includes a resume command matching the configured agent:
# Claude Code
cd /tmp/bye-bye-flag-worktrees/remove-flag-my-flag && claude --resume <session-id>
# Codex CLI
cd /tmp/bye-bye-flag-worktrees/remove-flag-my-flag && codex resume <session-id>This lets you continue the agent session with full context of what was already done.
Create a bye-bye-flag-config.json in your repos directory:
Example files are available in examples/.
{
"fetcher": {
"type": "posthog",
"projectIds": [12345, 67890],
"staleDays": 30
},
"worktrees": {
"basePath": "/tmp/bye-bye-flag-worktrees"
},
"orchestrator": {
"concurrency": 3,
"maxPrs": 10,
"logDir": "./bye-bye-flag-logs"
},
"repoDefaults": {
"shellInit": "source ~/.nvm/nvm.sh && nvm use",
"baseBranch": "main"
},
"repos": {
"my-api": {
"baseBranch": "development",
"setup": ["pnpm install", "pnpm run codegen"]
},
"my-frontend": {
"shellInit": "source ~/.asdf/asdf.sh",
"baseBranch": "main",
"setup": ["pnpm install"]
}
}
}agent.type: Agent identifier. Built-in values areclaudeandcodex(default:claude)agent.args: Extra CLI args appended to the agent invocation (optional)agent.timeoutMinutes: Timeout for a single agent run, in minutes (default: 60)agent.command(generic agents): CLI command to execute (defaults toagent.type)agent.promptMode(generic agents):stdin(default) orargagent.promptArg(generic agents): prompt flag whenpromptModeisarg(default:-p)agent.versionArgs(generic agents): args used for prerequisite check (default:["--version"])agent.sessionIdRegex(generic agents): regex to extract session IDs from outputagent.resume(generic agents): resume command templates used in PR metadata- Parse failures automatically trigger a second call to the same configured agent to normalize output into the expected JSON shape
Built-in agent example:
{
"agent": {
"type": "codex",
"args": ["--model", "o3"],
"timeoutMinutes": 60
}
}Generic agent example:
{
"agent": {
"type": "opencode",
"command": "opencode",
"args": ["run"],
"promptMode": "stdin",
"resume": {
"withSessionId": "cd {{workspacePath}} && {{command}} resume {{sessionId}}",
"withoutSessionId": "cd {{workspacePath}} && {{command}} resume"
}
}
}fetcher.type: Which fetcher to use (posthogormanual)fetcher.projectIds: PostHog project IDs to fetch flags from (required forposthog)fetcher.staleDays: Days since last update to consider a flag stale (default: 30)fetcher.host: PostHog host (optional, default:https://app.posthog.com)
orchestrator.concurrency: Max agents running in parallel (default: 3)orchestrator.maxPrs: Stop after creating this many PRs (default: 10)orchestrator.logDir: Directory for agent logs (default:./bye-bye-flag-logs)
worktrees.basePath: Where to create worktrees (optional, default:/tmp/bye-bye-flag-worktrees)
repoDefaults.shellInit(optional): Default command to run before each shell command (setup and agent)repoDefaults.baseBranch(optional): Shared base branch for worktree creation and code searchrepoDefaults.mainSetup(optional): Default mainSetup commands (applied to every repo unless overridden)repoDefaults.setup: Default setup commands for worktrees (recommended)repos.<name>.shellInit(optional): Override shellInit for a specific reporepos.<name>.baseBranch(optional): Override base branch for a specific repo (for exampledevelopment)baseBranchis required for every repo via eitherrepoDefaults.baseBranchorrepos.<name>.baseBranch(no implicit fallback)repos.<name>.mainSetup(optional): Override mainSetup commands for a reporepos.<name>.setup(optional): Override setup commands for a repo (supports${MAIN_REPO}substitution)
Simple setup: Install dependencies in each worktree (most compatible, slower):
{
"repoDefaults": {
"baseBranch": "main",
"setup": ["pnpm install"]
},
"repos": {
"my-repo": {}
}
}Optimization tip: Use mainSetup to run pnpm install once on the main repo (to populate the pnpm store), then use setup to prefer cached packages in each worktree:
{
"repos": {
"my-repo": {
"mainSetup": ["pnpm install"],
"setup": ["pnpm install --frozen-lockfile --prefer-offline"]
}
}
}This avoids network downloads for every flag while keeping the worktree node_modules layout consistent.
If you need to guarantee no network access, use --offline instead. Note: --offline will fail if the required packages are not already present in the pnpm store.
If you've verified that copying/linking node_modules works for your repo, you can do that instead (faster, but less portable):
{
"repos": {
"my-repo": {
"mainSetup": ["pnpm install"],
"setup": ["cp -al ${MAIN_REPO}/node_modules ./node_modules"]
}
}
}You can provide additional context to the agent by placing markdown files in your repos directory:
CLAUDE.md- Agent instructions (coding standards, patterns to follow)CONTEXT.md- General context about how the repositories relate
These files are automatically included in the prompt.
Create a .env file (copy from .env.example) if you're using the PostHog fetcher:
# PostHog integration (required for fetching stale flags)
POSTHOG_API_KEY=phx_xxxThe PostHog fetcher finds flags that are candidates for removal.
Criteria for stale flags:
- Updated more than 30 days ago (configurable)
- Either 0% or 100% rollout (no partial rollouts or complex targeting)
- No payload
- No multivariate variants
- If flag exists in multiple projects, must be consistent across all
Inactive flags are also included with keepBranch: "disabled".
# Fetch stale flags
pnpm run fetch:posthog -- --target-repos=/path/to/target-repos
# Custom stale threshold (days)
pnpm run fetch:posthog -- --target-repos=/path/to/target-repos --stale-days=60
# Show all flags with their status (not just stale ones)
pnpm run fetch:posthog -- --target-repos=/path/to/target-repos --show-allOutput is JSON to stdout:
[
{
"key": "old-feature",
"keepBranch": "enabled",
"reason": "100% rollout for 45 days",
"lastModified": "2025-12-01T00:00:00.000Z",
"metadata": {
"projects": ["12345", "67890"]
}
},
{
"key": "killed-feature",
"keepBranch": "disabled",
"reason": "Inactive for 90 days",
"lastModified": "2025-11-01T00:00:00.000Z",
"metadata": {
"projects": ["12345"]
}
}
]This can be piped to other tools or used to drive the removal agent.
════════════════════════════════════════════════════════════
bye-bye-flag Orchestrator
════════════════════════════════════════════════════════════
Repos directory: ./my-repos
Concurrency: 2
Max PRs: 10
Dry run: false
════════════════════════════════════════════════════════════
Logs: ./bye-bye-flag-logs/2024-01-15T10-30-00
Fetching latest from origin...
Fetching my-api...
Fetching my-frontend...
Running main setup on repos...
my-api:
Running: pnpm install
Fetching stale flags...
Fetched 25 stale flags
Checking for existing PRs...
Fetching PRs from my-api...
Found 3 bye-bye-flag PRs
Fetching PRs from my-frontend...
Found 2 bye-bye-flag PRs
⊘ old-feature: Open PR exists (skipping)
15 flags passed PR check (1 skipped)
Checking for code references...
○ unused-flag: No code references
12 flags have code references (3 have no code)
Processing up to 10 PRs with concurrency 2...
▶ Starting: enable-dashboard (0/10 PRs)
Keep: enabled | Worktree: /tmp/bye-bye-flag-worktrees/remove-flag-enable-dashboard
▶ Starting: new-feature (0/10 PRs)
Keep: enabled | Worktree: /tmp/bye-bye-flag-worktrees/remove-flag-new-feature
✓ Complete: enable-dashboard (1 PR(s), 1/10 total, 2m 15s)
✓ Complete: new-feature (2 PR(s), 3/10 total, 3m 42s)
...
════════════════════════════════════════════════════════════
bye-bye-flag Run Complete
════════════════════════════════════════════════════════════
Fetcher: posthog (found 25 stale flags)
Duration: 15m 30s
Processed: 10 flags
✓ 10 PRs created
○ 0 no changes needed
○ 3 no code references
✗ 0 failed
⊘ 1 skipped
… 11 remaining (maxPrs limit)
PRs created:
• enable-dashboard: https://github.com/org/my-frontend/pull/123
• new-feature: https://github.com/org/my-api/pull/456
• new-feature: https://github.com/org/my-frontend/pull/457
...
Skipped:
• old-feature: Open PR: https://github.com/org/my-frontend/pull/100
Logs: ./bye-bye-flag-logs/2024-01-15T10-30-00
To continue processing remaining flags, run the command again.
When running with the built-in Claude preset, bye-bye-flag passes --dangerously-skip-permissions to give the agent unrestricted access within the worktree. This is necessary for non-interactive automated operation, but it means:
- The agent can read, write, and delete any files within the worktree
- The agent can execute arbitrary shell commands (typecheck, lint, tests, etc.)
Mitigations built in:
- All work happens in isolated git worktrees, not your main repository checkout
- Changes are submitted as draft PRs for human review before merging
- The tool never pushes to your default branch directly
If you are using a custom agent via agent.command, review its permission model to understand what access it has.
Contributions are welcome! Some areas where help would be especially valuable:
- Agent adapters — add presets for more coding agents (Aider, Amp, Cursor CLI, etc.)
- Flag fetchers — add integrations for LaunchDarkly, Statsig, Unleash, or other feature flag providers
- Cloud/CI execution — make bye-bye-flag runnable in CI pipelines or cloud environments (GitHub Actions, etc.)
- Bug fixes and improvements — better error messages, edge case handling, documentation
To get started:
git clone https://github.com/RelevanceAI/bye-bye-flag.git
cd bye-bye-flag
nvm use
pnpm install
pnpm testMIT