Claude Code 深度拆解:工具系统——权限、沙盒与错误处理

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_SOURCESpermissions.ts L109)定义了完整的规则源优先级:userSettings → projectSettings → localSettings → flagSettings → policySettings → cliArg → command → session。每个源都可以设置 allowdenyask 三种行为。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 分类器的工作方式:

  1. 输入toAutoClassifierInput() 把工具名 + 参数序列化为评估文本

  2. 决策auto-allow(安全放行)或 auto-deny(危险拒绝)

  3. 限制:它只能拒绝或放行,不能弹出对话框------这是 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 机制的核心逻辑:

  1. 记录每个工具被拒绝的次数
  2. 当同一个工具被拒绝超过阈值时,自动降级
  3. 降级后,后续对该工具的调用会弹出对话框让用户决定

为什么需要这个机制?因为模型可能会"死缠烂打"------它想执行某个命令,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 把一段代码或命令的结构表示成一棵树,每个节点代表一个语法成分(比如命令名、参数、管道符)。拿到树之后,就可以精确地遍历每个子命令,独立做权限匹配。

  1. 把 Bash 命令字符串解析为 AST

  2. 遍历 AST,提取所有子命令

  3. 对每个子命令独立做权限匹配

    输入: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 是便利功能------某些命令(如 lscat)在沙盒中运行可能影响性能,但不涉及安全。把它排除在沙盒外可以提速,但不是安全边界 ------即使命令不在 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 源码感兴趣,欢迎关注本系列 后续更新。有任何想法或疑问,欢迎评论区留言讨论 👋

相关推荐
张忠琳2 小时前
【vllm】(六)vLLM v1 Sample — 模块超深度分析之一
ai·架构·vllm
专职2 小时前
AI核心概念大串联
agent
一只AI打工虾的自我修养2 小时前
DeepSeek V4 Hybrid Attention Architecture 技术解析
人工智能·ai·开源·aigc
薛定谔的猫3692 小时前
基于 MCP (Model Context Protocol) 的智能 Agent 开发指南
ai·llm·agent·mcp·software engineering
阿珊和她的猫2 小时前
大模型在客服场景:落地路径 + 效果评估
ai·agent·llama·cli·mcp
前端双越老师2 小时前
长文:Claude Code 实践 Harness 工程,开发效率翻几倍
人工智能·agent·ai编程
IRevers2 小时前
【Agent】基于Langchain的Agent数据库查询助手
数据库·人工智能·pytorch·sql·深度学习·langchain·agent
新知图书3 小时前
基于ReAct模式的智能体系统示例
人工智能·agent·智能体
阿泽的AI工具笔记3 小时前
OpenClaw 接入大模型 API 的完整配置流程(Windows 实测可用)
windows·ai