Feature Flags skill for Codex
Ship dark, roll out slowly, delete the flag after. Codex adds flags through the vendor-neutral OpenFeature API with a typed registry, a stable targeting key and a default that is safe when the provider is down. It evaluates each flag once at a clear seam, tests both branches, writes a rollout plan with rollback triggers, and removes the flag together with the losing code path once the rollout finishes.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- feature-flags Installs to ~/.agents/skills/feature-flags/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install feature-flags The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project feature-flags Installs to .agents/skills/feature-flags/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/feature-flags.zipunzip -o feature-flags.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) feature-flags PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $feature-flags, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs, implements and retires feature flags behind the vendor-neutral OpenFeature API - release toggles, kill switches, percentage rollouts, experiments and permission flags - with typed defaults, stable targeting keys, evaluation at the edge of the code, tests for both branches, an owner and expiry on every flag, and a cleanup procedure that deletes the flag and the losing branch together. Use when adding a flag or kill switch, rolling a feature out gradually, wiring LaunchDarkly, Unleash, flagd, Flagsmith or GrowthBook through OpenFeature, or cleaning up stale flags.
Use it when
- You need to merge unfinished or risky work without exposing it
- A feature should roll out gradually or to selected accounts first
- An expensive dependency needs an operational kill switch
- Old flags are piling up and nobody knows which are safe to remove
Use something else when
- You need statistical analysis of an experiment's results: use ab-test-analysis
- You are choosing blue-green or canary deployment infrastructure: use deployment-strategies
How to check the result
Run the tests with the flag forced on and forced off, then start the app with no provider and confirm it serves the documented default without errors.
The mistake it prevents
Archiving a flag in the vendor dashboard before the code that reads it is gone, so every request silently falls back to the default.
A flag is done well when its default is the safe behaviour, it is evaluated once at a clear seam, both branches are tested, it has an owner and a removal date, and it is actually removed when the rollout ends. A flag that outlives its rollout is technical debt with a runtime branch attached.
Pick the flag type first
| Type | Purpose | Lifetime | Default when the provider is down |
|---|---|---|---|
| Release toggle | Ship unfinished or risky code dark, roll out gradually | Days to weeks | Off (old behaviour) |
| Kill switch (ops toggle) | Turn off an expensive or fragile path under load or outage | Permanent, reviewed yearly | On (feature works) unless off is safer |
| Experiment | A/B test with measured outcome | Length of the experiment | Control variant |
| Permission / entitlement | Plan tiers, beta programs, internal-only tools | Long-lived | Least privilege |
Long-lived permission flags belong in an entitlement model or config, not in a rollout tool forever. If a “release toggle” is still needed after its rollout, reclassify it explicitly instead of letting it drift.
Before adding a flag
- Read
AGENTS.mdand search for an existing flag client (rg -n "OpenFeature|getBooleanValue|isEnabled\(|useFlag"). Reuse it; never add a second flag system. - Decide the targeting key: the stable identifier rollouts hash on (user ID, account ID, or device ID for anonymous traffic). Account-level features hash on account ID so a team sees one behaviour.
- Write down: key, type, owner, default, rollout plan, success metric, removal date.
Naming and registration
- Key format:
<area>-<feature>in kebab case, positive wording:checkout-new-tax-engine, notdisable-old-tax(double negatives cause inverted rollouts). - Kill switches say what they protect:
search-ranking-ml-enabled. - Register every flag in one place in code (a constants module or typed registry) so cleanup can find all uses with one search.
// src/flags.ts - the only place flag keys and defaults are defined
export const Flags = {
newTaxEngine: { key: "checkout-new-tax-engine", default: false, owner: "payments", removeBy: "2026-12-15" },
searchMlRanking: { key: "search-ranking-ml-enabled", default: true, owner: "search", removeBy: "kill-switch" },
} as const;
Implementation with OpenFeature
OpenFeature is a CNCF standard API; providers adapt it to a backend (flagd, LaunchDarkly, Unleash, Flagsmith, GrowthBook, ConfigCat, environment variables). Code depends on the API, so swapping vendors does not touch call sites. Installing the SDK and a provider package needs network.
Node (server SDK):
import { OpenFeature, InMemoryProvider } from "@openfeature/server-sdk";
import { Flags } from "./flags";
// Startup: real provider in prod, in-memory in tests and local dev.
await OpenFeature.setProviderAndWait(provider);
const flags = OpenFeature.getClient("checkout");
export async function computeTax(order: Order, user: User) {
const useNew = await flags.getBooleanValue(
Flags.newTaxEngine.key,
Flags.newTaxEngine.default, // returned on any error
{ targetingKey: user.accountId, plan: user.plan, country: order.country },
);
return useNew ? newTaxEngine(order) : legacyTax(order);
}
Tests pin flag values explicitly:
await OpenFeature.setProviderAndWait(new InMemoryProvider({
"checkout-new-tax-engine": { disabled: false, variants: { on: true, off: false }, defaultVariant: "on" },
}));
Python (openfeature-sdk):
from openfeature import api
from openfeature.evaluation_context import EvaluationContext
client = api.get_client("checkout")
use_new = client.get_boolean_value(
"checkout-new-tax-engine", False,
EvaluationContext(targeting_key=user.account_id, attributes={"plan": user.plan}),
)
Use get*Details (getBooleanDetails in Node, get_boolean_details in Python) when you
need the reason or error code, for example to log that a default was served because the
provider was not ready.
Where to evaluate
- Evaluate once, at the outermost seam (route handler, job entry, component root), and pass the decision down as a plain value or by choosing an implementation. Flag checks scattered through ten functions are impossible to remove cleanly.
- Prefer branching by strategy object over
ifchains:const tax = useNew ? newTaxEngine : legacyTax; - In request scope, evaluate per request, not at module import time, or rollout changes will not apply until restart.
- Client-side flags are visible to users. Never use them to hide secrets or enforce authorization; enforce on the server.
- For long-running jobs, evaluate once at start so one job does not mix both behaviours.
Rollout procedure
- [ ] Merge with flag off everywhere; both branches covered by tests
- [ ] Enable for internal accounts; watch errors, latency and the success metric
- [ ] 1% -> 10% -> 50% -> 100%, holding at each step long enough to see a full traffic cycle
- [ ] Define rollback triggers up front (error rate, p95 latency, support tickets)
- [ ] At 100% for one full release cycle with no rollback: schedule removal
Changing a production flag is a production change: confirm with the user before toggling any non-local environment, and say which environment and percentage.
Removal
Remove the flag and the losing branch in one PR:
rg -n "checkout-new-tax-engine|newTaxEngine" --hidden -g '!node_modules'
- Replace each evaluation with the winning behaviour; delete the losing code path and its tests (keep the tests for the winner).
- Delete the registry entry; run build, typecheck and tests.
- Deploy the code change everywhere, then archive the flag in the provider. Archiving first would make old code fall back to the default.
- Delete now-unused code with the dead-code workflow (dead-code-removal).
Gotchas
- Changing a flag’s default changes behaviour whenever the provider is unreachable. Defaults are part of the rollout plan, not an afterthought.
- Rollouts hashed on a session or request ID flip users between variants. Use a stable targeting key.
- Flags read in database migrations or schema changes create states that cannot coexist. Use expand/contract migrations instead of flagging schema.
- Nested flags (
if a && b) multiply states; test the combinations that can occur, or forbid nesting. - Caching evaluation results across users leaks one user’s variant to another.
Output
When adding a flag, report:
**Flag:** `checkout-new-tax-engine` (release toggle, owner: payments, remove by 2026-12-15)
**Default:** false (legacy tax) - served on provider error
**Targeting key:** accountId; attributes: plan, country
**Evaluated at:** src/checkout/tax.ts:12 (once per request)
**Tests:** tax.test.ts covers on and off
**Rollout plan:** internal -> 1% -> 10% -> 50% -> 100%; rollback if 5xx > 0.5% or p95 > 400 ms
Verification
- Tests run with the flag forced on and forced off, both green.
- With the provider unreachable (or an empty in-memory provider), the app serves the documented default without errors.
- After removal,
rgfor the key returns nothing outside the changelog.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Feature Flags"
short_description: "Ship dark, roll out slowly, delete the flag after"
default_prompt: "$feature-flags Put the new tax engine behind a feature flag and plan a 1% to 100% rollout" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.