# 九层宝塔：配置治理与策略执行

Claude Code 的配置系统有九个层级——从企业远程策略到用户本地偏好，每一层通过深合并叠加，上层永远不可被下层覆盖。本章解析这个"套娃"架构的合并语义、安全边界和企业合规设计。

> **🌍 行业背景**：多层配置系统在开发工具中是成熟实践——Git 有 system/global/local 三级 config，VS Code 有 Default/User/Workspace 三级 settings，ESLint 有向上遍历的 `.eslintrc` 层级链。Claude Code 的九层设计在层数上更多，但核心思路（"越具体的层级优先级越高，但企业/安全层级永远不可覆盖"）和这些工具一脉相承。Claude Code 的独到之处在于：(1) 引入了企业远程策略层，支持 MDM 下发（这在 AI Agent 中是必须的合规需求），(2) CLAUDE.md 文件同时承担"项目配置"和"AI 行为指令"的双重角色——这一点没有直接对标物。**对比 Cursor**：Cursor 的 `.cursorrules` 文件功能类似 CLAUDE.md，但只有一层（项目级），没有个人级/企业级/全局级的多层合并。

> **源码位置**：`src/utils/settings/`（配置系统核心）、`src/utils/settings/constants.ts`（`SETTING_SOURCES` 优先级数组）、`src/utils/settings/settings.ts`（合并逻辑）

---

## 引子：套娃

俄罗斯套娃（Matryoshka）——一个大娃娃里面套一个小娃娃，小娃娃里面再套一个更小的。最外层的娃娃决定了整个套娃的最大尺寸，内层永远不能超出外层。

Claude Code 的配置系统就是一个九层套娃。最外层是企业策略（"公司说不行就是不行"），最内层是用户偏好（"我喜欢深色主题"）。每一层只能在外层允许的范围内做调整。

> **🔑 OS 类比：** 就像手机设置的**多级管理**：运营商预装的默认设置（出厂配置）→ 公司 MDM 统一管理的设置（企业配置）→ 你自己改的壁纸和铃声（个人偏好）。高层级的设置优先于低层级。
>
> 📚 **课程关联**：这套配置合并机制和 CSS 的**层叠规则**（Cascading）本质相同——浏览器默认样式 < 用户样式表 < 作者样式表 < `!important`。Claude Code 的企业策略就是 `!important`——无论下层怎么设置都无法覆盖。也类似 Windows 的**组策略**（Group Policy）：域策略 > 本地策略 > 用户偏好。
>
> 💡 **通俗理解**：配置系统就像**穿衣服的层次**——贴身内衣 = 默认配置（最基础，一般不换）→ 衬衫 = 用户配置（你的个人风格）→ 外套 = 项目配置（团队统一要求）→ 防弹衣 = 企业策略（公司强制，谁也脱不掉）。穿的时候外层覆盖内层——外套遮住衬衫，防弹衣压过一切。

---

## 1. 5 层来源 + policySettings 内部 4 子层

> **源码核实**：`constants.ts:7-22` 的 `SETTING_SOURCES` 实际只定义了 **5 个值**：`userSettings` / `projectSettings` / `localSettings` / `flagSettings` / `policySettings`。其中 `policySettings` 内部按 **first-source-wins** 策略再分 4 个子来源（见 `settings.ts:322-345`）。本书早期版本写"九层宝塔"是把 5 层外层 + 4 个 policy 子层相加，但严格说**主层只有 5 层**，子层只影响 policy 内部的拼装顺序——详细层级差异见 Part 3 第 9 章「设置系统完全解析」。

### 1.1 5 层主来源优先级（从低到高）

```
优先级 1（最低）· userSettings        ~/.claude/settings.json
优先级 2         · projectSettings    .claude/settings.json
优先级 3         · localSettings      .claude/settings.local.json（gitignored）
优先级 4         · flagSettings       --settings <path> + SDK 内联
优先级 5（最高）· policySettings     企业策略（4 个子来源）
```

### 1.2 policySettings 内部 4 个子来源（first-source-wins）

在 `policySettings` 这一层内部，`loadManagedPolicySettings()` 按下面的顺序查找**第一个非空**的配置源就停止：

| 子优先级 | 子来源 | 机制 |
|---------|--------|------|
| 高 | `remote` | 远程管理设置 API（`getRemoteManagedSettingsSyncFromCache()`）|
| 中高 | `MDM`（HKLM/plist）| 系统级 MDM（macOS Property List / Windows HKLM 注册表）|
| 中 | `file` | `managed-settings.json` + drop-in 目录 `managed-settings.d/*.json` |
| 低 | `HKCU` | Windows 用户注册表 |

> ⚠️ **常见混淆**：policy 内部是"first-source-wins"（高优先级不为空就直接用，忽略其他子来源），**不是**跨层级的"拼接+去重"。跨层级合并见下一节。

---

## 2. 合并语义：两套截然不同的规则

配置合并不是简单的"高优先级覆盖低优先级"——数组类型的字段有特殊的合并语义。

### 2.1 跨来源合并（不同层之间）

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

合并结果: { allowedTools: ["Read", "Edit", "Bash", "Glob"] }  // 拼接 + 去重
```

**数组拼接 + 去重**：不同来源的数组会被**并集**合并。这一点对读者常见的误区需要明确区分：

- 对于**白名单型字段**（如 `allowedTools`、`allowedCommands`）：跨层拼接 = **各层共同扩充允许范围**。企业策略的加项**只会放宽**，不会收紧——企业要收紧权限，必须走黑名单字段（如 `blockedCommands`）或专门的锁定开关（如 `allowManagedPermissionRulesOnly`，详见 Part 3 第 9 章 §4）。
- 对于**黑名单型字段**（如 `blockedCommands`、`deniedMCP`）：跨层拼接 = **各层共同扩充禁止范围**。这才是企业策略"添加限制"的机制。

> 原书早期版本写"企业策略可以添加限制但不能移除项目级的配置"——这对黑名单字段成立，但对白名单字段会误导读者以为企业通过 allowedTools 就能收紧权限。**准确表述**：对白名单字段，跨层合并只能"扩充允许项"，不能"收紧允许范围"。

### 2.2 同来源合并（同一层的多个文件）

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

合并结果: { allowedTools: ["Bash", "Glob"] }  // 后者替换前者
```

**数组替换**：同一来源的多个文件，后加载的替换先加载的。

**为什么两套规则不同？**
- 跨来源拼接：允许每一层都*贡献*配置——企业策略加安全限制，用户加个人偏好
- 同来源替换：同一层的多个文件之间，后者为准——避免配置碎片化

### 2.3 `settingsMergeCustomizer`

这个 lodash `mergeWith` 的自定义函数（`settings.ts:538-547`）实现了上述语义。它只有 10 行代码，但影响了整个系统的配置行为。

---

## 3. Drop-in 目录：systemd 的灵感

### 3.1 什么是 Drop-in

```
/etc/claude-code/managed-settings.d/
  ├── 00-base.json         ← 基础配置
  ├── 10-security.json     ← 安全策略（覆盖基础中的安全部分）
  └── 20-team-override.json ← 团队特定覆盖
```

按文件名字母序加载，后加载的覆盖先加载的（同来源替换规则）。

### 3.2 为什么用这种模式

这直接借鉴了 Linux systemd 的 drop-in 目录设计：
- **不需要修改主配置文件**——添加新的覆盖只需放入一个新文件
- **可追溯性**——每个文件对应一个配置意图，容易审计
- **可组合性**——不同团队可以各自提供自己的配置片段

---

## 4. Zod Schema 验证

### 4.1 验证策略

所有配置文件在加载时通过 Zod v4 schema 验证（`validation.ts`）。

**关键设计**：`filterInvalidPermissionRules()`——一条坏的权限规则不会毒化整个文件。系统只过滤掉无效的规则，保留有效的。

**比喻**：水厂过滤杂质——不是一发现杂质就关闭整个供水系统，而是过滤掉杂质，干净的水继续供应。

### 4.2 向后兼容合约

`types.ts:210-240` 中的注释定义了向后兼容合约：
- 新版本的 schema 必须接受旧版本的配置文件
- 可以添加新字段（用默认值）
- 不能移除或改变已有字段的含义
- 不能让之前有效的配置变无效

---

## 5. `strictPluginOnlyCustomization`：前向兼容降级

这是配置系统中最精巧的设计之一。

### 5.1 问题

企业策略设置了 `strictPluginOnlyCustomization: true`（只允许企业指定的插件）。但旧版本的 Claude Code 不认识这个字段——旧版本会完全忽略它。

### 5.2 解决方案

`preprocess()` 函数在旧版本中把不认识的字段降级处理。降级的方向是**只能更宽松，不能更严格**：

```
新版本（认识此字段）: strictPluginOnlyCustomization: true → 严格限制
旧版本（不认识此字段）: 忽略 → 默认行为（不限制）
```

**降级方向是"少锁"**——旧版本的行为比新版本更宽松。这在安全上不是最优（更宽松=更不安全），但在可用性上是正确的（旧版本至少能正常工作，不会因为不认识的配置崩溃）。

---

## 6. 反递归保护

### 6.1 问题

`loadSettingsFromDisk()` 可能被递归调用——加载配置时可能触发其他代码，其他代码又需要读取配置。

### 6.2 解决方案

`settings.ts:639-649`：一个全局布尔标志 `isLoading`。如果在加载过程中被再次调用，直接返回缓存的旧值。

这是一个简单但有效的**反递归保护**——防止无限循环。

---

## 7. 运行时更新

### 7.1 `updateSettingsForSource()`

配置不仅在启动时加载——运行时也可以修改（通过 `/config` 命令或 IDE 集成）。

`updateSettingsForSource()`（`settings.ts:416-524`）处理运行时更新，有一个关键语义：**`undefined` 表示删除**。如果把一个字段设为 undefined，它会从配置中移除，恢复为上一层的值。

### 7.2 Settings 变更监听

系统使用 `chokidar`（文件系统监听库）监听配置文件变化。当检测到变化时，自动重新加载配置。

---

## 8. 设计取舍

### 优秀

1. **九层配置 + 两套合并规则**——看起来复杂，但精确满足了"企业锁定 + 用户灵活"的需求
2. **Drop-in 目录**借鉴 systemd 最佳实践——可组合、可追溯、不修改主文件
3. **filterInvalidPermissionRules**的容错设计——一条坏规则不毒化整个文件
4. **向后兼容合约**明确写在代码注释中——不是口头约定，是可验证的承诺
5. **`strictPluginOnlyCustomization` 的降级方向**选择"少锁"——优先保证可用性
6. **反递归保护**简单有效——用最小的代码量解决了真实的递归风险

### 代价

1. **九个来源的优先级**对用户认知是巨大负担——"为什么我改了配置没生效"很可能是因为高优先级来源覆盖了
2. **两套合并规则（跨来源拼接 vs 同来源替换）**行为不直观——需要记住"什么时候拼接、什么时候替换"
3. **Policy Settings 的 first-source-wins**意味着冲突被静默解决——没有警告说"你的配置被企业策略覆盖了"
4. **前向兼容降级到"少锁"**在安全上是妥协——旧版本的 Claude Code 会比新版本更不安全
5. **`undefined` 表示删除**是一个容易出错的 API 设计——和 JavaScript 的 `undefined` 语义混淆

---

> **[图表预留 2.11-A]**：九层套娃图 — 从 Policy Settings 到 Default Settings 的嵌套关系
> **[图表预留 2.11-B]**：两套合并规则对比图 — 跨来源拼接 vs 同来源替换的数据流
> **[图表预留 2.11-C]**：配置生效诊断流程图 — "为什么我的配置没生效"的排查步骤
