s-blog

深扒 Claude Code 源码(四):配置体系、权限引擎与认证存储

settings 五源合并 / 反 RCE 权限管线 / OAuth-PKCE 与秘钥存储的跨平台短板

ssssmy · 2026-06-11 · 9 min · LLM

本文是《深扒 Claude Code 源码》系列第四篇,聚焦配置治理、权限引擎与认证。承接第三篇·API 与 MCP。本篇两章极密集,已浓缩极细节、保留全部小节与核心设计。

8. 配置体系与权限引擎

Claude Code 有两套相互咬合的子系统:面向"声明式策略"的 settings 体系(五源合并 + MDM/drop-in 企业治理),与运行时的权限决策引擎(规则解析、路径安全、LLM 化分类与远程熔断)。此外还有一套并存的"旧体系" ~/.claude.jsonconfig.ts),承载状态而非策略。

8.1 settings 五源合并:后覆盖前、policy/flag 不可裁剪

设置源以 顺序敏感 的常量数组声明:userSettings → projectSettings → localSettings → flagSettings → policySettings,注释明确"later sources override earlier ones"。lodash mergeWith 是"后者覆盖前者"的深合并,因此末尾的 policySettings 拥有最高有效优先级——企业策略永远压过用户配置

settingsMergeCustomizer 是合并语义的关键:对象走深合并,但数组走并集去重。这意味着 permissions.allow 这类数组在五源间是 累加 的——用户、项目、企业的 allow 规则叠加成超集。这是有意权衡:累加让企业能"追加"规则而不必复制,但低优先级源无法通过空数组"清空"高优先级源的规则。

裁剪边界是安全核心。getEnabledSettingSources() 取允许的源后无条件 add('policySettings')add('flagSettings'),且 --setting-sources 只接受 user/project/local。强制了一条不变量:--setting-sources 只能裁剪前三源,永远无法关闭 policy 与 flag不可编辑在写路径上三道防线落实(类型层 Exclude + 运行时 no-op + permissionsLoader 检查)。

值得注意的反 RCE 设计:读取"是否已接受危险模式弹窗"“分类器规则"这类安全敏感开关时,刻意只查 user/local/flag/policy 而排除 projectSettings——注释直言"a malicious project could otherwise auto-bypass the dialog (RCE risk)”。即被检出仓库的 .claude/settings.json 无权自我提权。

8.2 managed-settings.d drop-in(对标 systemd/sudoers)

loadManagedFileSettings() 实现两层结构:先解析 managed-settings.json 作基座,再读 managed-settings.d/ 目录、按文件名字母序排序后逐个叠加。注释明确对标 systemd/sudoers 的 drop-in 约定:基文件给默认值,drop-in 做定制(20-security.json 覆盖 10-otel.json),不同团队投递互不耦合的策略片段。

8.3 MDM 读取与 first-source-wins 优先级

平台映射:macOS 读 com.anthropic.claudecode 偏好域(仅 /Library/Managed Preferences/);Windows 读 HKLM\SOFTWARE\Policies\ClaudeCode(管理员)和 HKCU(用户可写);Linux 退回文件型。注册表键刻意放在 SOFTWARE\Policies 下,因为该路径在 WOW64 共享键列表上——32/64 位进程读到同一份值不被重定向到 WOW6432Node

first-source-wins 是 policy 源的核心特性:普通源是合并,但 policy 是"最高优先级的非空源整盘胜出"。优先级链 remote > HKLM/plist > managed-settings(file) > HKCU关键拐点在落到 HKCU 之前先检查文件型策略是否存在,存在则让上层走文件分支——保证 HKCU 这个用户可篡改的源永远是最低优先级。

8.4 config.ts 旧体系与 re-entrancy guard

~/.claude.json 承载 状态 而非策略(OAuth 账户、主题、onboarding、信任传播)。re-entrancy guard 是最精巧的防御:模块级 insideGetConfig 标志针对一条具体的无限递归链 getConfig → logEvent → shouldSampleEvent → getGlobalConfig → getConfig——配置损坏时分析采样要读 GrowthBook(存于全局配置)又回头调 getConfig。守卫把分析日志限制在最外层调用打破递归。

另一组关键设计是 防 auth 丢失(GH #3117):wouldLoseAuthState 比较内存缓存与即将写入的值,若缓存有 oauthAccount 而新值没有,判定为"另一进程 mid-write 读到默认值",拒绝写回。读路径用内存快照 + watchFile 1 秒轮询实现跨进程感知,自身写入通过 write-through 把 mtime 故意超调使 watcher 跳过自己的写。

8.5 权限决策:规则源、字符串解析、企业锁定

规则字符串解析非平凡:用"前导反斜杠奇偶数判转义"定位第一个未转义 ( 和最后一个未转义 ),要求闭括号必须在末尾。转义顺序敏感(先转反斜杠再转括号),保证 python -c "print(1)" 这类内含括号的命令往返无损。LEGACY_TOOL_NAME_ALIASESTask→AgentKillShell→TaskStop 等历史名规范化。

决策管线步骤编号即优先级:1a 工具级 deny → 1b 工具级 ask → 1c 工具自身 checkPermissions → 1d 工具实现 deny → 1e requiresUserInteraction → 1f/1g 内容级 ask 规则与 safetyCheck 是 bypass-immune 的(用户显式配的 ask 与 .git/.claude 安全检查,连 bypassPermissions 模式都得弹窗)→ 2a bypass 放行 → 2b 工具级 allow → 3 passthrough 转 ask。这个**"deny 与 safety 先于 bypass、bypass 先于 allow"的排序是整个引擎的安全骨架**。

企业锁定 allowManagedPermissionRulesOnly === true 时只加载 policy 一源、清空 user/project/local 全部规则,并拒绝持久化新规则、隐藏"始终允许"选项——给受监管环境的"只认管理员规则"硬锁。

8.6 filesystem 路径权限

读写权限都对 原始路径 + 符号链接解析后路径 双重检查防 symlink 绕过。读权限的步骤序意味深长:deny → ask 必须先于"edit access implies read access"(注释强调 SECURITY),即显式读 deny 不会被"有写权即有读权"绕过。

路径遍历防护多层叠加:算相对路径后拒 ..、一律转小写防 .cLauDe/ 大小写绕过、hasSuspiciousWindowsPathPattern 拦截 NTFS ADS(file::$DATA)、8.3 短名、长路径前缀、DOS 设备名、UNC 路径。注释解释为何选"检测而非规范化":规范化依赖文件已存在、有 TOCTOU 风险。

~/.claude 保护由危险目录/文件清单驱动。.claude/ 有精细豁免:session 级的 .claude/** allow 规则可越过安全块,但要求前缀以 /.claude/ 开头、不含 ..、以 /** 结尾——实现"只授权单个 skill 目录"而不暴露 settings.json/hooks/

8.7 yoloClassifier:LLM 化权限分类 + 远程 killswitch

TRANSCRIPT_CLASSIFIER 特性下,auto 模式不弹窗而用 LLM 分类器替代。安全考量贯穿构造:buildTranscriptEntries 只取 assistant 的 tool_use 块、丢弃 assistant 文本——因为模型自述可能被构造来影响分类器判决;用 JSON 转义保证恶意内容无法伪造 {"user":...} 行越权。

分类器有 2-stage XMLtool_use 两条路径。XML 两阶段是性能/精度权衡:stage1 用 max_tokens=64 求快速 yes/no,若 allow 立即返回,若 block 才升级 stage2 跑 chain-of-thought 降假阳。所有不可解析、错误、超长情况一律 shouldBlock: true(fail-safe blocking)

denialTracking 回退 防分类器陷入"拒绝-重试-拒绝"死循环:maxConsecutive: 3, maxTotal: 20,超限时交互模式回退人审、headless 直接抛 AbortError 终止 agent。

bypassPermissions 远程 killswitch 是企业反制本地提权的终极手段:首个 query 前跑一次 checkSecurityRestrictionGate('tengu_disable_bypass_permissions_mode'),命中则用户即便本地开了 --dangerously-skip-permissions 也被远程吊销——"远程门控优先于本地设置"的双层关停。


9. 认证与秘钥存储

本章剖析"认证来源决策"与"秘钥持久化"两条主线。前者由 auth.ts 中两个互为镜像的纯函数统领,后者由 secureStorage/ 的平台分发器决定凭据落到 macOS Keychain 还是明文文件。

9.1 来源决策的优先级与守卫

isAnthropicAuthEnabled() 回答"是否走 OAuth",getAuthTokenSource() 回答"bearer token 从哪来"。决策顺序从高到低:

  1. --bare 模式:气密模式,OAuth 一律禁用,token 源只认 flag 提供的 apiKeyHelper,env/keychain 全部忽略。
  2. ssh 代理特例 ANTHROPIC_UNIX_SOCKETclaude ssh 时 API 调用经反向转发的 Unix socket 打到本地"注入式代理"。launcher 把 CLAUDE_CODE_OAUTH_TOKEN 设成占位符只为让远端带上 oauth-2025 beta 头。远端 ~/.claude 的 apiKeyHelper 绝不能翻转这个判定,否则与代理注入的头不一致导致 “invalid x-api-key”。
  3. 第三方云 3P:Bedrock/Vertex/Foundry 任一为真即禁 OAuth,凭据走各云 SDK(AWS STS / GCP ADC),均带"项目设置须先过 trust dialog"的安全闸。
  4. 外部 API key / auth token:存在则禁用 OAuth——除非处于托管 OAuth 上下文。

isManagedOAuthContext() 守卫是关键安全设计:判定 CLAUDE_CODE_REMOTECLAUDE_CODE_ENTRYPOINT==='claude-desktop'。语义是 CCR 与 Claude Desktop 这类托管会话由宿主用 OAuth 拉起,绝不能回落到用户为本地终端准备的 API-key 配置——否则用户终端里的 key 会被每个 CCD 会话复用,key 过期/组织错配就直接挂掉。3P 标志受此守卫保护(云路由是部署级决策)。

9.2 OAuth 主流程:PKCE + state、token 交换

PKCE 原语标准正确:verifier = base64url(randomBytes(32)),challenge = base64url(sha256(verifier))。buildAuthUrl 硬编码 code_challenge_method=S256(无 plain 降级)。CSRF 防护落在监听器:回调里 if (state !== expectedState) 则拒绝。

exchangeCodeForTokens POST 带 code_verifier(PKCE 验证)。refreshOAuthToken 的一项重要优化:当 profile 与 subscription 数据都齐全时跳过 /api/oauth/profile 往返,注释称每天省约 7M 次请求。两套 scope 对应两套流程——Console 流程拿 OAuth 仅为换长期 API key(createAndStoreApiKey),Claude.ai 订阅流程直接用 OAuth token 推理。

CLIENT_ID 是 9d1c250a-e61b-44d9-88ed-5944d1962f5e,token 端点 platform.claude.com/v1/oauth/tokenCLAUDE_CODE_CUSTOM_OAUTH_URL 仅允许白名单(FedStart/PubSec),否则抛错防 token 外泄。

9.3 秘钥存储:平台分发与安全短板

getSecureStorage() 平台分发逻辑只有三行有效语义:

  • darwin:Keychain 为主、明文为兜底。
  • 其余所有平台(含 Linux 与 Windows):直接明文 plainTextStorage

这里有一处明确的安全短板index.ts:14 留了 // TODO: add libsecret support for Linux,Windows 路径根本没有出现——没有任何 DPAPI/Credential Manager 集成。全部凭据(含 OAuth refresh token)以 JSON 明文写到 .credentials.jsonupdate()chmodSync(0o600) 并返回明文警告。在 Windows 上 chmod 0600 几乎是 no-op(NTFS ACL 不遵循 POSIX mode),所以 Windows 上长效 refresh token 实质是当前用户可读的明文文件,缺乏 OS 级加密——这是该子系统在非 macOS 平台上最薄弱的一环。

macOS Keychain 实现的工程细节扎实:写入用 security -istdin 而非 argv,把 JSON 先 hex 编码——目的(INC-3028)是让 CrowdStrike 这类进程监控只看到 security -i 而非 payload;但 stdin 有 4096 字节缓冲限制,超长退回 argv 传 hex(“能被定向观察者还原,但好过静默凭据损坏”)。读取带 30s TTL 缓存(同步 spawn 约 500ms,50+ connector 启动时短 TTL 会引发 5.5s 事件循环停顿)并实现 stale-while-error(刷新失败继续供旧值而非缓存 null)。

fallback 语义有两处迁移防护:首次从 secondary 迁到 primary 成功时删除 secondary(解决 host/container 共享 .claude);primary 写失败但改写 secondary 成功时 best-effort 删除 primary 旧条目,否则已轮换掉的旧 refresh token 会遮蔽新值导致 /login 死循环(#30337)。

9.4 OAuth token 读写汇合点与并发治理

并发与跨进程一致性是重头戏:checkAndRefreshOAuthTokenIfNeeded() 对非 force 调用做 in-flight 去重;刷新前用 lockfile.lock 取文件锁,ELOCKED 时带抖动退避重试至多 5 次;invalidateOAuthCacheIfDiskChanged() 比对 .credentials.json mtime 解决"终端1刷新、终端2 memoize 永不重读导致 /login 无限回归"(CC-1096)。401 处理先比对 keychain 里是否已有别的 tab 刷出的新 token,有则复用,否则强制 force-refresh。isOAuthTokenExpired 留 5 分钟缓冲,推理-only token(expiresAt===null)永不判过期。


下一篇(五)进入终端 UI 与多 Agent:魔改 Ink fork 的差分渲染管线、多 Agent 编排的四条派生路径与文件邮箱 IPC。

原文链接:https://www.ssssmy.com/notes/claude-code-deep-dive-config-auth