Claude Code for Feature Flags: Safe Deployments and Gradual Rollouts — Claude Skills 360 Blog
Blog / Development / Claude Code for Feature Flags: Safe Deployments and Gradual Rollouts
Development

Claude Code for Feature Flags: Safe Deployments and Gradual Rollouts

Published: June 30, 2026
Read time: 8 min read
By: Claude Skills 360

Feature flags decouple deployment from release — you can ship code to production before it’s visible to users, gradually roll out to 1% → 10% → 100%, and instantly kill a feature if something goes wrong. Claude Code understands flag patterns well enough to implement them correctly: server-side evaluation to prevent flicker, consistent bucketing for UX coherence, and clean code that’s easy to remove when flags are retired.

This guide covers feature flags with Claude Code: implementation patterns, gradual rollouts, testing, and flag lifecycle management.

OpenFeature (Provider-Agnostic)

Set up feature flags in our Node.js API.
We want to be able to switch providers (LaunchDarkly → Flagsmith)
without rewriting application code.
// src/feature-flags/index.ts
import { OpenFeature, Client } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@openfeature/launchdarkly-provider';

// Initialize once at app startup
export async function initFeatureFlags(): Promise<void> {
  const provider = new LaunchDarklyProvider(process.env.LAUNCHDARKLY_SDK_KEY!);
  await OpenFeature.setProviderAndWait(provider);
}

// Get a client with user context
export function getFlagClient(user: { id: string; email: string; plan: string }): Client {
  return OpenFeature.getClient();
}

// Typed flag evaluation helpers
export const flags = {
  async newDashboard(client: Client, userId: string): Promise<boolean> {
    return client.getBooleanValue(
      'new-dashboard',
      false,  // Default: off
      { targetingKey: userId }
    );
  },
  
  async checkoutV2(client: Client, userId: string): Promise<boolean> {
    return client.getBooleanValue('checkout-v2', false, { targetingKey: userId });
  },
  
  async aiSuggestions(client: Client, userId: string): Promise<boolean> {
    return client.getBooleanValue('ai-suggestions', false, { targetingKey: userId });
  },
  
  async maxUploadSizeMb(client: Client, userId: string): Promise<number> {
    return client.getNumberValue('max-upload-size-mb', 10, { targetingKey: userId });
  },
} as const;
// Using flags in a route
app.get('/api/dashboard', async (req, res) => {
  const client = getFlagClient(req.user);
  
  const useNewDashboard = await flags.newDashboard(client, req.user.id);
  
  if (useNewDashboard) {
    const data = await getDashboardV2Data(req.user.id);
    return res.json({ version: 'v2', ...data });
  }
  
  const data = await getDashboardData(req.user.id);
  return res.json({ version: 'v1', ...data });
});

Client-Side Flags (React)

Add feature flags to the React frontend.
Prevent flicker — don't show the old UI while the flag loads.
// src/hooks/useFeatureFlag.ts
import { useEffect, useState } from 'react';
import { OpenFeature } from '@openfeature/web-sdk';
import { useAuth } from './useAuth';

// Server-renders with defaults to prevent flash of old content
const flagDefaults: Record<string, boolean | string | number> = {
  'new-dashboard': false,
  'dark-mode-enabled': false,
  'max-file-upload-mb': 10,
};

export function useBooleanFlag(flagKey: string): { value: boolean; loading: boolean } {
  const { user } = useAuth();
  const [value, setValue] = useState<boolean>(
    () => flagDefaults[flagKey] as boolean ?? false
  );
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    if (!user) return;
    
    const client = OpenFeature.getClient();
    
    // Evaluate immediately if provider is ready
    setValue(client.getBooleanValue(flagKey, flagDefaults[flagKey] as boolean ?? false));
    setLoading(false);
    
    // Re-evaluate if flag changes (real-time streaming)
    const handler = () => {
      setValue(client.getBooleanValue(flagKey, flagDefaults[flagKey] as boolean ?? false));
    };
    
    client.addHandler('PROVIDER_CONFIGURATION_CHANGED', handler);
    return () => client.removeHandler('PROVIDER_CONFIGURATION_CHANGED', handler);
  }, [flagKey, user?.id]);
  
  return { value, loading };
}
// Usage in components — shows default during loading (no flicker)
export function Dashboard() {
  const { value: showNewDashboard, loading } = useBooleanFlag('new-dashboard');
  
  if (loading) {
    // Render a skeleton that works for both versions — no layout shift
    return <DashboardSkeleton />;
  }
  
  return showNewDashboard ? <DashboardV2 /> : <DashboardV1 />;
}

Gradual Rollout Implementation

We want to roll out the new payment flow to 5% of users,
then 20%, then 50%, then 100%. The same user should always
get the same experience (consistent bucketing).

For simple rollouts without an external service:

// src/lib/rollout.ts
import { createHash } from 'crypto';

// Deterministic bucketing: same userId always maps to same number 0-99
function getBucket(userId: string, flagKey: string): number {
  const hash = createHash('sha256')
    .update(`${flagKey}:${userId}`)
    .digest('hex');
  
  // Take first 8 hex chars → convert to integer → mod 100
  return parseInt(hash.slice(0, 8), 16) % 100;
}

export function isInRollout(
  userId: string,
  flagKey: string,
  rolloutPercentage: number,
): boolean {
  if (rolloutPercentage >= 100) return true;
  if (rolloutPercentage <= 0) return false;
  return getBucket(userId, flagKey) < rolloutPercentage;
}

// Database-backed flag config
interface FlagConfig {
  key: string;
  enabled: boolean;
  rolloutPercentage: number;
  allowlist: string[];  // Specific user IDs that always get the flag
  denylist: string[];   // Specific user IDs that never get it
}

export async function evaluateFlag(
  flagKey: string,
  userId: string,
): Promise<boolean> {
  const config = await getFlagConfig(flagKey); // From Redis/DB with short TTL cache
  
  if (!config?.enabled) return false;
  if (config.denylist.includes(userId)) return false;
  if (config.allowlist.includes(userId)) return true;
  
  return isInRollout(userId, flagKey, config.rolloutPercentage);
}

Testing Code Behind Flags

Write tests for components and APIs that are behind feature flags.
Need to test both the old and new behavior.
// tests/dashboard.test.ts
import { vi } from 'vitest';
import * as featureFlags from '../src/feature-flags';

describe('Dashboard API', () => {
  describe('with new-dashboard flag OFF', () => {
    beforeEach(() => {
      vi.spyOn(featureFlags.flags, 'newDashboard').mockResolvedValue(false);
    });
    
    it('returns v1 dashboard data', async () => {
      const response = await supertest(app)
        .get('/api/dashboard')
        .set('Authorization', `Bearer ${userToken}`);
      
      expect(response.status).toBe(200);
      expect(response.body.version).toBe('v1');
    });
  });
  
  describe('with new-dashboard flag ON', () => {
    beforeEach(() => {
      vi.spyOn(featureFlags.flags, 'newDashboard').mockResolvedValue(true);
    });
    
    it('returns v2 dashboard data', async () => {
      const response = await supertest(app)
        .get('/api/dashboard')
        .set('Authorization', `Bearer ${userToken}`);
      
      expect(response.status).toBe(200);
      expect(response.body.version).toBe('v2');
    });
  });
});
// React component test
import { render, screen } from '@testing-library/react';
import * as flagHooks from '../hooks/useFeatureFlag';

test('renders V2 dashboard when flag is enabled', () => {
  vi.spyOn(flagHooks, 'useBooleanFlag').mockReturnValue({
    value: true,
    loading: false,
  });
  
  render(<Dashboard />);
  
  expect(screen.getByTestId('dashboard-v2')).toBeInTheDocument();
  expect(screen.queryByTestId('dashboard-v1')).not.toBeInTheDocument();
});

Flag Lifecycle and Cleanup

We have 30 old flags that were never cleaned up.
Help me audit and remove them.

Claude Code generates a cleanup process:

// scripts/audit-flags.ts
import { getAllFlags } from './src/feature-flags';

// Find flags where rollout is 100% (can remove the flag, just ship the code)
// Find flags where rollout is 0% (dead code — remove it)
const flags = await getAllFlags();
const stale = flags.filter(f =>
  f.rolloutPercentage === 100 || f.rolloutPercentage === 0 || f.archivedAt
);

console.log('Stale flags to clean up:');
stale.forEach(f => {
  const reason = f.rolloutPercentage === 100 ? 'fully rolled out'
    : f.rolloutPercentage === 0 ? 'never rolled out (dead code)'
    : 'archived';
  console.log(`  ${f.key}: ${reason} (created ${f.createdAt})`);
});

For A/B testing that goes beyond binary feature flags to measure statistical significance, the performance guide covers experiment design. For deploying behind feature flags safely with zero-downtime database migrations, see the database migrations guide. The Claude Skills 360 bundle includes feature flag skill sets for gradual rollouts and A/B testing patterns. Start with the free tier to add feature flags to your deployment pipeline.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free