Archive or delete a stale feature flag. Two paths: archive (reversible, soft-disable) or delete (permanent; always goes through archive first as a safety gate). The skill coordinates code-cleanup with the agent's general Read/Edit tools, surfacing call sites from GrowthBook's Code References API (when configured) or falling back to Grep against the user's current directory.
All API calls go through the bundled helper: ${CLAUDE_PLUGIN_ROOT}/scripts/gb-call. It needs GB_API_KEY — set in your shell, or written to ~/.config/growthbook/.env by /growthbook:gb-setup. If unset or invalid, gb-call's error message points back at /growthbook:gb-setup.
Required inputs
Collect from the user before starting. Prompt for what's missing.
- Flag ID — kebab-case key. If the user gives a description, route to
flag-searchfirst to resolve. - Action —
archive(reversible) ordelete(permanent; includes archive). Inferred from wording: "archive" / "disable" → archive; "delete" / "remove" / "clean up" / "get rid of" → delete. Confirm before mutating.
Workflow
Track progress with this checklist. Do not skip or reorder; each step gates the next.
- [ ] 1. Fetch flag, verify safety preconditions (incl. bulk experiment check)
- [ ] 2. Compute defaultValue + detect behavior-change divergences
- [ ] 3. Find call sites (Code References API → Grep fallback w/ cwd check)
- [ ] 4. Walk the user through inlining, batched by file
- [ ] 5. Archive the flag (revision-and-publish; branch to 5a on 403, 5b on 409)
- [ ] 6. Verify-or-rollback gate (delete path only)
- [ ] 7. Delete the flag (delete path only)
- [ ] 8. Report, including reverse-prerequisite limitation warning
1. Fetch flag and verify safety preconditions
gb-call GET /api/v2/features/<flag-id>
Capture from the response: archived (boolean), defaultValue, valueType, environmentSettings (which envs are enabled), rules (the full array, including any experiment-ref rules), holdout (informational), project.
If 404, halt: "no flag with id <flag-id>." Suggest flag-search to list flags.
Run the safety checks. Halt if any of these fire:
-
archived === trueand the user asked to archive → already archived. Either re-confirm intent ("you said archive; this flag is already archived — did you mean delete?") or exit clean. -
archived === trueand the user asked to delete → this is a continuation of a prior cleanup session (or the user archived in the UI). Ask:This flag is already archived — looks like you're picking up where a previous cleanup left off. Did you already inline the flag's value at call sites in your codebase?
- Yes → I'll skip the behavior-change check and code-cleanup walkthrough; we'll go straight to the verify-or-rollback gate before delete.
- No → I'll still walk through steps 2–4 first, since the code references may still exist.
If yes, jump to step 6 (the archive step 5 is a no-op since
archived: true → truemakes no change server-side; the handler short-circuits whenfeature.archivedequals the requested value). -
neverStale: trueis set on the feature. Confirm via the staleness endpoint:gb-call GET '/api/v2/stale-features?ids=<flag-id>'If the response includes
staleReason: "never-stale", halt:This flag is marked
neverStale— someone explicitly said it should never be cleaned up automatically (kill switch, ops toggle, license gate, etc.). To proceed, remove theneverStaleflag in the GrowthBook UI at<host>/features/<flag-id>first and re-run me. -
Active experiment-ref rule pointing at a
runningexperiment. One bulk call covers the common case:gb-call GET '/api/v1/experiments?trackingKey=<flag-id>&status=running'This works because
experiment-launchsetstrackingKey === flag-idby convention. If the response array is non-empty, halt:This flag is wired into experiment
<exp-id>(<exp-name>) which is still running. Stop that experiment first (experiment-stop), then re-run me.Defensive cross-check. If the bulk call returns empty but the flag has
experiment-refrules in itsrulesarray, the experiment may have been manually wired withexperimentId ≠ trackingKey. Iterate the experiment-ref rules and fetch each one as a fallback:gb-call GET /api/v1/experiments/<experiment-id-from-rule>If any returns
status: "running", halt with the same message.Temporary rollout check. While fetching linked experiments, also note any with
status: "stopped"ANDenableTemporaryRollout: true— these are not a blocker (the experiment is stopped), but step 2 must use the winner value as the inline replacement. Surface a reminder:"Temporary rollout is active on experiment
<exp-id>— all users currently see the winner value. Step 2 will use the winner value as the inline replacement, notdefaultValue." -
Draft experiment referencing the flag. Lower-priority than running, but worth a warn-and-confirm. Reuse the bulk query with a different status filter:
gb-call GET '/api/v1/experiments?trackingKey=<flag-id>&status=draft'If non-empty, warn:
Note: this flag is the
trackingKeyfor draft experiment<exp-id>. Deleting the flag won't break anything immediately, but it will prevent that experiment from launching successfully. Proceed? -
Unresolved draft revision on the flag. A draft would be discarded by the archive:
gb-call GET /api/v2/features/<flag-id>/revisions/latestIf 404, no draft exists — continue. If a draft is returned, halt:
There's an active draft revision (
<version>) on this flag. Cleaning up the flag would discard those pending changes. Either publish or discard the draft in the GrowthBook UI at<host>/features/<flag-id>first. -
/stale-featuresdoesn't flag this as stale and the user is asking to delete. Warn but don't halt:Heads-up: GrowthBook doesn't flag this as stale. It's still enabled in
<env>with active rules. Proceed?
2. Compute the inline-replacement value and detect behavior changes
After archival, every rule stops evaluating — the flag returns defaultValue for all callers. The inline replacement value is usually defaultValue, but not always — see the temporary rollout case below.
Check for an active temporary rollout first. For each experiment-ref rule in the flag, fetch the linked experiment:
gb-call GET /api/v1/experiments/<experiment-id>
If experiment.status === "stopped" AND experiment.enableTemporaryRollout === true:
-
The experiment's
releasedVariationIdtells you which variation is serving 100% of traffic. -
Find that variation's value in the experiment-ref rule's
variationsarray. -
That value — not
defaultValue— is what all users currently see. -
Use it as the inline replacement value. Warn the user clearly:
"Temporary rollout is active on this flag. All users currently see the winner value
<winner_value>. After cleanup, all users will shift todefaultValue: <default_value>. If these differ, inlining<winner_value>is the correct replacement — not<default_value>."
For valueType: "json", surface the raw JSON-encoded string and let the user adapt the inline shape.
The real question this step exists to answer is: does cleanup change behavior in production?
Walk the rules array and flag anything that previously diverged from defaultValue:
- A
forcerule serving a different value to a targeted segment. - An active
rolloutrule (coverage > 0) — even one with no condition. - An
experiment-refrule: fetch the linked experiment. If stopped with temporary rollout, the winner value is the divergence (handled above). If still assigning traffic normally (stopped experiment, no temporary rollout), users are split across variations — archiving shifts everyone todefaultValue, which is a change for treatment-group users.
Surface a table:
Behavior-change check for <flag-id>:
defaultValue (post-cleanup): "true"
Pre-cleanup divergences (these stop applying after archive):
production: rule 1 (force, "beta testers" saved-group) served "false" → those users will now get "true"
production: rule 2 (rollout, 25% via id) served "false" → 25% of traffic will shift to "true"
staging: no divergence — all traffic gets "true" today
Is this the intent?
If the table is empty (no divergences), the cleanup is purely cosmetic and the user can proceed without further confirmation. If any rule diverged, halt and confirm — this is real behavior change.
3. Find call sites
Try Code References first:
gb-call GET /api/v1/code-refs/<flag-id>
The response is {codeRefs: [{ repo, branch, platform, refs: [{filePath, startingLineNumber, lines, flagKey}] }, ...]}. One document per (repo, branch) — a feature may have multiple entries if Code References has been pushed from multiple branches.
-
Non-empty response → present line-level references to the user, grouped by file, with branch info if multiple branches are represented. Cite that the data is from Code References (so the user knows it may be stale relative to local edits).
-
Empty response → either Code References isn't configured, or the flag genuinely isn't referenced. Before falling back to Grep, confirm the working directory:
Code References returned no results. I'll search the current directory (
<pwd>) for<flag-id>. Is that where the code that uses this flag lives, or should I look somewhere else?After confirmation, use the Grep tool to search for the flag ID (kebab-case key, exact match) in the project. Limit to relevant file types (source code, not vendored directories).
If both Code References and Grep return empty, surface:
Couldn't find any code references to
<flag-id>. If you're confident the flag isn't used anywhere, proceed to step 5. Otherwise, double-check the working directory or look at Code References at<host>/features/<flag-id>for stale data.
4. Walk the user through inlining
Batch by file, not by line. A single file may have multiple references to the same flag; asking the user to approve N separate Edits in the same file is friction. For each file with references:
- Use the Read tool to load the full file (once).
- Surface a per-file summary: file path + count of references + the lines that reference the flag.
- Propose a single Edit that replaces all references in the file with
defaultValue(or the user-confirmed inline value). - The Edit goes through Claude Code's normal permission flow — the user reviews and approves the whole-file change once.
- The user can also say "skip this file" (handle later) or "edit some but not all" (fall back to per-reference Edits within just that file).
Track which files have been handled and which were skipped. When the user signals "done with inlining" (or there are no call sites), proceed.
If the user wants to defer code-cleanup entirely ("just archive the flag now, I'll clean up code later"), allow it — the archive is reversible and the user can re-run this skill later to finish the code-cleanup before deletion.
5. Archive the flag
echo '{"archived": true}' \
| gb-call POST /api/v2/features/<flag-id> -
What happens server-side. This isn't a metadata patch. Setting archived triggers createAndPublishRevision server-side — a new revision is created and published atomically. The same failure modes that affect any v2 publish apply:
- 2xx → archived. Proceed to step 6 (delete path) or step 8 (archive-only path).
- 403 with "approval required" body → step 5a.
- 409 → step 5b (merge conflict; another actor changed the flag between our GET and our archive POST).
- Other 4xx → halt with the body.
A revision-history entry will appear for the archive event — surface this in step 8.
5a. Approval required for archive
Same three-option branch as flag-targeting, but with one critical asymmetry between archive and delete:
Your org requires approval before this flag can be archived. To proceed:
A. Standard review flow (recommended) — I'll request review on the change; a teammate approves it in the GrowthBook UI at
<host>/features/<flag-id>; you re-run me to resume.B. Org-wide bypass — admin enables "REST API always bypasses approval requirements" in Settings → General → Approvals. This single setting authorizes both archive and the final delete step.
C. Per-token bypass — use a PAT with
bypassApprovalCheckspermission. This authorizes archive but NOT delete. The per-token permission is intentionally a review-workflow bypass only, not a destructive-action override. If your end goal is archive-only, this works. If you want to delete, you'll still need the org-wide setting from option B (or an admin to do the delete in the UI). Surface this asymmetry to the user before they pick C — don't let them discover it at step 7.
For path A, request review on the draft and halt:
echo '{"comment":"Auto-requested by flag-cleanup"}' \
| gb-call POST /api/v2/features/<flag-id>/revisions/<version>/request-review -
Do not attempt submit-review — the API rejects self-approval.
5b. Merge conflict on archive
A 409 on the archive POST means the draft's base revision is stale — a teammate (or another agent invocation) published changes to the same flag between our step 1 (GET) and our step 5 (POST). Do not auto-rebase. Halt with:
Your archive of
<flag-id>couldn't be applied — the flag has changed since I last looked at it. To resolve:
- Open
<host>/features/<flag-id>in the GrowthBook UI.- Reconcile the change (either rebase our pending archive on top, or discard and re-run me to start fresh against the new live state).
Re-run me after the conflict is resolved.
Surface the conflict body verbatim. The POST /api/v2/features/<id>/revisions/<version>/rebase endpoint exists for opt-in resolution; v1 stays conservative for the same reasons flag-targeting does — merge resolution needs human judgment per conflicting field.
6. Verify-or-rollback gate (delete path only)
After archive succeeds, halt and confirm with the user. Frame the irreversibility of delete in the prompt itself, not just in a guardrail the user may not have read:
The flag is archived. At this point the action is fully reversible — unarchive in the UI (or reply "rollback") and everything goes back. Once we delete, the flag and all its revisions are gone permanently; there's no undo.
Before I delete:
- Check that nothing in your codebase broke. Run your tests; deploy to staging; verify the app behaves correctly with
defaultValueinstead of the old rules; whatever your normal post-flag-removal check is.- When you're confident, reply "proceed" and I'll delete.
- Reply "rollback" and I'll unarchive.
This is the load-bearing gate of the skill. Permanent deletion is a one-way door and the user should sit with the archived-but-not-deleted state for at least one verification cycle. Don't collapse the two steps even if the user pushes for it.
If the user picks "rollback": undo the archive by setting archived: false via the same endpoint:
echo '{"archived": false}' \
| gb-call POST /api/v2/features/<flag-id> -
This goes through the same revision-and-publish flow as the archive — same approval-required and merge-conflict failure modes apply. Verify it completes cleanly, surface the outcome, and exit.
7. Delete the flag (delete path only)
gb-call DELETE /api/v2/features/<flag-id>
- 2xx with
{deletedId}→ proceed to report. - 403 → almost certainly the
restApiBypassesReviewssetting is off. The flag is archived, just not deletable via the API. Surface:Your org requires the "REST API always bypasses approval requirements" setting to be enabled before flags can be deleted via the API. The flag is archived; you can either:
- Ask an admin to enable the setting (Settings → General → Approvals), then re-run me to finish the delete.
- Delete manually in the GrowthBook UI at
<host>/features/<flag-id>(the archived flag is still listed there).
Per-token
bypassApprovalChecksdoes not authorize this — it's intentionally a review-workflow bypass only, not a destructive-action override. - Other 4xx → halt with the body.
Server-side cleanup. Deletion removes the feature record, all its revisions, all revision logs, and unlinks any experiments that had this flag in their linkedFeatures. The experiments themselves are not deleted — they continue to exist with stale tracking keys. Surface this in step 8 so the user knows the experiments are still there.
8. Report
Print a summary:
- Flag ID and final state (
archivedordeleted). - For archive: revision-history entry was created (user's audit log will show it).
- For delete: linked experiments were unlinked (but not deleted); their tracking keys now point at a non-existent flag.
- Number of call sites found, files edited, and any that were skipped.
- Inline replacement value used (
defaultValuefor the flag), surfaced for transparency. - UI link:
<host>/features/<flag-id>— only useful for archived flags (deleted flags 404). Derive<host>fromGB_API_URLby swappingapi.→app.. - Limitation warning: "Heads-up: I can't detect whether other flags use this one as a prerequisite. If you're not sure, check the GrowthBook UI's 'Used by' panel before deleting — or after archive but before delete, since archive is reversible."
- Holdout warning (if
feature.holdoutwas present): "This flag was associated with holdout<holdout-id>. The holdout'slinkedExperimentsmay still reference experiments that pointed at this flag; their tracking keys are now stale. Audit the holdout in the GrowthBook UI if your experiment-analysis depends on it being clean." - Suggested follow-up: re-run Code References (CLI/Action) so the dashboard reflects the post-cleanup codebase state.
Guardrails
- Archive-before-delete is the canonical flow on live flags. The back-end rejects DELETE on non-archived features unless
restApiBypassesReviewsis set org-wide. Even if the setting is on, prefer archive→verify→delete for the safety pause. - Archive triggers a revision-and-publish. Setting
archived: trueviaPOST /v2/features/<id>creates and publishes a revision server-side. The same failure modes that affect any v2 publish apply — approval-required (5a) and merge-conflict (5b). bypassApprovalChecksis asymmetric across archive and delete.- Archive: per-token
bypassApprovalChecksworks (updateFeatureV2.tschecks both org-wide and per-token). - Delete: per-token
bypassApprovalChecksdoes NOT work (deleteFeature.tsexplicit comment: "review-workflow bypass, not destructive-action override"). - Only org-wide
restApiBypassesReviewsauthorizes both. Don't conflate them when surfacing approval options to the user.
- Archive: per-token
neverStale: trueflags are deliberately permanent. Halt if the user asks to clean one up; they need to explicitly remove that flag in the UI first.- Active running experiments block cleanup. A flag with an
experiment-refrule pointing at arunningexperiment cannot be cleaned up — stopping the experiment is a separate decision routed toexperiment-stop. - Unresolved drafts block cleanup. Surface the draft and ask the user to discard or publish first.
- Permanent deletion is a one-way door. The skill stops between archive and delete to let the user verify nothing broke. Don't collapse the two steps even if the user pushes for it. Frame the irreversibility in the user-facing prompt, not just in this guardrail.
- Code-cleanup is per-file approval, batched. Group call sites by file and propose one Edit per file. Don't batch across files; don't bypass the agent's normal Edit-tool permission flow.
- Code References can be stale. The data reflects the last CLI/GitHub Action run, not the live working tree. If the user reports edits that aren't in Code References yet, trust the working tree (or re-run the CLI).
/code-refsempty doesn't mean "not used in code." It could mean Code References isn't set up, or the user's working directory isn't the right codebase. Always confirm the working directory before assuming the flag is unreferenced.- One bulk experiment lookup beats N per-rule lookups. Use
listExperiments?trackingKey=<flag-id>&status=runningfor the primary check; fall back to per-rule fetches only when the bulk call returns empty butexperiment-refrules exist (manual-wiring edge case). - After archival, every rule stops evaluating —
defaultValueis what callers get. Step 2's job isn't to compute per-user behavior; it's to detect whether cleanup changes production behavior by listing rules that diverged fromdefaultValue. - Reverse prerequisite-detection is out of scope. The API doesn't expose a reverse-lookup for "what depends on this flag." Surface the limitation in the report; don't silently miss the case.
- Holdout linkages aren't explicitly cleaned up on delete. The feature delete handler doesn't update holdout records. If a flag with a holdout association is deleted, the holdout's
linkedExperimentsmay have stale references. Warn the user in the report whenfeature.holdoutwas present. - Deletion unlinks experiments but doesn't delete them. Experiments that referenced this flag continue to exist with stale
trackingKeyvalues. Mention this in the report for the delete path. - Don't push updated Code References after delete. The CLI/Action does this naturally on the next codebase scan. Pre-clearing refs could mask a still-broken state where the code hasn't actually been updated.
Endpoints used
GET /api/v2/features/<id>— fetch flag state (rules, defaultValue, archived, environmentSettings, holdout, project)GET /api/v2/stale-features?ids=<id>— confirm staleness + checkneverStalereasonGET /api/v1/experiments?trackingKey=<flag-id>&status=running(and&status=draft) — bulk lookup for experiments wired to this flagGET /api/v1/experiments/<id>— defensive cross-check fallback for the manual-wiring edge caseGET /api/v2/features/<id>/revisions/latest— detect unresolved drafts (404 means none)GET /api/v1/code-refs/<id>— line-level call sites if Code References is configured; returns{codeRefs: []}(not 404) when emptyPOST /api/v2/features/<id>— archive (body:{archived: true}) or unarchive ({archived: false}); triggers revision-and-publishDELETE /api/v2/features/<id>— permanent delete; requiresrestApiBypassesReviewsfor live flags (rejected without it)POST /api/v2/features/<id>/revisions/<version>/request-review— used only in the 5a approval branch
Handoffs
flag-search— for finding and auditing stale flags before cleanup (Path C in flag-search is the natural caller).flag-graph— to check what depends on a flag before archiving or deleting it.flag-targeting— if the user really wants to change the flag's behavior rather than remove it.flag-revisions— to check for open drafts that must be resolved before archiving.flag-publish— handles the approval-required (5a) and merge-conflict (5b) branches on archive.experiment-stop— must precede cleanup of a flag wired to a running experiment.- Manual UI step (no skill): unarchiving an archived-but-not-yet-deleted flag — done at
<host>/features/<flag-id>. The skill's rollback step handles the in-session case; outside the session, the user falls back to the UI.