Feature flags in git: the case for flags as code
Feature flags in git means your flags live in the repository as code, reviewed in a pull request and versioned next to the features they gate, instead of configured in a separate dashboard. The flag file is the source of truth, and changing a flag is a commit.
Most flag tools keep the flag state somewhere else: a hosted dashboard, or a service your app queries. That leaves you with two sources of truth, the code in git and the flags in the dashboard, and the job of reconciling them by hand. Nobody does that job for long. The flag drifts from the code, and six months later nobody can say which flags are safe to delete. Putting the flags in git collapses the two back into one.
What follows is what that buys you, how to do it with or without a dedicated tool, and the one thing it costs you.
Key Takeaways
- Feature flags in git means the flag definition is a file in your repo, so a flag change is a commit and its history is the git history.
- Your existing workflow becomes the flag workflow: pull request review is the approval flow,
git revertis the rollback, and CI is the validation.- You can start today with a
flags.jsonand anif. A dedicated tool adds percentage rollouts, validation, and stale-flag detection.- The tradeoff: turning a flag off is a commit and a deploy, not a sub-second dashboard toggle. For a critical-path kill switch, that is the wrong model.
- Because the flags are files, a coding agent in your repo can read and edit them like any other source.
What “feature flags in git” means
A feature flag in git is a flag whose definition lives in a file in your repository, not in a database behind a dashboard. The file holds the flag’s id, its state, who owns it, and how it is targeted or split. Your application reads the compiled result, and the file is what you edit to change behavior.
This is configuration as code applied to flags: the thing that controls runtime behavior is checked in, reviewed, and versioned like source. A flag is a good fit for it, because a flag is temporary and tied to a specific code change, so it wants to travel in the same pull request as the code it gates. Martin Fowler’s feature toggles treats them as inventory to keep low, which is easier when each flag is a file you can see in a diff.
The simplest version is a config file:
{
"new-checkout": true,
"dark-mode": false
}
and a check at the call site:
if (flags["new-checkout"]) {
return renderNewCheckout();
}
That is already feature flags in git. The flag is in the repo, the change goes through review, and the history is in git log. What you do not get yet is percentage rollouts, targeting, validation, or any way to stop old flags from piling up.
Your git workflow becomes the flag workflow
The reason to keep flags in git is that you already have a mature system for changing code safely, and a flag is a code change. Everything git and CI already do, they now do for flags too.
- Pull request review is the approval flow. Turning a flag on for everyone is a diff someone approves, with the reasoning in the PR description. There is no separate approval system to wire up.
- git history is the audit log. Who changed a flag, when, and why is answered by
git blameand the commit message, not by a dashboard’s activity feed you have to trust. - Rollback is a commit. Reverting a flag is
git revertor editing one value back, so why you rolled back, and when, is in the history rather than in someone’s memory of a 2am toggle. - CI is the validation. A check can confirm that every referenced flag exists and that a rollout is well formed, so a broken flag fails the build like any other broken code.
- The flags travel with the code. A flag and the feature it gates land in the same pull request, so a reviewer sees both, and neither ships without the other.
None of this needs a new product. It needs the flags to be files.
How it works in practice
A config file and an if get you started, but they stop scaling once you want to roll a change out to 10% of users, target a segment, or keep dead flags from accumulating. This is where a purpose-built git-native tool earns its place. dif is one: every flag and every experiment is a single Markdown file next to the code it changes.
---
id: new-checkout
status: active
owner: sam@acme.com
surface: checkout
audience:
include:
- device_type: [mobile, tablet]
variants:
- id: "off"
weight: 90
- id: "on"
weight: 10
metrics:
primary: completed_checkout
---
## Brief
Ramp to 25% once the guardrails hold for a week.
The frontmatter carries the targeting and the split; the prose below is the rationale. dif validate runs in CI and checks that the weights total 100, the owner is a real email, and the code does not reference a flag that no longer exists. A broken flag fails the pull request like a broken build. dif build compiles the active files into a typed client plus a context.json summary, and the call site is one function:
if (dif("new-checkout") === "on") {
return <NewCheckout />;
}
return <LegacyCheckout />;
Assignment is deterministic, a hash of the user id, so there is no network call to evaluate a flag and no assignment database to keep in sync. The .md format and the CLI reference cover the rest.
Feature flags as code, with or without a dedicated tool
You do not need dif, or any product, to put flags in git. The spectrum runs from a hand-rolled config to a purpose-built tool:
- A config file and an
if. Free, instant, and fine for on/off flags in a small app. No rollouts, no validation, no bucketing. - A config plus your own helpers. Add percentage logic and a validation script. Now you are maintaining flag infrastructure, which is rarely the job you want.
- A git-native flag tool. A file format, deterministic bucketing, CI validation, and stale-flag detection, without a hosted dashboard.
The config file is the right call until it is not. When flags start colliding, when a typo in a flag name ships silently, or when nobody can tell which of thirty flags are live, the hand-rolled version has stopped paying for itself. That is the point to reach for a tool built for it.
The tradeoff
Keeping flags in git costs you the instant toggle. Changing a flag is a commit and a deploy, so if your build takes ten minutes, your worst-case time to turn something off is ten minutes. A hosted dashboard flips a flag in under a second with no deploy, and for an ops kill switch on a payment path, that second matters.
The model has a clear boundary. For most flags, where the failure mode is “some users see the new thing,” a merge and a deploy is fine. For a flag that has to stop a live incident immediately, a streaming dashboard is the right tool and a git-native flag is not. The open-source LaunchDarkly alternative post works through that tradeoff in detail.
When feature flags in git fit
Choose flags in git if your team already lives in pull requests and wants one source of truth instead of a dashboard to reconcile. It fits trunk-based development, where you merge unfinished work behind a release flag and ship it dark, and it fits teams adopting coding agents, because an agent can read and write a flag file but cannot see a dashboard.
It is a weaker fit if you need sub-second kill switches on critical paths, or if non-engineers have to flip flags without a deploy. Most flags are fine in git; a few critical-path kill switches are not, so do not assume one model covers all of them.
Getting started
The lightest start is a config file you already know how to write. When you outgrow it, a git-native tool gives you the rollouts and validation without a dashboard. With dif, install the CLI and scaffold a project:
npm install -g @dif.sh/cli
dif init
dif init writes a dif/ directory, a typed client, and the flag files, with no account or API key. From there dif new drafts a flag, dif validate checks it in CI, and dif build compiles it. See what feature flags are for the fundamentals, and the feature flags page for the full workflow.
Putting flags in git is a decision about where the source of truth lives. Keep it in the repo, and the tools you already trust, review, history, CI, and revert, do the work a dashboard would otherwise do in a system of its own.
FAQ
Can feature flags live in version control? Yes. A feature flag in git is a file that defines the flag, checked into your repo and read by your app. The change goes through pull request review, and the history lives in git log like any other code change.
What does “feature flags as code” mean? It means the flag definition is source, not dashboard state. The flag lives in a file, is reviewed in a pull request, and is versioned with the feature it gates, so the flag and the code do not drift apart.
How do you roll back a feature flag in git? Revert the commit that changed it, or edit the value back and merge. There is no separate rollback system, so the reason and the timing stay in the git history. The cost is that a rollback is a deploy, not an instant toggle.
Do feature flags in git work with CI/CD? Yes, and CI is part of the point. A validation step can confirm every referenced flag exists and every rollout is well formed, so a broken flag fails the pipeline before it merges. Tools like dif ship a validate command built for this.
Is a config file enough, or do I need a tool? A flags.json and an if are enough for on/off flags in a small codebase. You outgrow it when you want percentage rollouts, targeting, type safety, or automatic detection of flags nobody removed. That is when a git-native flag tool earns its place.
How is this different from a dashboard tool like LaunchDarkly? A dashboard keeps flag state in a separate service and can flip a flag in under a second. Git-native keeps flag state in the repo, reviewed and versioned, at the cost of a deploy to change it. The open-source LaunchDarkly alternative post compares the two directly.