Hi,大家好,欢迎来到维元码簿。
本文属于 《Claude Code 源码 Deep Dive》 系列,专注于工具系统中的 权限、沙盒与错误处理 板块。如果你想了解整个系列,可以先看系列开篇 | Claude Code 源码架构概览:51万行代码的模块地图。
本文聚焦一件事:Claude Code 怎么保证一个能执行任意命令的 AI 不搞破坏。 从权限检查链、到沙盒隔离、再到出错恢复------三道防线各管各的,互相补位。
读完全文,你将能回答这几个问题:
- 模型想执行
rm -rf /,谁拦它? 答案:8 层检查链 + AST 权限解析 + AI 分类器,三重防线。 - 大家都在讨论的 YOLO 模式是什么? 答案:
--dangerously-skip-permissions背后的 AI 分类器------你以为跳过了权限,其实还有一个 AI 在守门。 - 开了 bypass 模式就完全不拦了吗? 答案:不是,安全路径检查连 bypass 都免疫。
- 沙盒和权限是什么关系? 答案:两条独立防线。权限是逻辑层(能/不能),沙盒是系统层(做了也隔离)。
本篇覆盖的源码范围
| 模块 | 核心文件 | 核心代码行 | 文件总行 | 职责 |
|---|---|---|---|---|
| 权限系统 | src/utils/permissions/permissions.ts |
L122-231(getAllowRules/getDenyRules/getAskRules) | 1486 行 | 8 层检查链、规则匹配、对话框 |
| YOLO 分类器 | src/utils/permissions/yoloClassifier.ts |
L1012-1487(classifyYoloAction + formatActionForClassifier) | 1495 行 | Auto 模式 AI 守卫 |
| Bash AST 检查 | src/tools/BashTool/bashPermissions.ts |
L161-1491(getSimpleCommandPrefix → checkCommandAndSuggestRules) | 2621 行 | tree-sitter 解析、子命令提取 |
| 沙盒系统 | src/utils/sandbox/sandbox-adapter.ts |
L99-927(resolvePathPatternForSandbox + convertToSandboxRuntimeConfig + SandboxManager) | 985 行 | sandbox-runtime 桥接、FsConfig/NetworkConfig |
| 沙盒决策 | src/tools/BashTool/shouldUseSandbox.ts |
L130-153(shouldUseSandbox 函数) | 153 行 | shouldUseSandbox() 四条判断 |
| 错误处理 | src/services/tools/toolExecution.ts |
L599 起(checkPermissionsAndCallTool catch 块) | 1745 行 | ShellError/McpAuthError/AbortError 分类处理 |
前情提要:安全是工具系统的底线
在姊妹篇[运行时流水线](./03-Claude Code深度拆解-工具系统-运行时流水线.md)中,我们拆解了从 tool_use 到结果回流的 8 步流水线。其中第 4 步"权限检查"只讲了个位置------它在 Hook 之后、执行之前。但权限系统远不止一个检查点:它有 8 层检查链、AST 权限解析、AI 分类器,还有和它互补的沙盒系统。
一个能执行任意 Bash 命令的 AI,安全怎么保证?这就是本文要回答的核心问题。
Claude Code 的安全体系分为三层防线:
- 权限系统(逻辑层):决定"能/不能"------8 层检查链,从硬性拒绝到放行逐层递进
- 沙盒系统(系统层):决定"做了也隔离"------系统调用级隔离,权限拦不住的沙盒兜底
- 错误处理(恢复层):决定"出了问题怎么办"------分类处理、遥测上报、状态恢复

权限系统------8 层检查链
权限检查不是一次判断,而是 8 层逐级决策。 每一层有自己的检查逻辑和保护对象,先命中的先生效------拒绝永远优先于放行。
这在姊妹篇[运行时流水线](./03-Claude Code深度拆解-工具系统-运行时流水线.md)中已经提到了流水线第 4 步的位置------现在我们展开它的完整内部结构:
| 层级 | 名称 | 类型 | 作用 |
|---|---|---|---|
| 1a | 硬 Deny | 拒绝 | 匹配 deny 规则的工具直接拒绝 |
| 1b | Ask | 拒绝 | 匹配 ask 规则的工具弹出对话框 |
| 1c | 工具自身检查 | 拒绝 | 工具定义的 checkPermissions() 回调 |
| 1d | 工具实现拒绝 | 拒绝 | 工具内部逻辑拒绝(如 rm -rf /) |
| 1e | 用户交互要求 | 拒绝 | 必须用户参与的工具(如 AskUserQuestion) |
| 1f | 内容级 Ask | 拒绝 | 按参数内容匹配的 ask 规则(如 Bash(npm publish:*) → ask) |
| 1g | 安全路径 | 拒绝 | 保护 .git/ 等关键路径,bypass 也拦不住 |
| 2a | bypass | 放行 | --dangerously-skip-permissions 模式放行 |
| 2b | alwaysAllow | 放行 | 用户已允许的规则放行 |
| 3 | 兜底 | 兜底 | 以上都没命中,弹出权限对话框让用户决定 |

核心设计原则:拒绝优先于放行。 1a-1g 是硬性拒绝层------如果任何一层命中拒绝,不管 2a/2b 是否放行,工具都不能执行。只有穿过所有拒绝层后,放行层才起作用。
这个顺序保证了:即使用户选了 bypass 模式(2a),硬 Deny 规则(1a)仍然有效。 没有任何用户操作可以绕过 1a-1g 的拒绝检查。
这些规则从哪来? 8 层检查链中的规则来源分三股:
| 来源 | 配置位置 | 生效方式 | 举例 |
|---|---|---|---|
| 用户显式配置 | claude config add alwaysAllow "Bash(git:*)" 或在权限对话框中勾选「always allow」 |
写入 userSettings / projectSettings / localSettings |
Bash(git push:*) → allow |
| Managed settings | managed-settings.json 或远程 API 拉取的策略 |
写入 policySettings,优先级最高,用户无法覆盖 |
全公司统一 deny Bash(rm -rf:*) |
| 系统内置 | checkPermissions() 回调、安全路径硬编码、工具实现内部检查 |
代码逻辑,不可配置 | 1c 工具自身检查、1d rm -rf / 拒绝、1g .git/ 保护 |
源码中的 PERMISSION_RULE_SOURCES(permissions.ts L109)定义了完整的规则源优先级:userSettings → projectSettings → localSettings → flagSettings → policySettings → cliArg → command → session。每个源都可以设置 allow、deny、ask 三种行为。loadAllPermissionRulesFromDisk()(permissionsLoader.ts L120)从所有源加载规则,然后 getAllowRules()/getDenyRules()/getAskRules() 按行为分组------这就是 8 层检查链的数据来源。
Managed settings 可以通过 allowManagedPermissionRulesOnly 配置锁死权限------开启后用户自己配的规则全部失效,只保留 managed settings 下发的策略。这意味着企业场景下,权限系统由 IT 部门完全掌控。
安全路径:Bypass 免疫的硬规则
1g 层"安全路径"是最特殊的拒绝层------它连 bypass 模式都免疫。
什么是安全路径?Claude Code 维护了一组绝对不能被 AI 修改的文件和目录:
.git/目录------Git 仓库的元数据,被改了整个仓库就废了.bashrc/.zshrc等 Shell 配置------被改了可能影响用户整个系统- SSH 密钥文件------泄露了就是安全事故
为什么需要"bypass 免疫"?--dangerously-skip-permissions 这个参数名本身已经警告了用户------它只跳过 2a 层的权限检查,不跳过 1a-1g 的硬性拒绝。安全路径属于 1g 层,所以即使在 bypass 模式下,模型也无法修改 .git/ 目录。
这不是在限制用户,而是在保护用户------你不会想让 AI 不小心把你的 Git 历史改了吧?
YOLO 分类器:Auto 模式的 AI 守卫
上一节提到了 2a 层的 bypass 放行------也就是社区常说的「YOLO 模式」。名字来自 --dangerously-skip-permissions 这个参数,"You Only Live Once",意思是"豁出去了,跳过所有权限检查"。但源码中的实现远没有这么"豁出去"------bypass 只是跳过了 2a 层的用户确认,1a-1g 的硬性拒绝照常工作,而且还在上面叠加了一层 AI 分类器。
yoloClassifier.ts 是一个 52KB 的文件------它是 bypass 模式下的 AI 分类器。用户选了 Auto 模式后,不是完全无审查------你以为跳过了权限,其实还有一个轻量 AI 分类器在默默守门。
YOLO 分类器的工作方式:
-
输入 :
toAutoClassifierInput()把工具名 + 参数序列化为评估文本 -
决策 :
auto-allow(安全放行)或auto-deny(危险拒绝) -
限制:它只能拒绝或放行,不能弹出对话框------这是 bypass 模式的前提条件
BashTool: git push --force
│
▼
toAutoClassifierInput() → "Bash command: git push --force"
│
▼
yoloClassifier() → auto-deny → 拒绝执行
为什么需要 YOLO?因为用户选了 Auto 模式后,不可能每个命令都弹框确认------那和手动模式没区别。但也不能完全放任------rm -rf / 这种命令必须拦住。AI 分类器就是中间态:快速判断、无交互、自动决策。
YOLO 的局限性。 它是一个轻量模型,不是万能的。有些命令的安全边界模糊------npm publish 可能是安全的(发布自己的包),也可能是危险的(覆盖别人的包)。YOLO 分类器可能会误判,所以后面还有拒绝追踪(denialTracking)自动降级机制。
denialTracking:自动降级防止死循环
denialTracking:自动降级防止死循环。
这是 denialTracking 机制的核心逻辑:
- 记录每个工具被拒绝的次数
- 当同一个工具被拒绝超过阈值时,自动降级
- 降级后,后续对该工具的调用会弹出对话框让用户决定
为什么需要这个机制?因为模型可能会"死缠烂打"------它想执行某个命令,YOLO 拒绝了,模型换了个参数又试,又被拒绝,再换参数......如果不降级,模型可能一直在这个循环里浪费 token。自动降级打破了死循环:弹出对话框让人类做最终决定。
BashTool 的 AST 权限检查------子命令粒度的精细控制
Bash 是最危险的工具,也是最需要精细权限控制的工具。
问题来了:怎么检查一个 Bash 命令是否安全?比如 git push --force && npm publish------这是一个命令还是两个?cat file | grep pattern------管道两端的命令安全等级可能不同。
正则匹配不够用。 Bash 语法太复杂:管道 |、重定向 >、子 shell $()、命令替换 `````、逻辑运算 && ||......正则无法准确拆分这些结构。
Claude Code 的方案是用 tree-sitter 做 AST(Abstract Syntax Tree,抽象语法树)解析------AST 把一段代码或命令的结构表示成一棵树,每个节点代表一个语法成分(比如命令名、参数、管道符)。拿到树之后,就可以精确地遍历每个子命令,独立做权限匹配。
-
把 Bash 命令字符串解析为 AST
-
遍历 AST,提取所有子命令
-
对每个子命令独立做权限匹配
输入:git push --force && npm publish
│
▼
tree-sitter AST 解析
│
├── git push --force → 匹配安全路径规则 → ✗ 拒绝
│
└── npm publish → 匹配 Ask 规则 → ? 弹出对话框

AST 解析让权限检查可以精确到子命令粒度。权限规则 Bash(npm publish:*) → ask 匹配的不是整个命令字符串,而是解析后的子命令。这意味着 echo hello && npm publish 中,echo hello 可以直接放行,只有 npm publish 需要确认。
权限拒绝 → 建议系统:拒绝后给出配置建议
权限拒绝后,系统不会只给一个冷冰冰的"拒绝"------它会给建议。
当工具被权限系统拒绝时,系统会生成一段建议文本,告诉用户:
- 这个命令为什么被拒绝
- 如果确实想执行,可以怎么配置(比如添加 alwaysAllow 规则)
- 对应的配置命令
用户可以从建议中快速添加 alwaysAllow 规则------比如 claude config add alwaysAllow "Bash(git push:*)"。这样下次再执行 git push 就不需要确认了。
这个设计解决了权限系统最大的痛点:安全和效率的矛盾。 太严格了每次都要确认,太松了又不安全。建议系统让用户可以渐进式地放宽权限------先严格,遇到常用的命令再逐步放行。这也是渐进式披露的另一种体现:不是一次性放开所有权限,而是按需逐步授权。
沙盒系统------系统调用级隔离
权限系统是逻辑层:它能决定"能不能做"。沙盒是系统层:它能保证"做了也隔离"。
两者是独立防线。权限通过了,沙盒仍然可以隔离;权限拒绝了,沙盒也不需要介入。它们解决的不是同一个问题。
Claude Code 使用 @anthropic-ai/sandbox-runtime 外部 NPM 包实现沙盒。sandbox-adapter.ts 是桥接层,把 Claude Code 的权限规则翻译成 sandbox 的配置(权限系统的完整逻辑见姊妹篇权限系统章节):
| 沙盒维度 | Claude Code 侧 | 沙盒侧 |
|---|---|---|
| 文件系统 | Edit/Read 路径列表 | allowWrite / denyWrite 路径列表 |
| 网络 | WebFetch 域名列表 | allowedDomains / deniedDomains |
| 命令 | 用户 excludedCommands |
tengu_sandbox_disabled_commands(远程加载) |

shouldUseSandbox() 四条判断
不是所有命令都需要沙盒。shouldUseSandbox() 决定了什么时候启动沙盒:
| 路径 | 条件 | 结果 |
|---|---|---|
| 1 | isSandboxingEnabled() = false |
不沙盒 |
| 2 | dangerouslyDisableSandbox = true |
不沙盒 |
| 3 | 命令在 excludedCommands 中 |
不沙盒(便利功能) |
| 4 | 其他 | 走沙盒 |
路径 3 的 excludedCommands 是便利功能------某些命令(如 ls、cat)在沙盒中运行可能影响性能,但不涉及安全。把它排除在沙盒外可以提速,但不是安全边界 ------即使命令不在 excludedCommands 中,沙盒也不保证安全,它只是系统级隔离的额外防线。
autoAllowBashIfSandboxed:沙盒内的信任加速
沙盒启动时,Bash 命令可以自动允许。 这是一个很有意思的设计。
为什么?因为沙盒提供了系统级隔离------即使命令执行了危险操作,也只影响沙盒环境,不影响真实文件系统。既然已经有了物理隔离,权限检查的严格程度可以相应降低。
这不是安全边界的放松,而是安全层级的转移:从"逻辑层拦截"(权限弹框)转移到"系统层隔离"(沙盒兜底)。用户在沙盒模式下不需要频繁确认命令,因为沙盒已经提供了足够的安全保障。
错误处理系统------出了问题怎么办
错误处理不是简单的 try-catch,而是分类、遥测、Hook、状态更新的完整体系。
工具执行中的错误被分为四类:
| 错误类型 | 场景 | 处理方式 |
|---|---|---|
| ShellError | Bash/PowerShell 退出码非 0 | PostToolUseFailure Hook → 结果返回模型(含 stderr) |
| McpAuthError | MCP Server 认证过期(401) | 更新 MCP 客户端状态为 needs-auth → 提示用户重新授权 |
| AbortError | 被取消(siblingAbort 或用户中断) | siblingAbortController 级联取消 → 所有并行工具终止 |
| 通用 Error | 其他运行时错误(Zod 校验失败等) | tengu_tool_use_error 遥测 → 日志 → PostToolUseFailure Hook |

每种错误的处理细节
ShellError 最常见------Bash 命令执行失败。退出码非 0 不一定代表工具"出错"(可能是 grep 没找到匹配),但系统会把 stderr 内容返回给模型,让模型根据错误信息决定下一步。模型通常会修正命令重试,或者换一种方式解决问题。
McpAuthError 是 MCP 特有的问题------MCP Server 的认证 Token 过期了。系统不会简单地报错,而是更新 MCP 客户端的状态为 needs-auth,用户界面会提示重新授权。授权完成后,MCP 工具可以继续使用。
AbortError 在姊妹篇[运行时流水线](./03-Claude Code深度拆解-工具系统-运行时流水线.md)中已经讲过------它是错误级联取消的核心。一个工具被取消后,其他并行工具也会被取消。
通用 Error 包括 Zod 校验失败、文件不存在等各种运行时错误。系统会先发遥测(tengu_tool_use_error),再写日志(logForDebugging),再触发 PostToolUseFailure Hook,最后把错误信息返回给模型。
所有错误的最终归属都一样:错误信息回流到 messages[],模型看到后决定下一步。 错误处理的目标不是"隐藏错误",而是"让模型知道发生了什么,让它自己决定怎么办"------这也是 Agent 系统和传统软件系统的根本区别:传统系统出错要程序自己处理,Agent 系统出错可以交给模型处理。
本章小结
这篇的安全系统读下来,我反复想到一个词:纵深防御。8 层检查链,每层有自己的职责和优先级,拒绝永远优先于放行------这个设计其实很朴素,但效果极强。特别是安全路径那一层,连 bypass 模式都免疫,说明 Anthropic 的工程师在设计权限系统时,心里有一条不可逾越的红线。不管用户怎么配置,.git/ 目录就是不能碰。这种"有些东西就是不能碰"的坚持,比那些"默认允许、出事再补"的设计让人安心得多。
YOLO 分类器的存在让我挺意外的。社区里大家都说 --dangerously-skip-permissions 就是"跳过所有权限",但读了 yoloClassifier.ts 之后才知道,你以为跳过了权限,其实还有一个 AI 在默默地帮你分类------安全的放行,危险的拒绝。这让我重新理解了 Auto 模式的定位:它不是"无权限",而是"用 AI 替代人工确认"。当然,AI 分类器不是万能的,所以后面还有拒绝追踪的自动降级------连续拒绝就弹框,把最终决定权交回人类。这个"AI 先试,试不了交给人"的设计,比我想象的要谨慎得多。
沙盒和权限的关系也值得说一句。读之前我以为沙盒是权限的一部分,是权限检查通过后的"最后一道关"。但源码告诉我,它们是两条完全独立的防线------权限决定"能不能做",沙盒决定"做了也跑不掉"。这种冗余在安全系统里不是浪费,而是必要。任何一层单独看都有漏洞,但叠在一起就补上了彼此的盲区。这大概是安全工程最朴素也最深刻的道理:不要相信单一防线。
系列导航:
本文属于 《Claude Code 源码 Deep Dive》 系列中「工具系统」命题的子篇章,专注于 权限、沙盒与错误处理。
姊妹篇(可独立阅读):
- Claude Code 深度拆解:工具系统------30+ 工具怎么统一注册、按需加载
- Claude Code 深度拆解:工具系统------30+ 内置工具地图与 MCP / Skills 协作
- Claude Code 深度拆解:工具系统------运行时流水线
如果这篇文章对你有帮助,欢迎点赞收藏 支持一下。如果你对 Claude Code 源码感兴趣,欢迎关注本系列 后续更新。有任何想法或疑问,欢迎评论区留言讨论 👋