Book 第二十八章:配置治理为什么要拆成几层
第五部分:会话续航与治理

第二十八章:配置治理为什么要拆成几层

拆企业治理、只读策略、用户漫游和远端受管配置这几条并行配置面。

1. 为什么这一块必须单独拆

Claude Code 里和“配置治理”有关的代码,不是一套配置文件读写这么简单。

至少有三条完全不同的线同时存在:

  1. Settings Cascade 本地和远端配置怎么合并,最后变成一份有效 settings。
  2. Managed / Policy 企业或管理员怎么把只读策略压进运行时,影响权限、MCP、hooks、插件、登录、远端能力。
  3. Settings Sync 用户自己的设置和 memory 怎么在不同环境之间漫游。

这三条线表面上都像“settings”,但设计目标完全不一样:

  • policySettings管理员注入
  • policyLimits服务端功能闸门
  • settingsSync用户个人漫游

如果不把它们拆开,后面做 C# 版时非常容易抄歪:

  • 把用户同步数据当成企业策略
  • 把组织级 feature gate 塞进本地 settings 合并树
  • 把 managed settings 误做成可编辑 source
  • 把远端策略加载做成启动期阻塞点

相关源码锚点:

  • src/utils/settings/settings.ts
  • src/utils/settings/types.ts
  • src/utils/settings/constants.ts
  • src/utils/settings/changeDetector.ts
  • src/utils/settings/applySettingsChange.ts
  • src/utils/settings/mdm/settings.ts
  • src/utils/managedEnv.ts
  • src/utils/permissions/permissionsLoader.ts
  • src/services/remoteManagedSettings/*
  • src/services/policyLimits/*
  • src/services/settingsSync/*
  • src/main.tsx
  • src/entrypoints/init.ts

2. 先说结论

我对这一块的判断是:

Claude Code 的配置治理不是一条 settings 链,而是四层叠起来的运行时治理面。

  1. 本地设置层 user / project / local / flag / plugin base
  2. 受管策略层 policySettings 只读、最高信任、能影响多个子系统
  3. 服务端功能闸门层 policyLimits 不进 settings 树,直接决定某些能力能不能开
  4. 用户漫游层 settingsSync 把用户自己的 settings / memory 在环境间搬来搬去

这里面最值钱的设计,不是“配置从哪里读”,而是下面这些边界:

  • policySettingspolicyLimits 是两种不同治理面。
  • managed settings 是“只读策略注入”,不是普通 source。
  • settings sync 是“个人状态同步”,不是企业管理。
  • 企业策略加载默认 fail-open,但某些高风险点会单独加防线。
  • 热更新不是 watcher 直接改内存,而是统一走 cache reset + listener fan-out。

3. 总体结构图

flowchart TD
    A["本地文件 / 进程参数 / 插件"] --> B["settings.ts<br/>本地 settings 合并"]
    C["remoteManagedSettings API"] --> D["policySettings"]
    E["MDM / HKLM / plist / managed-settings.json"] --> D
    B --> F["effective settings"]
    D --> F

    G["policyLimits API"] --> H["isPolicyAllowed()<br/>功能闸门"]

    I["settingsSync API"] --> J["用户文件漫游<br/>settings.local.json / CLAUDE.md"]
    J --> B

    K["changeDetector + notifyChange"] --> L["applySettingsChange"]
    L --> M["AppState / PermissionContext / Hooks / Env"]

    F --> M
    H --> M

这张图最关键的一点是:

Claude Code 没把所有治理都塞进一份 settings.json。

它明确分成了:

  • 合并型配置
  • 只读型策略
  • 服务端布尔闸门
  • 用户数据同步

4. 本地 Settings Cascade:这是配置树,不是策略树

源码锚点:

  • src/utils/settings/constants.ts
  • src/utils/settings/settings.ts
  • src/utils/settings/settingsCache.ts

最基础的设置来源顺序在 SETTING_SOURCES 里定义得很清楚:

  1. userSettings
  2. projectSettings
  3. localSettings
  4. flagSettings
  5. policySettings

还有一个经常被忽略的底座:

  • pluginSettingsBase

它是最低优先级 base layer,后面的文件型 source 会盖上去。

4.1 普通 source 用“深合并”,policy source 不是

普通 settings source 的合并方式是:

  • 对象走深合并
  • 数组走“拼接 + 去重”

这是 settingsMergeCustomizer() 干的。

policySettings 很特殊,它不是“多个政策来源字段级合并”,而是:

先选一个赢家,再把赢家整份并进总 settings。

这就是文档里一直写的:

first source wins

5. policySettings 的真正语义:受管策略注入层

源码锚点:

  • src/utils/settings/settings.ts
  • src/utils/settings/mdm/settings.ts
  • src/utils/settings/managedPath.ts
  • src/services/remoteManagedSettings/syncCacheState.ts

policySettings 不是一个普通文件来源,而是一个“聚合后的受管策略 source”。

它内部又有自己的优先级:

  1. remote managed settings
  2. MDM / HKLM / macOS plist
  3. managed-settings.json + managed-settings.d/*.json
  4. HKCU

注意这里最重要的点:

这四层不是字段级 merge,而是整份 source 选最高优先级赢家。

也就是说:

  • 一旦 remote managed settings 有内容
  • 本地 MDM / 文件 / HKCU 全部不再参与 policySettings 的最终值

5.1 但文件型 managed settings 自己内部又会 merge

这个地方很容易看错。

policySettings 整体是 first-source-wins,但文件型 managed settings 内部又有一层自己的 merge 规则:

  • 先读 managed-settings.json
  • 再读 managed-settings.d/*.json
  • drop-in 按文件名排序
  • 后面的 drop-in 盖前面的

也就是说它同时用了两种模型:

  1. 跨 managed 来源 first-source-wins
  2. 文件型 managed 来源内部 base + drop-ins merge

这个设计很成熟,因为它同时满足了两种诉求:

  • 企业可以定义“哪一类来源最可信”
  • 同一台机器上的管理员团队又能拆成多个独立 policy fragment

5.2 policySettings 是只读 source

settings.tsupdateSettingsForSource() 明确把 policySettingsflagSettings 排除在可编辑 source 之外。

这意味着:

  • 用户可以改 user/project/local
  • 管理策略只能来自外部治理面
  • 运行时不会把 managed settings 当成本地可写配置口

这个边界很关键。

6. Remote Managed Settings:企业策略的远端注入层

源码锚点:

  • src/services/remoteManagedSettings/index.ts
  • src/services/remoteManagedSettings/syncCache.ts
  • src/services/remoteManagedSettings/syncCacheState.ts
  • src/services/remoteManagedSettings/securityCheck.tsx

这一层干的不是“同步用户设置”,而是:

把企业管理员在服务端配置的 managed settings 拉下来,投进 policySettings

6.1 它有很强的 eligibility 和性能意识

remote managed settings 不是所有人都打 API。

源码里会先判断:

  • 是否 first-party provider
  • 是否 first-party base URL
  • 是否是支持这套策略的订阅形态
  • 是否是特殊 entrypoint,比如 local-agent

而且这套 eligibility 还专门拆成了 syncCache.ts / syncCacheState.ts,就是为了避开 settings.ts -> auth.ts -> settings.ts 这种启动环依赖。

这说明作者对启动图谱是非常敏感的。

6.2 它不是盲拉,而是 checksum + 缓存校验

remoteManagedSettings 会:

  • 对本地缓存 settings 做稳定排序
  • sha256
  • 用它做 If-None-Match

服务端回:

  • 304 继续用本地缓存
  • 204/404 视为“当前没有 remote managed settings”
  • 200 返回完整 settings

这个细节很值钱,因为它不是简单“本地有缓存就读缓存”,而是:

本地缓存 + 内容校验 + 幂等刷新

6.3 它默认 fail-open,但会吃 stale cache

如果网络失败:

  • 有旧缓存,就继续用旧缓存
  • 没缓存,就继续启动,但不带 remote managed settings

这就是一个很典型的企业客户端策略:

  • 不让治理 API 把本地 CLI 整体拖死
  • 但尽量维持上一次已知策略

6.4 它不是只在启动拉一次,还会后台轮询

remoteManagedSettings 会启动每小时一次的后台 poll。

如果内容变化了,就会:

  • 更新 session cache / 持久化文件
  • 触发 settingsChangeDetector.notifyChange('policySettings')

后面的 permission、hooks、AppState、env 等更新,就统一走这条热更新链路。

6.5 它对“危险策略变更”加了额外检查

这一块很值得学。

不是所有 remote managed settings 更新都会静默生效。

如果新 settings 里包含危险配置,并且和旧配置相比发生了危险变化,交互模式下会弹出一个阻塞对话框让用户确认。

也就是说:

remote managed settings 虽然是管理员推下来的,但 Claude Code 仍然会对高风险配置变更再加一层本地确认。

这说明它不是无脑“企业策略最大”,而是在危险面上加了用户可见防线。

7. policyLimits:服务端功能闸门,不进 settings 树

源码锚点:

  • src/services/policyLimits/index.ts
  • src/services/policyLimits/types.ts

这是这一块最容易和 managed settings 混淆的地方。

policyLimits 不是 settings。

它是另一条服务:

  • 独立 endpoint
  • 独立缓存文件
  • 独立 polling
  • 独立 isPolicyAllowed(policy) 查询接口

返回的数据形态也不一样:

  • 只返回被限制的 policy
  • 缺席的 key 就表示 allowed

7.1 它管的是功能开关,不是参数配置

典型用法是:

  • allow_remote_sessions
  • allow_remote_control
  • allow_product_feedback

这些都不是“配置某个子系统的参数”,而是:

这个能力到底能不能开。

这就是为什么它不该并进 settings 树。

7.2 它的默认哲学也是 fail-open,但有例外

大多数情况下:

  • 没拉到 policy limits
  • 没缓存
  • 不认识这个 policy key

都会按 allowed 处理。

但它也有一个非常有意思的例外:

  • essential-traffic-only 模式下
  • 某些策略会在 cache miss 时 fail-closed

当前代码里给出的例子是:

  • allow_product_feedback

这说明 Claude Code 的 fail-open 不是教条,而是:

默认 fail-open,碰到合规/高敏感流量再按点收紧。

8. settingsSync:这是用户漫游,不是企业策略

源码锚点:

  • src/services/settingsSync/index.ts
  • src/services/settingsSync/types.ts
  • src/cli/print.ts

这一块也特别容易被误读成“远端 settings”。

但它和 remote managed settings 完全不是一回事。

settingsSync 管的是:

用户自己的设置和 memory 文件,在不同 Claude Code 环境之间漫游。

8.1 它同步的是文件内容,不是结构化 settings patch

API 结构是个很简单的平面 entries: Record<string, string>

同步键值长这样:

  • ~/.claude/settings.json
  • ~/.claude/CLAUDE.md
  • projects/<projectId>/.claude/settings.local.json
  • projects/<projectId>/CLAUDE.local.md

这说明它同步的是:

  • 用户全局 settings
  • 用户全局 memory
  • 项目本地 settings
  • 项目本地 memory

而不是完整的 source tree,也不是策略字段级 patch。

8.2 它非常明确地区分 upload 和 download 场景

Interactive CLI:

  • 主要是 本地上传到远端
  • 增量 diff,只传有变化的 entry

CCR / remote / headless:

  • 主要是 从远端下载到本地
  • 而且会尽量在插件安装前完成

这个设计很值钱,因为它没有假设所有宿主都该做双向同步。

8.3 它同步的是 user/local,不是 project/policy

这一点非常关键。

代码里同步的是:

  • userSettings
  • localSettings
  • User memory
  • Local memory

没有把这些东西同步进去:

  • projectSettings
  • policySettings
  • flagSettings

这背后的设计思路非常清楚:

  • checked-in 的 projectSettings 应该跟仓库走,不该跟账户漫游走
  • policySettings 是管理员控制面,不是用户个人数据
  • flagSettings 是本次启动参数,不是持久状态

8.4 它会主动避开 watcher 回声

settingsSync 落本地文件时会先 markInternalWrite(...),然后写文件。

changeDetector 看到这个文件事件时会把它当内部写回声吞掉,避免形成:

  • settings sync 写文件
  • watcher 触发
  • 又当外部变化处理
  • 再次抖动

这个细节很工程化,也很值得在 C# 版照抄。

9. Claude Code 在这块做了哪些额外处理

如果只看功能名,这一层像“配置系统 + 同步系统”。

但源码里实际多做了不少成熟 runtime 才会做的事情。

9.1 启动顺序被仔细编排过

init.ts 里会先初始化 remote managed settings 和 policy limits 的 loading promise。

main.tsx preAction 再:

  1. 先等早起的 MDM / keychain 预取
  2. 再跑 init()
  3. 再异步触发 loadRemoteManagedSettings()
  4. 再异步触发 loadPolicyLimits()
  5. 最后 fire-and-forget 做 settingsSync upload

这说明它在把:

  • 本地可同步拿到的策略
  • 远端可晚到的策略
  • 用户漫游

分成不同优先级执行。

9.2 env 应用有 trust 边界

managedEnv.ts 非常值得学。

在 trust 之前:

  • userSettings
  • flagSettings
  • policySettings

这几个高信任来源的 env 可以完整应用。

projectSettings / localSettings

  • 只能先应用安全白名单 env
  • 全量 env 要等 trust 之后

也就是说:

Claude Code 把“配置来源信任级别”直接做进了 env 应用管线。

9.3 变更传播是统一通道,不是各改各的

配置变化最终都走:

  • settingsChangeDetector
  • fanOut()
  • resetSettingsCache()
  • applySettingsChange()

applySettingsChange() 再统一刷新:

  • merged settings
  • permission rules
  • hooks snapshot
  • AppState
  • effortLevel 等派生状态

这样不会出现每个子系统自己监听文件、自己做半套热更的碎片化局面。

9.4 headless 和 TUI 都有热更新

TUI 路径里有 React 树和 settings hook。

但 headless 模式没有,所以 print.ts 会显式订阅 settingsChangeDetector,再调用 applySettingsChange()

这个点很值钱,因为它说明:

热更新不是 UI 特性,而是 runtime 特性。

10. 这套治理层到底能控制什么

只看 types.ts 里暴露出来的策略面,就已经很明显了。

10.1 权限和审批

  • allowManagedPermissionRulesOnly
  • permissions.*
  • skipDangerousModePermissionPrompt
  • skipAutoPermissionPrompt

其中 allowManagedPermissionRulesOnly 甚至会直接让:

  • 非 managed permission rule 失效
  • “always allow” 选项隐藏
  • 新 permission rule 不再允许落盘

10.2 Hooks 和执行面

  • allowManagedHooksOnly
  • allowedHttpHookUrls
  • httpHookAllowedEnvVars

10.3 MCP 和插件生态

  • allowedMcpServers
  • deniedMcpServers
  • allowManagedMcpServersOnly
  • strictPluginOnlyCustomization
  • strictKnownMarketplaces
  • blockedMarketplaces
  • channelsEnabled
  • allowedChannelPlugins

这里面我最喜欢的一个细节是:

allowManagedMcpServersOnly 只把 allowlist 收口到 managed settings, 但 denylist 仍然允许用户自己加。

换句话说:

  • 管理员决定“哪些能被允许”
  • 用户仍然保留“我自己不要这个”的否决权

这个边界拿捏得很好。

10.4 身份和组织边界

  • forceLoginMethod
  • forceLoginOrgUUID
  • sshConfigs
  • remote.defaultEnvironmentId

其中 forceLoginOrgUUID 还会在启动后主动验证当前 token 的 org。

而且这个验证是 fail-closed 的:

  • 如果配了 required org
  • 但当前拿不到 profile 来确认 org

那就直接判校验失败。

这说明 Claude Code 在“组织归属”这件事上是认真做了强约束的。

11. 对 C# 版最有价值的抽象

如果把这块翻成 C#,我建议不要只做一个 SettingsService

更合适的拆法是:

11.1 配置合并层

  • ISettingsSource
  • SettingsCascade
  • ManagedPolicySourceResolver
  • SettingsCache
  • SettingsChangeBus

11.2 远端策略层

  • IRemoteManagedSettingsClient
  • ManagedSettingsEligibility
  • ManagedSettingsCacheStore
  • ManagedSettingsSecurityGuard
  • ManagedSettingsPoller

11.3 功能闸门层

  • IPolicyLimitsClient
  • PolicyGateStore
  • PolicyGateEvaluator

11.4 用户漫游层

  • IUserSettingsSyncClient
  • SettingsSyncEntryMapper
  • SettingsSyncUploader
  • SettingsSyncDownloader

11.5 应用层联动

  • SettingsRuntimeApplier
  • PermissionRulesReloader
  • HookConfigReloader
  • ManagedEnvironmentApplier

这里最值得照抄的原则是:

把“配置值”“管理员策略”“服务端功能开关”“用户漫游数据”拆成四个不同对象模型。

不要全塞进一个 Settings 类,不然后面一定会互相污染。

12. 最后一句判断

如果只把这块看成 settings 读写,Claude Code 在这部分并不惊艳。

但如果按 runtime 视角看,它其实已经搭出了一套很完整的治理分层:

  • 本地配置怎么合并
  • 管理策略怎么注入
  • 服务端怎么硬关某些能力
  • 用户自己的状态怎么跨环境漫游
  • 热更新怎么统一传播
  • trust 边界怎么卡住危险 env

对做 C# 版来说,这份源码最值钱的地方不是某个字段名,而是这个分层本身。

只要这层分得对,后面不管你宿主是 CLI、桌面端、IDE 插件,还是多会话服务,治理逻辑都不会塌。