The Pagoda of Nine Layers: Configuration Governance and Policy Enforcement

Claude Code's configuration system has nine layers—from enterprise remote policies to user-local preferences—each stacked through deep merging, with higher layers never overridable by lower ones. This chapter unpacks the "Matryoshka" architecture: its merge semantics, security boundaries, and enterprise compliance design.

> **🌍 Industry Context**: Multi-layer configuration systems are a well-established practice in developer tools. Git has system/global/local three-level config, VS Code has Default/User/Workspace three-level settings, and ESLint has an upward-traversing `.eslintrc` hierarchy chain. Claude Code's nine-layer design has more layers, but the core idea ("the more specific layer has higher priority, yet enterprise/security layers are irrevocable") is directly in line with these tools. Claude Code's unique twists are: (1) it introduces an enterprise remote policy layer with MDM delivery support—a compliance necessity for AI agents, and (2) the `CLAUDE.md` file plays a dual role as both "project configuration" and "AI behavior instruction"—something with no direct peer. **Comparison with Cursor**: Cursor's `.cursorrules` file is functionally similar to `CLAUDE.md`, but only has one layer (project-level) with no multi-layer merge across personal, enterprise, or global levels.

> **Source locations**: `src/utils/settings/` (configuration system core), `src/utils/settings/constants.ts` (`SETTING_SOURCES` priority array), `src/utils/settings/settings.ts` (merge logic)

---

## Prologue: Matryoshka

A Russian Matryoshka doll—one large doll nesting a smaller doll, which in turn nests an even smaller one. The outermost doll sets the maximum size of the whole set, and an inner doll can never exceed the outer.

Claude Code's configuration system is a nine-layer Matryoshka doll. The outermost layer is enterprise policy ("if the company says no, it's no"), and the innermost layer is user preference ("I like dark mode"). Each layer can only adjust within the bounds permitted by the layers outside it.

> **🔑 OS analogy:** This is like the **multi-level management** of a phone's settings: carrier pre-installed defaults (factory config) → company MDM-managed settings (enterprise config) → your own wallpapers and ringtones (personal preference). Higher-level settings take precedence over lower-level ones.
>
> 📚 **Course Connection**: This configuration merge mechanism is fundamentally the same as CSS **cascading rules**: browser defaults < user stylesheet < author stylesheet < `!important`. Claude Code's enterprise policy is the `!important`—no lower layer can override it. It also resembles Windows **Group Policy**: domain policy > local policy > user preference.
>
> 💡 **Plain English**: The configuration system is like **layers of clothing**: underwear = default settings (the base layer, rarely changed) → shirt = user settings (your personal style) → jacket = project settings (team uniform) → bulletproof vest = enterprise policy (company-mandated, impossible to remove). When dressing, outer layers cover inner ones—the jacket hides the shirt, and the bulletproof vest wins over everything.

---

## 1. Nine Sources, Five Priority Levels

### 1.1 Five Priority Levels (Highest to Lowest)

```
┌── Policy Settings ──────────────────────────────┐
│  Enterprise policy (highest priority, irrevocable)│
│  Source: MDM config / admin server delivery       │
│  ┌── Flag Settings ─────────────────────────────┐ │
│  │  Feature flag overrides                       │ │
│  │  Source: GrowthBook / build-time flags        │ │
│  │  ┌── Enterprise Settings ──────────────────┐  │ │
│  │  │  Enterprise config file                   │  │ │
│  │  │  Source: /etc/claude/ or admin path       │  │ │
│  │  │  ┌── Managed Settings ────────────────┐  │  │ │
│  │  │  │  Managed settings (drop-in directory)│  │  │ │
│  │  │  │  Source: /etc/claude/settings.d/*.json│  │  │ │
│  │  │  └─────────────────────────────────────┘  │  │ │
│  │  └────────────────────────────────────────────┘  │ │
│  └──────────────────────────────────────────────────┘ │
├── Project Settings ─────────────────────────────────┘
│  Project-level settings
│  Source: .claude/settings.json
│
├── Local Project Settings
│  Local project settings (not committed to git)
│  Source: .claude/settings.local.json
│
├── User Settings
│  User global settings
│  Source: ~/.claude/settings.json
│
└── Default Settings (lowest priority)
   System defaults
```

### 1.2 Nine Concrete Sources

Within the five levels, enterprise policy itself has four sub-layers, so the actual sources total **nine**:

```
1. Policy Settings (flag)      ← highest
2. Policy Settings (managed)
3. Policy Settings (enterprise)
4. Policy Settings (project)
5. Project Settings             ← middle
6. Local Project Settings
7. User Settings
8. Default Settings             ← lowest
9. Flag Settings (runtime)      ← special: runtime override
```

---

## 2. Merge Semantics: Two Radically Different Rules

Configuration merging is not as simple as "higher priority overwrites lower priority"—array fields have special merge semantics.

### 2.1 Cross-Source Merging (between different layers)

```
Source A: { allowedTools: ["Read", "Edit"] }
Source B: { allowedTools: ["Bash", "Glob"] }

Merged result: { allowedTools: ["Read", "Edit", "Bash", "Glob"] }  // concat + dedupe
```

**Array concatenation + deduplication**: arrays from different sources are merged together. This means enterprise policy can **add** restrictions but cannot **remove** project-level configuration.

### 2.2 Same-Source Merging (multiple files within the same layer)

```
File A: { allowedTools: ["Read", "Edit"] }
File B: { allowedTools: ["Bash", "Glob"] }

Merged result: { allowedTools: ["Bash", "Glob"] }  // latter replaces former
```

**Array replacement**: for multiple files within the same source, the later-loaded file replaces the earlier.

**Why two different rules?**
- Cross-source concatenation: allows every layer to *contribute* configuration—enterprise policy adds security constraints, users add personal preferences
- Same-source replacement: among multiple files at the same layer, the later one wins—preventing configuration fragmentation

### 2.3 `settingsMergeCustomizer`

This lodash `mergeWith` custom function (`settings.ts:538-547`) implements the semantics above. It is only 10 lines of code, yet it governs the configuration behavior of the entire system.

---

## 3. Drop-in Directories: Inspired by systemd

### 3.1 What is a Drop-in

```
/etc/claude/settings.d/
  ├── 00-base.json         ← base config
  ├── 10-security.json     ← security policy (overrides security parts of base)
  └── 20-team-override.json ← team-specific overrides
```

Files are loaded in alphabetical order; later-loaded files overwrite earlier ones (same-source replacement rule).

### 3.2 Why Use This Pattern

This directly borrows from Linux systemd's drop-in directory design:
- **No need to modify the main config file**—adding an override only requires dropping in a new file
- **Traceability**—each file maps to a specific configuration intent, making auditing easy
- **Composability**—different teams can each supply their own configuration snippets

---

## 4. Zod Schema Validation

### 4.1 Validation Strategy

All configuration files are validated against a Zod v4 schema upon loading (`validation.ts``.

**Key design**: `filterInvalidPermissionRules()`—one bad permission rule does not poison the entire file. The system filters out only the invalid rules and keeps the valid ones.

**Analogy**: A water treatment plant filters out impurities—it doesn't shut down the entire water supply upon finding a single contaminant; clean water keeps flowing.

### 4.2 Backward-Compatibility Contract

The comments in `types.ts:210-240` define the backward-compatibility contract:
- A newer schema must accept configuration files from older versions
- New fields may be added (with default values)
- Existing fields must not be removed or have their meaning changed
- Previously valid configurations must not be made invalid

---

## 5. `strictPluginOnlyCustomization`: Forward-Compatibility Degradation

This is one of the most elegant designs in the configuration system.

### 5.1 The Problem

Enterprise policy sets `strictPluginOnlyCustomization: true` (only enterprise-specified plugins allowed). But older versions of Claude Code do not recognize this field—they silently ignore it.

### 5.2 The Solution

The `preprocess()` function degrades unrecognized fields on older versions. The degradation direction is **only looser, never stricter**:

```
New version (recognizes field): strictPluginOnlyCustomization: true → strict restriction
Old version (does not recognize): ignored → default behavior (no restriction)
```

**Degradation direction is "less lock"**—the old version behaves more permissively than the new one. This is not optimal for security (more permissive = less secure), but it is the right call for usability (the old version continues to work normally instead of crashing on an unrecognized config).

---

## 6. Anti-Recursion Guard

### 6.1 The Problem

`loadSettingsFromDisk()` can be called recursively—loading configuration might trigger other code, which in turn needs to read configuration.

### 6.2 The Solution

`settings.ts:639-649`: a global boolean flag `isLoading`. If called again during a load, it returns the cached old value immediately.

This is a simple but effective **anti-recursion guard**—preventing infinite loops.

---

## 7. Runtime Updates

### 7.1 `updateSettingsForSource()`

Configuration is not only loaded at startup—it can also be modified at runtime (via the `/config` command or IDE integrations).

`updateSettingsForSource()` (`settings.ts:416-524`) handles runtime updates with a key semantic: **`undefined` means delete**. Setting a field to `undefined` removes it from the configuration, reverting it to the value from the previous layer.

### 7.2 Settings Change Watching

The system uses `chokidar` (a filesystem watching library) to monitor configuration files for changes. When a change is detected, the configuration is automatically reloaded.

---

## 8. Design Trade-offs

### Strengths

1. **Nine-layer config + two merge rules**—looks complex, but precisely satisfies the "enterprise lockdown + user flexibility" requirement
2. **Drop-in directories** borrow a systemd best practice—composable, traceable, and main-file-preserving
3. **`filterInvalidPermissionRules`** fault-tolerant design—one bad rule does not poison the whole file
4. **Backward-compatibility contract** explicitly written in code comments—not a verbal agreement, but a verifiable promise
5. **`strictPluginOnlyCustomization` degradation direction** chooses "less lock"—prioritizing availability
6. **Anti-recursion guard** is simple and effective—solving a real recursion risk with minimal code

### Costs

1. **Nine-source priority stack** is a heavy cognitive burden for users—"why didn't my config change take effect" is likely because a higher-priority source overrode it
2. **Two merge rules (cross-source concat vs. same-source replace)** behave non-intuitively—you must remember "when to concat and when to replace"
3. **Policy Settings' first-source-wins** means conflicts are resolved silently—with no warning that "your config was overridden by enterprise policy"
4. **Forward-compatibility degrading to "less lock"** is a security compromise—older versions of Claude Code are less secure than newer ones
5. **`undefined` means delete** is an API design prone to confusion—it collides with JavaScript's native `undefined` semantics

---

> **[Chart placeholder 2.11-A]**: Nine-layer Matryoshka diagram — nested relationship from Policy Settings down to Default Settings
> **[Chart placeholder 2.11-B]**: Two merge rules comparison diagram — cross-source concatenation vs. same-source replacement data flow
> **[Chart placeholder 2.11-C]**: Configuration effect diagnosis flowchart — troubleshooting steps for "why didn't my config take effect"
