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.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- feature-flags

Installs to ~/.agents/skills/feature-flags/ for every project on this machine.

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

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.

~/.agents/skills/feature-flags/SKILL.md

--- name: feature-flags description: Designs, implements and retires feature flags behind the vendor-neutral OpenFeature API - release toggles, kill switches, percentage rollouts,… license: MIT ---

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

TypePurposeLifetimeDefault when the provider is down
Release toggleShip unfinished or risky code dark, roll out graduallyDays to weeksOff (old behaviour)
Kill switch (ops toggle)Turn off an expensive or fragile path under load or outagePermanent, reviewed yearlyOn (feature works) unless off is safer
ExperimentA/B test with measured outcomeLength of the experimentControl variant
Permission / entitlementPlan tiers, beta programs, internal-only toolsLong-livedLeast 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.md and 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, not disable-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 if chains: 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'
  1. Replace each evaluation with the winning behaviour; delete the losing code path and its tests (keep the tests for the winner).
  2. Delete the registry entry; run build, typecheck and tests.
  3. Deploy the code change everywhere, then archive the flag in the provider. Archiving first would make old code fall back to the default.
  4. 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, rg for the key returns nothing outside the changelog.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

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.

esc