Meet Unleash in person at a Conference near you➩ Book a meeting

Automating feature flag cleanup with Unleash, GitHub, and Copilot

Most feature flags do their job and then quietly stick around. The rollout finished six months ago, the variant has been chosen, the metrics are stable, and yet the if blocks, helper functions, test fixtures, and dead branches are still in the repo. Every developer who reads that code pays a small tax to figure out which path is now real, and the people who shipped the feature have already moved on to the next thing.

This post shows how to close that loop automatically. You mark a flag as Completed in the Unleash dashboard, and a few seconds later GitHub Copilot opens a pull request that removes the flag from your codebase. Behind the scenes there is a webhook, a labeled GitHub issue, a small GitHub Actions workflow, and the Unleash MCP server that gives Copilot framework-aware cleanup instructions.

Let’s build the whole thing from scratch.

Why automate flag cleanup

Stale flags carry real cost. They split simple functions into two paths that no one will ever toggle again, they make code reviews harder because reviewers have to mentally evaluate dead branches, and they create a small but non-zero risk that someone re-enables a feature that the team had already decided to drop. Worse, the longer a flag sits unused, the more the surrounding code drifts. A helper that only existed to support the discarded variant gets imported somewhere else, a test that asserts behavior under both branches becomes a flaky reminder of a decision no one remembers making.

The completion moment is the right time to act on all of this. When you mark a flag complete in Unleash, you already record the outcome the team chose: kept, kept with a specific variant value, or discarded. That is the same information a cleanup task needs. We just have to deliver it to the right place, in a format an automated agent can act on.

The end-to-end workflow at a glance

The workflow has five steps. Each one is small, and each artifact is a few lines of configuration or YAML.

  1. You mark a feature flag as completed in the Unleash UI.
  2. Unleash fires a feature-completed webhook to the GitHub REST API and creates an issue tagged with the unleash-flag-completed label.
  3. The issue contains the flag name, the chosen outcome, and step-by-step cleanup guidance.
  4. A GitHub Actions workflow listens for that label and assigns the issue to @copilot.

Copilot picks up the task, queries the Unleash MCP server for language-aware cleanup instructions, removes the flag, and opens a PR for human review.

The rest of the post walks through each step with the actual config you need.

Step 1: Mark a feature flag as completed in Unleash

Open the flag in your Unleash project, find the lifecycle section, and click Mark ready for cleanup under the flag’s lifecycle menu. Unleash asks you to record the outcome you reached. You can choose Kept if the feature is staying in the product, Kept with variant if the rollout used variants and you want to keep one specific value, or Discarded if the feature is being removed entirely.

Marking a flag as complete does not change its configuration. The flag stays where it is, traffic continues to flow, and Unleash keeps recording usage metrics. That last part matters because production metrics tell you whether any callers are still hitting the flag after you thought you were done with it. The Completed stage is a signal that the rollout decision has been made and that cleanup work should start, not a switch that turns the feature off.

 

The interesting side effect for our automation is that this action emits a feature-completed event with the chosen outcome attached as event.data.status and, when relevant, a event.data.statusValue field with the variant name. We will template both of those into the GitHub issue.

Step 2: Send a webhook on the feature-completed event

In the Unleash dashboard, head to Configure > Integrations and add a new Webhook integration. The full reference for this integration is in the Unleash webhook docs, but we only need a few fields.

Set the Webhook URL to the GitHub REST API endpoint that creates an issue:


https://api.github.com/repos/<OWNER>/<REPO>/issues

Replace <OWNER> and <REPO> with your target. This is the standard create-an-issue endpoint, so we just need to send a token and a body.

Unleash webhooks have a dedicated Authorization field, so paste your GitHub token there as “Bearer <GITHUB_TOKEN>” and you are done. It’s very important that you prepend “Bearer “ to the GitHub token for the API call to work.

The token can be a fine-grained personal access token or a GitHub App token with Issues: Read and write permission on the target repository. Keep its scope narrow. There is no reason for this token to see anything other than the one repo that should receive cleanup issues.

Subscribe the integration only to the feature-completed event. Unleash supports many other lifecycle events, and you can wire some of them into different workflows later, but for cleanup we want one event and one path.

The last piece is the Body Template. Unleash uses Mustache to render the body, with the full event available under event. Drop in this template:


{
  "labels": ["unleash-flag-completed"],
  "title": "🧹 Flag {{ event.featureName }} marked as completed",
  "body": "The feature flag `{{ event.featureName }}` has been marked as **completed** in Unleash.\n\n### 📝 Details\n- 🏷️ Flag name: `{{ event.featureName }}`\n- 📈 Feature outcome: **{{ event.data.status }}**{{#event.data.statusValue}}\n- 🎯 Variant to keep: **{{ event.data.statusValue }}**{{/event.data.statusValue}}\n\n### ✅ Suggested cleanup\n1. 🔍 Search the codebase for all references to this flag.\n2. ✂️ Remove conditional logic and retain only the code for the intended outcome.\n3. 🧼 Clean up related tests, unused helpers, imports, and configuration entries. Use the Unleash MCP and the cleanup_flag tool. Remember to fix any lint issues.\n\n_This issue was automatically generated by an Unleash integration._"
}

A couple of details are worth calling out. The {{#event.data.statusValue}} … {{/event.data.statusValue}} block is a Mustache section, which only renders when the field is present. That keeps the variant line out of the issue when the team simply chose to keep or discard the feature without picking a variant.

All those \n escapes are hard to read in JSON form, so here is what the rendered issue body looks like in plain markdown:


The feature flag `{{ event.featureName }}` has been marked as **completed** in Unleash.

### 📝 Details
- 🏷️  Flag name: `{{ event.featureName }}`
- 📈 Feature outcome: **{{ event.data.status }}**
- 🎯 Variant to keep: **{{ event.data.statusValue }}**

### ✅ Suggested cleanup
1. 🔍 Search the codebase for all references to this flag.
2. ✂️  Remove conditional logic and retain only the code for the intended outcome.
3. 🧼 Clean up related tests, unused helpers, imports, and configuration entries. Use the Unleash MCP and the cleanup_flag tool. Remember to fix any lint issues.

_This issue was automatically generated by an Unleash integration._

The cleanup instructions explicitly mention the cleanup_flag MCP tool, which is the hook that Copilot will use later.

Step 3: A labeled GitHub issue is born

When the webhook fires, GitHub returns a 201 Created response and you get an issue labeled as unleash-flag-completed with all the details to start cleaning up.

And the label is doing real work. It decouples Unleash from the workflow that triggers the agent. You can also use it to filter cleanup work in your project boards, route different label patterns to different repositories, or pause automation by simply removing the label. GitHub creates the label on first use if it does not already exist, so you do not need to seed it manually.

Step 4: Trigger a GitHub Actions workflow on the label

Save this file as .github/workflows/cleanup-workflow.yml in the repo that should receive cleanup PRs:


name: Trigger Copilot on Label
on:
  issues:
    types: [labeled]

jobs:
  assign-copilot:
    if: github.event_name == 'issues' && github.event.label.name == 'unleash-flag-completed'
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - name: Assign Copilot Agent
        run: |
          gh issue edit ${{ github.event.issue.number }} --repo ${{ github.repository }} --add-assignee "@copilot"
        env:
          GH_TOKEN: ${{ secrets.COPILOT_TRIGGER_TOKEN }}

The on: issues, types: [labeled] trigger fires for every label change on every issue, so the if: guard in the job is the part that keeps the workflow focused. Only issues that carry the unleash-flag-completed label proceed to the assignment step.

The actual work is the gh issue edit call that adds @copilot as an assignee. That single line is what kicks off the Copilot coding agent. The workflow uses a separate COPILOT_TRIGGER_TOKEN secret rather than the default GITHUB_TOKEN because the default workflow token cannot assign Copilot. You can mint a fine-grained PAT or a GitHub App token with Issues: Read and write access on the repo, store it under Settings > Secrets and variables > Actions, and you are done.

Two prerequisites to check before you ship this.

  1. The repository (or the org that owns it) needs an active Copilot plan that includes the coding agent, which today means Copilot Pro, Pro+, Business, or Enterprise. For Business and Enterprise, an admin has to enable the policy that allows Copilot to be assigned to issues.
  2. The user identity attached to your token needs permission to assign issues in the repo, which is true by default for repository collaborators.

Step 5: Copilot cleans up the flag using the Unleash MCP

This is where the workflow earns its keep. As soon as @copilot lands on the issue, GitHub provisions an ephemeral environment for the agent. Copilot reads the issue body, sees the flag name, the chosen outcome, the optional variant, and the explicit instruction to use the Unleash MCP, and starts working on a branch.

To make the Unleash MCP server available to Copilot in the cloud, you configure it in the repository settings on GitHub. Go to Settings > Copilot > Cloud agent, scroll to the MCP configuration section, and paste in the JSON below. The full reference is in the GitHub docs on extending Copilot coding agent with MCP.


{
  "mcpServers": {
    "unleash-mcp": {
      "type": "local",
      "command": "npx",
      "args": ["-y", "@unleash/mcp@latest", "--log-level", "error"],
      "env": {
        "UNLEASH_BASE_URL": "$COPILOT_MCP_UNLEASH_BASE_URL",
        "UNLEASH_PAT": "$COPILOT_MCP_UNLEASH_PAT",
        "UNLEASH_DEFAULT_PROJECT": "default"
      },
      "tools": ["*"]
    }
  }
}

The Copilot coding agent only exposes secrets and variables whose names start with COPILOT_MCP_. So add COPILOT_MCP_UNLEASH_BASE_URL and COPILOT_MCP_UNLEASH_PAT under Settings > Environments > copilot (create a new environment named “copilot” if it doesn’t exist already) and reference them from the MCP config as shown above. Make sure the base URL ends with /api, since that is what the Unleash MCP package expects. The PAT only needs permission to read and update flags in the relevant project. The same JSON shape works in .vscode/mcp.json for local development, and full IDE setup details live in the Unleash MCP integration docs.

Once the MCP server is reachable, Copilot has nine tools to work with. Three of them are the ones it leans on for a cleanup task: detect_flag to find every reference to the flag in the repo, get_flag_state to confirm the chosen variant when the issue specifies one, and cleanup_flag to receive a list of file locations and the framework-specific removal instructions. The MCP knows what unleash-client for Node.js looks like, what the Python SDK looks like, what guard patterns Java and Go developers tend to write, and it adapts the cleanup advice to the file it is editing.

What the before and after will look like

For a Node.js Express service, the before and after look like this. Before:


const { isEnabled } = require('unleash-client');

app.post('/checkout', async (req, res) => {
  const context = { userId: req.user.id };

  if (isEnabled('stripe-payment-integration', context)) {
    const result = await stripeService.processPayment(req.body);
    return res.json(result);
  } else {
    const result = await legacyPaymentService.process(req.body);
    return res.json(result);
  }
});

After Copilot applies the cleanup with outcome=kept:


app.post('/checkout', async (req, res) => {
  const result = await stripeService.processPayment(req.body);
  return res.json(result);
});

The agent removes the unleash-client import if it is no longer used elsewhere in the file, drops the context object that only existed to feed into isEnabled, deletes the legacyPaymentService import if it is now orphaned, and updates any tests that asserted on both branches. The same shape applies to Python, Go, Java, and other supported languages: the MCP gives Copilot the framework-aware patterns and the agent applies them.

The agent commits the changes on a branch, pushes, and opens a pull request that links back to the original issue. From the team’s point of view, the cleanup arrives as a normal PR with a small, focused diff.

The workflow in practice

Here is what the running pipeline looks like end to end on a real flag, from the moment Unleash dispatches the webhook to the cleanup PR landing in GitHub.

 

The Unleash dashboard shows the feature-completed event was dispatched and the GitHub API returned a successful response. This view is also the first place to look if anything misfires later, since failed deliveries surface their status code and response body right here.

 

 

The cleanup issue lands in the target repo with the unleash-flag-completed label, the workflow assigns @copilot, and the GitHub UI shows an active Copilot session working on the task. From this point on, the agent is operating in its own ephemeral environment.

 

 

Opening the running session reveals the agent’s live trace: it boots the ephemeral environment, scans the repository for references to the flag, drafts a cleanup plan, and calls into the Unleash MCP server to fetch the framework-aware removal instructions. This is also the place to step in mid-run if you want to nudge the agent in a different direction.

 

 

A few minutes later Copilot pushes a branch and opens a pull request with the flag-related code removed and the surrounding tests, helpers, and imports tidied up. The PR links back to the originating issue so the audit trail stays clean and reviewers can jump back to the Unleash event that started everything.

Production hardening notes

Before turning this on across an organization, a few details are worth getting right.

Keep the webhook subscribed only to the feature-completed event. Unleash supports a long list of lifecycle events, and a noisier subscription will start opening issues for things you do not want automated yet. If you later want to handle archival or stale flags, build a second webhook with its own label and its own workflow.

Use a fine-grained PAT or, better, a small GitHub App for the webhook. The token only needs Issues: Read and write on a single repository. A token that is wider than that is a token you have to worry about.

Wait for production usage metrics on the flag to drop to zero after merging the PR, before archiving the flag. The Unleash UI keeps showing usage data after a flag is marked Completed for exactly this reason. The MCP gives you a clean diff, but a human should still gate the merge and archival, especially when the outcome was kept with variant and the change rewrites a critical path.

For monorepos, point Copilot at the right working directory in the issue body or in .github/copilot-instructions.md. The agent will otherwise assume the repo root and may take longer to find the references.

Wrap up

You now have a workflow where flag completion in Unleash directly produces a Copilot-authored pull request, with humans involved only at the moments that matter: deciding the outcome, and approving the merge. Everything in between, the issue creation, the routing, the cleanup logic, the framework-specific edits, runs without anyone copy-pasting flag names into a JIRA ticket.

If you want to extend this further, the Unleash GitHub Copilot integration exposes a full set of MCP tools beyond cleanup_flag. The same pattern can drive flag creation when a new feature lands, rollout adjustments based on metrics, or environment-specific toggles tied to deployment events. Cleanup is just the easiest place to start, because the moment of completion already tells you exactly what the team decided.

Share this article

LinkedInTwitter