Skip to content

Transformer Math

Module 57 · AI Engineering

Configuration & Schemas

Zod validates every key at startup — one typo in settings.json blocks the entire CLI from booting.

Status:

Configuration comes from three sources that merge at startup: user settings, project settings, and enterprise MDM policies. Every config value is validated at runtime with Zod schemas — catching typos, wrong types, and invalid values before they cause bugs downstream.

  • Three sources: user (~/.claude/settings.json) → project (.claude/settings.json) → enterprise MDM
  • Zod validation at runtime — TypeScript types are erased, Zod catches real bad data
  • Feature flags via GrowthBook — dead code elimination at build time
  • Merge priority: MDM > project > user (enterprise wins)
🎮

Config Merge Pipeline

What you are seeing

Three config sources are loaded, validated individually with Zod, then merged with priority. Enterprise MDM policies always win conflicts. The resulting config is type-safe and guaranteed to match the schema.

What to try

Notice how the user allows Bash(*) but MDM denies Bash(curl *)— enterprise policy overrides the user's permissive setting.

# Source 1: User settings (~/.claude/settings.json)

{ "permissions": { "allow": ["Bash(*)"], "mode": "default" } }

# Source 2: Project settings (.claude/settings.json)

{ "permissions": { "allow": ["Bash(npm *)"], "deny": ["Bash(rm -rf)"] } }

# Source 3: Enterprise MDM

{ "permissions": { "deny": ["Bash(curl *)"], "mode": "acceptEdits" } }

# After Zod validation + merge (MDM wins)

{

"permissions": {

"allow": ["Bash(npm *)"],

"deny": ["Bash(rm -rf)", "Bash(curl *)"],

"mode": "acceptEdits" ← MDM overrides user

}

}

🗂️

Three-File Merge Hierarchy

What you're seeing

Each source is validated by Zod independently, then merged in priority order. CLI args can also override any field at invocation time. The final merged config is the only object the rest of the CLI reads.

~/.claude/settings.jsonUser · lowest priority.claude/settings.jsonProject · mid priorityEnterprise MDM PolicyHighest priorityZod.parse()Zod.parse()Zod.parse()deepMerge(user, project, mdm)MDM wins scalars · deny lists union · allow lists intersectFinal merged Settings object (type-safe)
💡

The Intuition

Worked Example: The Typo Bug

User types "permisions" instead of "permissions" in settings.json.

Without Zod

The config loads. permissions is undefined. Agent runs with no permission rules — every tool call requires manual approval. Silent failure.

With Zod

Startup fails fast: "Unrecognized key: permisions. Did you mean permissions?" Caught before the agent runs a single tool call.

Why Zod, Not Just TypeScript?

TypeScript types are erased at compile time — they don't exist at runtime. When you load a JSON file from disk, TypeScript has no way to verify the data matches your interface. A user types "permisions" instead of "permissions"— TypeScript won't catch it. Zod validates at runtime: it parses the actual data, checks every field, and returns descriptive errors for mismatches. You define the schema once with Zod, then derive the TypeScript type via z.infer<> — one source of truth for both compile-time and runtime.

Three-Source Merge

The three config sources serve different audiences:

  • User settings — personal preferences (model choice, permission mode, UI settings)
  • Project settings — team conventions (allowed tools, coding standards, hook configurations)
  • Enterprise MDM — organization policy (security restrictions, audit requirements, mandatory hooks)

MDM has highest priority because enterprise security policies must not be overridden by individual users. The merge uses different strategies per field: scalars take the highest-priority value, deny lists union (any source can deny), allow lists use the most restrictive intersection.

💡 Tip · The allowedTools pattern syntax is surprisingly expressive: "Bash(npm test)"matches Bash with "npm test" in the command, "Read" matches all Read calls, and "Edit(src/**)" matches Edit with paths matching the glob.

Feature Flags at Build Time

Feature flags via GrowthBook serve two purposes: gradual rollout of new features, and dead code elimination. When feature('HISTORY_SNIP')evaluates to false at build time, the bundler removes the entire code block from the output. This is different from runtime flags — the code literally doesn't exist in the shipped binary. This reduces bundle size and attack surface.

✨ Insight · Zod schemas serve as living documentation. When a new contributor asks "what settings are available?", the schema is the authoritative answer — not a README that might be outdated.

Schema Evolution: Handling Breaking Changes

Config schemas change as features are added, renamed, or removed. The challenge: existing user configs on disk must keep working after an upgrade. There are three tiers of change, each requiring a different strategy:

  1. Additive (safe):New optional fields with defaults. Old configs validate fine — Zod's .optional() with .default() fills in missing values. No migration needed.
  2. Rename (warn and alias): A Zod .transform()reads the old key name, writes it to the new key, and emits a deprecation warning. Users' configs keep working; they see a prompt to update the field name.
  3. Breaking (versioned migration): Add a schemaVersion field. On load, check the version, run migration functions in sequence (v1→v2→v3 as pure functions: old config in, new config out), then validate against the current schema. This is the same pattern as database migrations — never skip versions, always migrate forward incrementally.

The golden rule: never crash on a bad config field. Use safeParse() instead of parse(), log the validation error, and fall back to defaults. A schema upgrade that breaks every existing user config on startup is a production incident.

Array Merge Semantics: Union vs Intersection

Scalar fields (like mode) take the highest-priority value when merging three config sources. But arrays need a deliberate strategy. For deny lists, use union: any source can add a deny rule, and the result denies everything any source denied. For allow lists, use intersection: a command is only auto-approved if all config sources agree. This means enterprise MDM can always restrict what users allow, and users cannot grant themselves permissions the project settings didn't intend. Getting this wrong — unioning allow lists — means a permissive user config can override a restrictive MDM policy.

Quick Check

Why validate config with Zod at runtime instead of just TypeScript types?

📐

Key Code Patterns

Settings Schema (Zod — TypeScript)

typescript
import { z } from "zod";

// Hook entry schema (command-type hooks)
const HookSchema = z.object({
  type: z.literal("command"),
  command: z.string(),
  timeout: z.number().positive().optional(), // seconds; default 600 (10 min)
});

// Define the schema — single source of truth
const SettingsSchema = z.object({
  permissions: z.object({
    allow: z.array(z.string()).optional(),   // ["Bash(npm test)", "Read"]
    deny: z.array(z.string()).optional(),    // ["Bash(rm -rf)"]
    mode: z.enum(["default", "bypassPermissions", "plan", "acceptEdits", "dontAsk"]).optional(),
  }).optional(),
  model: z.string().optional(),   // "opus", "sonnet", "haiku"
  hooks: z.object({
    PreToolUse: z.array(HookSchema).optional(),
    PostToolUse: z.array(HookSchema).optional(),
  }).optional(),
});

// Derive TypeScript type from the schema — always in sync
type Settings = z.infer<typeof SettingsSchema>;

Config Loading and Merge

typescript
function loadConfig(): Settings {
  // Three sources merge — enterprise MDM wins conflicts

  // 1. Load each source
  let user = loadJson("~/.claude/settings.json");
  let project = loadJson(".claude/settings.json");
  const mdm = loadMdmPolicies(); // enterprise managed

  // 2. Validate each with Zod (catches typos, wrong types)
  const userResult = SettingsSchema.safeParse(user);
  if (!userResult.success) {
    warn(`Invalid user config: ${userResult.error}`);
    user = {}; // fall back to defaults
  }

  const projectResult = SettingsSchema.safeParse(project);
  if (!projectResult.success) {
    warn(`Invalid project config: ${projectResult.error}`);
    project = {};
  }

  // MDM is pre-validated by enterprise tooling
  const validatedMdm = SettingsSchema.parse(mdm);

  // 3. Merge with priority: MDM > project > user
  return deepMerge(user, project, validatedMdm);
}

function deepMerge(user: Partial<Settings>, project: Partial<Settings>, mdm: Settings): Settings {
  // Field-specific merge strategies — result nests under permissions key
  return {
    permissions: {
      mode: mdm.permissions?.mode ?? project.permissions?.mode ?? user.permissions?.mode ?? "default",
      deny: union(user.permissions?.deny, project.permissions?.deny, mdm.permissions?.deny),
      allow: intersect(user.permissions?.allow, project.permissions?.allow, mdm.permissions?.allow),
    },
  };
}

Feature Flags — Build-Time Dead Code Elimination

typescript
// Feature flag evaluated at build time
if (feature("HISTORY_SNIP")) {
  // This entire block is REMOVED from the bundle
  // when HISTORY_SNIP is off
  enableSnipCompaction();
  registerSnipCommands();
}

// vs. Runtime flag (code always included)
if (config.enableSnip) {
  // Both branches exist in the bundle
  // Larger bundle, more attack surface
  enableSnipCompaction();
}

// Build-time: smaller bundle, requires rebuild to toggle
// Runtime: larger bundle, instant toggle
// CLI tools prefer build-time (users install a binary)
🔧

Break It — See What Happens

No Zod validation
Flat merge (no priority hierarchy)
📊

Real-World Numbers

MetricValue
Config sources3 (user, project, enterprise MDM)
Validation libraryZod (runtime schema validation)
Feature flag serviceGrowthBook (build-time elimination)
Merge priorityMDM > project > user
Permission modes5+ (default, bypassPermissions, plan, acceptEdits, dontAsk)
Schema-to-type derivationz.infer<> (one source of truth)
✨ Insight · Zod validation at startup adds ~5ms to load time but prevents hours of debugging from silent config errors. The most common user-reported bugs before validation: misspelled field names and wrong types in settings.json.
🧠

Key Takeaways

What to remember for interviews

  1. 1TypeScript types are erased at compile time — Zod validates actual runtime data from disk, catching typos like 'permisions' that TypeScript silently ignores.
  2. 2Config comes from three sources (user, project, MDM) merged with strict priority: MDM > project > user, so enterprise policies can never be overridden.
  3. 3Deny lists union across sources (any source can restrict), while allow lists intersect (all sources must agree) — getting this backwards lets users bypass MDM policies.
  4. 4Feature flags via GrowthBook evaluated at build time enable dead code elimination: false flags remove entire code paths from the bundle, not just runtime branches.
  5. 5Schema changes should use three strategies: additive (optional + default), rename via Zod transform, or versioned migration chain — never crash on a bad config field.
🧠

Recap quiz

🧠

Config & Schemas recap

Trade-off

A user settings.json allows Bash(*). The project .claude/settings.json denies Bash(rm -rf). Enterprise MDM forces mode: acceptEdits. What does the merged config&apos;s mode field contain?

A user settings.json allows Bash(*). The project .claude/settings.json denies Bash(rm -rf). Enterprise MDM forces mode: acceptEdits. What does the merged config&apos;s mode field contain?
Trade-off

A developer adds a new required field to the Zod settings schema without a default value. What happens to users who upgrade with an old settings.json that lacks this field?

A developer adds a new required field to the Zod settings schema without a default value. What happens to users who upgrade with an old settings.json that lacks this field?
Trade-off

Which migration strategy is correct when renaming a settings key from oldKey to newKey without breaking existing user configs?

Which migration strategy is correct when renaming a settings key from oldKey to newKey without breaking existing user configs?
Trade-off

The user&apos;s settings.json sets model: sonnet. The project .claude/settings.local.json sets model: opus. Which model does the merged config use?

The user&apos;s settings.json sets model: sonnet. The project .claude/settings.local.json sets model: opus. Which model does the merged config use?
📚

Further Reading

  • Zod: TypeScript-first schema validation The runtime validation library used for config schemas — bridges the gap between TypeScript types and runtime data.
  • GrowthBook Feature Flags The feature flag service used for build-time dead code elimination and gradual rollouts.
  • Apple MDM Protocol Reference The enterprise device management protocol used to deploy organization-wide settings policies.
  • JSON Schema Specification The standard that Zod's validation model is built on — understanding this helps design config schemas.
  • 12-Factor App: Config The canonical rule for config management — strict separation of config from code, environment-variable-first, directly applicable to agent settings design.
  • Launch Darkly Feature Flags Guide The industry standard for feature flag systems — covers build-time vs. runtime flags, targeting rules, and rollout strategies used in agent feature gating.
  • TypeScript satisfies Operator The TypeScript operator that validates config objects against a schema without widening the type — the compile-time complement to Zod's runtime validation.
🎯

Interview Questions

Difficulty:
Company:

Showing 4 of 4

Design a config system with three priority levels that validates all input.

★★★
Google

How do feature flags enable dead code elimination at build time?

★☆☆
Meta

How would you handle config migration when the schema changes?

★★☆
Anthropic

How would you design a config schema that supports both JSON and TypeScript definitions while keeping them in sync?

★★☆
Google