Piebald (Feature Flags)

Description

Claude Code uses a hybrid two-layer feature flag system: build-time flags via the feature() function for dead code elimination (the "piebald" system), and runtime gates via GrowthBook for A/B testing, gradual rollouts, and dynamic configuration. Build-time flags remove entire subsystems from the compiled bundle. Runtime gates control access within a running build. Together they enable 80+ build-time feature gates and hundreds of runtime experiments.

Build-time feature flags

The feature() function

File: src/shims/bun-bundle.ts

const FEATURE_FLAGS: Record<string, boolean> = {
  PROACTIVE: envBool('CLAUDE_CODE_PROACTIVE', false),
  KAIROS: envBool('CLAUDE_CODE_KAIROS', false),
  BRIDGE_MODE: envBool('CLAUDE_CODE_BRIDGE_MODE', false),
  DAEMON: envBool('CLAUDE_CODE_DAEMON', false),
  VOICE_MODE: envBool('CLAUDE_CODE_VOICE_MODE', false),
  // ... 80+ more flags
}

export function feature(name: string): boolean {
  return FEATURE_FLAGS[name] ?? false
}

In development (Bun), flags read from environment variables at runtime. In production builds (esbuild), the function is inlined as a boolean constant, enabling tree-shaking to remove entire unreachable code branches.

Dead code elimination pattern

The codebase requires a positive ternary pattern for tree-shaking to work:

// CORRECT — esbuild eliminates the entire branch
return feature('BRIDGE_MODE')
  ? isClaudeAISubscriber() && getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_bridge', false)
  : false

// WRONG — string literals leak into bundles
if (!feature('BRIDGE_MODE')) return
// ... code with inline strings ...

This pattern is documented explicitly in comments at src/bridge/bridgeEnabled.ts:25-30.

Build process

scripts/build-bundle.ts configures esbuild with: - alias: { 'bun:bundle': 'src/shims/bun-bundle.ts' } — resolves the feature function - treeShaking: true — eliminates dead branches - define: { 'process.env.USER_TYPE': '"external"' } — removes internal-only code - minify for production — further eliminates unreachable paths

Major build-time flags (80+ total)

Core subsystems: - KAIROS / KAIROS_BRIEF / KAIROS_CHANNELS / KAIROS_DREAM — Kairos session management - BRIDGE_MODE / CCR_AUTO_CONNECT / CCR_MIRROR — IDE Remote Control - DAEMON — background daemon mode - VOICE_MODE — voice input/output - PROACTIVE — autonomous agent mode - COORDINATOR_MODE — multi-agent coordination - AGENT_TRIGGERS — triggered autonomous actions

AI/context features: - ULTRAPLAN / ULTRATHINK — planning and extended thinking - CONTEXT_COLLAPSE — context compression - TRANSCRIPT_CLASSIFIER — AFK mode + auto-classification (30+ uses in code) - HISTORY_SNIP — history compression

Skills/tools: - MCP_SKILLS / MCP_RICH_OUTPUT — MCP integration - FORK_SUBAGENT — subagent forking - WEB_BROWSER_TOOL — web browser tool - MONITOR_TOOL — monitoring/tracing

Infrastructure: - BG_SESSIONS — background sessions - SSH_REMOTE — SSH remote execution - SELF_HOSTED_RUNNER / BYOC_ENVIRONMENT_RUNNER — runner modes - PERFETTO_TRACING — performance profiling - ABLATION_BASELINE — always false in external builds

Runtime GrowthBook gates

Integration

File: src/services/analytics/growthbook.ts (1156 lines)

GrowthBook provides A/B testing, gradual rollouts, and dynamic configuration. It's initialized asynchronously during startup (doesn't block main init) and refreshes every 6 hours (20 minutes for internal users).

Access patterns

Cached (preferred — non-blocking):

getFeatureValue_CACHED_MAY_BE_STALE<T>(feature, defaultValue): T

Pure read from in-memory cache. Falls back to disk cache in ~/.claude.json. Priority: env overrides → config overrides → in-memory → disk → default. Used on all hot paths.

Blocking with early-exit (entitlement gates):

checkGate_CACHED_OR_BLOCKING(gate): Promise<boolean>

Fast path if cache says true → return immediately. Slow path if false/missing → fetch fresh from server (max ~5s). Used for gates where stale false would unfairly block access.

Dynamic configuration (object values):

getDynamicConfig_CACHED_MAY_BE_STALE<T>(configName, defaultValue): T

Same as feature values but returns complex objects — used for configuration like batch sizes, thresholds, prompts.

User targeting attributes

GrowthBook receives attributes for experiment targeting: - id / deviceID / sessionId — identity - platform — win32/darwin/linux - organizationUUID / accountUUID — org-level targeting - userType — 'ant' vs 'external' - subscriptionType / rateLimitTier — plan-level targeting - appVersion — version-specific rollouts - email — individual targeting

Initialization flow

  1. Create GrowthBook client with user attributes
  2. client.init() — fetch payload from https://api.anthropic.com/ via remote eval
  3. Process payload: transform API response format, cache feature values
  4. Sync to disk at ~/.claude.json under cachedGrowthBookFeatures
  5. Set up periodic refresh (6h production, 20min internal)
  6. Notify subscribers via onGrowthBookRefresh signal

On auth changes (login/logout): destroy old client, recreate with fresh auth.

Disk cache

Features cached in ~/.claude.json:

{
  "cachedGrowthBookFeatures": {
    "tengu_ccr_bridge": true,
    "tengu_bridge_repl_v2": false,
    "tengu_1p_event_batch_config": { "key": "value" }
  }
}

Wholesale replacement on refresh (not merge) — features deleted server-side are dropped from disk. Provides immediate fallback values before network fetch completes.

Developer overrides (internal only)

Environment variable:

export CLAUDE_INTERNAL_FC_OVERRIDES='{"my_feature": true, "my_config": {"key": "val"}}'

Config UI: via /config Gates tab. Stored in ~/.claude.json under growthBookOverrides. Fires onGrowthBookRefresh listeners when changed.

How both systems work together

A common pattern gates features at both layers:

export function isBridgeEnabled(): boolean {
  return feature('BRIDGE_MODE')           // Build-time: is the code even in this bundle?
    ? isClaudeAISubscriber() &&
        getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_bridge', false)  // Runtime: is this user enabled?
    : false
}

Build-time removes the code entirely from external builds. Runtime controls which users/orgs get access in builds where the code exists.

Aspect Build-time (feature()) Runtime (GrowthBook)
When evaluated At bundle time At runtime
Performance cost Zero (dead code eliminated) Minimal (cache lookup)
Use case Major subsystem gates A/B tests, gradual rollouts
Distribution Environment variables at build Remote server config
Update speed Requires new build Dynamic, refreshes every 6h
Kill switch No Yes (immediate via server)

The "piebald" name

Piebald is also the name of a community-built tool that extracts prompt strings from Claude Code releases by analyzing compiled JavaScript bundles. It has tracked 141+ versions, providing a longitudinal view of how system prompts evolved. The ease of extraction from compiled JS is partly why anti-distillation defenses exist. The community tool and the internal feature flag system share a name but serve different purposes.

What depends on it

Design trade-offs

Decision Trade-off
Two-layer system (build + runtime) Flexible but complex — developers must know which layer to use
Positive ternary pattern required Reliable tree-shaking, but unintuitive — wrong pattern silently leaks code
80+ build-time flags Fine-grained control, but flag proliferation makes the build matrix complex
6-hour GrowthBook refresh Low network overhead, but config changes take up to 6 hours to propagate
Disk cache for GrowthBook Immediate fallback values, but stale data possible if disk is corrupted
Async GrowthBook init Doesn't block startup, but first-turn features may use stale/default values

Key claims

Relations

Sources

src-20260409-a62a26aa6e15, source code at src/shims/bun-bundle.ts, src/services/analytics/growthbook.ts, scripts/build-bundle.ts