一句话结论
这个项目里的沙箱不是用来替代权限系统,而是用来给 shell 命令 再套一层 OS 级能力边界:- 权限系统决定:这次工具调用要不要执行
- 沙箱决定:就算执行了,这个子进程最多能碰到哪些文件、哪些网络目标
实现分层:仓库里的适配器,加底层运行时
这个项目的“沙箱实现”其实分成两层:- 这一层仓库自己负责:策略、配置转换、启停判断、命令包裹、清理和权限联动
- 真正做 OS 级隔离的是外部运行时
@anthropic-ai/sandbox-runtime
src/utils/sandbox/sandbox-adapter.ts 里,可以很清楚地看到这条边界:项目导入 SandboxManager as BaseSandboxManager、SandboxViolationStore 等运行时对象,然后在外面再包一层符合 Claude Code 自身权限模型的适配器。
底层隔离在不同平台上的落地也不是同一套实现:
- macOS 走
sandbox-exec - Linux / WSL2 走
bubblewrap + seccomp - Windows 原生不支持这套 shell 沙箱
它到底解决什么问题
如果只有应用层权限系统,Claude Code 需要在命令执行前尽量判断:- 这条命令是不是只读
- 会不会写危险路径
- 会不会连到外网
- 会不会通过复合命令、重定向、子进程、解释器脚本绕过检查
bash script.shpython -c "..."makenpm install- 某个命令再启动另一个子进程
为什么“拦住它”本身就是价值
很多人第一次看到沙箱会直觉觉得:
如果连 /etc/hosts 这种文件都默认不让我改,那沙箱是不是没什么用?
这个项目的答案正好相反。沙箱不是为了让 /etc/... 这种系统路径也能随便改,而是为了把 shell 命令的能力压缩到一个可接受的安全边界里:
- 权限系统负责判断“要不要执行”
- 沙箱负责限制“就算执行了,最多能做到什么”
/etc/... 被默认拦住,说明这条边界真的在生效,而不是说明沙箱没价值。更具体地说,沙箱至少补上了 4 件权限系统单独做不好的事。
1. 给 shell 一个 OS 级兜底
src/utils/bash/ast.ts 开头就写得很明确:Bash AST 分析不是沙箱,它只是在判断我们能不能可靠地理解命令结构,不能阻止危险命令真的运行。
这就是为什么应用层再聪明,也很难仅靠“执行前推断”覆盖完整风险面。像下面这些命令,真实副作用都要到运行时才完全展开:
bash script.shpython -c "..."makenpm install- 一个命令再起新的子进程
2. 让“安全边界内”的命令可以少弹窗甚至自动放行
默认沙箱白名单里就包含当前工作目录和 Claude 临时目录,这也是为什么工作区内的大多数开发命令都能顺畅运行:npm testrggit status- 工作区内的构建、测试和生成文件
autoAllowBashIfSandboxed。它的核心思路不是“更大胆地信任模型”,而是“既然命令已经被 OS 级边界收紧,就没必要再让用户为大量低风险 Bash 命令反复点确认”。
换句话说,没有沙箱的话,系统通常只剩两种都不太理想的选择:
- 频繁弹窗,让工作流很碎
- 更激进地信任应用层判断,把风险全压在静态分析上
3. 把“出错”的后果从系统级破坏,降成一次受限失败
这也是 Defense-in-Depth 最实际的一层收益。模型偶尔会出错,应用层规则也可能有漏判。沙箱的意义不是假设前面永远正确,而是即使前面偶尔判错,后果也尽量可控。 例如这类命令:sudo tee /etc/hostsmv ... ~/.ssh/...curl 外网 | bash
4. 拦截运行时绕过和逃逸路径
这个仓库在src/utils/sandbox/sandbox-adapter.ts 里专门把一些高风险路径额外加入 denyWrite,例如:
settings.json.claude/skills- 一些 bare git repo 相关路径
- “沙箱把
/etc拦了,所以没用”
- “沙箱把 shell 的默认权限收缩到工作区和白名单里,因此系统级路径默认写不了;正因为这样,项目才敢把一大批工作区内命令自动放行。”
设计边界:它保护什么,不保护什么
保护对象
- Bash / shell 命令执行
- 在支持平台上的 PowerShell 执行
- shell 子进程的文件系统写入范围
- shell 子进程的网络访问范围
- 一些已知的高风险路径和沙箱逃逸向量
不直接保护的对象
FileEditTool/FileWriteTool这类直接文件工具- 纯应用层的权限弹窗和规则匹配
- Bash AST 解析本身
哪些场景会走沙箱
1. 启动阶段先判断“沙箱能不能用”
沙箱不是等到第一条命令执行时才临时判断的。REPL / CLI 启动时,就会先检查当前环境是否真的具备沙箱条件。核心判断包括:- 当前平台是否受底层 runtime 支持
- 依赖是否齐全
sandbox.enabled是否打开- 当前平台是否落在
enabledPlatforms范围内
sandbox.failIfUnavailable,则会直接拒绝启动,而不是悄悄降级成无沙箱模式。
另外,启动时不只是“看一眼能不能用”,而是真的会调用初始化流程,把当前设置转换成 runtime 配置并交给底层 BaseSandboxManager.initialize(...)。后续如果设置变化,还会通过 updateConfig(...) 热更新,而不是要求重启整个会话。
2. BashTool 默认会走
只要满足下面条件,Bash 命令默认会进入沙箱:- 当前平台支持沙箱
- 沙箱依赖齐全
sandbox.enabled打开- 当前平台在
enabledPlatforms范围内 - 这条命令没有被显式排除
- 这次调用没有被允许以
dangerouslyDisableSandbox绕过
packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts 和 src/utils/sandbox/sandbox-adapter.ts。
3. PowerShell 只在支持平台上走
PowerShell 的处理要更细一点:- Linux / macOS / WSL2:可以走沙箱
- Windows 原生:不支持沙箱,直接返回
shouldUseSandbox: false
4. Hook 命令会复用“网络专用沙箱”
Hook 不是完整复用 Bash 那套文件系统限制,而是额外套了一层 network-only sandbox:- 重点拦网络访问
- 文件系统不额外收紧到 Bash 那个程度
哪些场景不会走沙箱
1. FileEditTool / FileWriteTool
这类工具不是靠 shell 修改文件,而是直接在应用层做文件 I/O,所以它们不通过Shell.exec(),自然也不会被 wrapWithSandbox() 包裹。
它们走的是另一条链路:
checkWritePermissionForTool()checkPathSafetyForAutoEdit()- 工作目录检查
- allow/ask/deny 规则
- “shell 改
/etc/hosts”通常是沙箱在 OS 层拦 - “FileEdit 改
/etc/hosts”通常是权限系统在应用层拦
2. 明确排除的命令
如果命中sandbox.excludedCommands,这条命令会直接跳过沙箱。
支持三类模式:
- 精确匹配
- 前缀匹配
- 通配符匹配
3. 允许 unsandboxed fallback 的命令
如果:- 这次调用显式设置了
dangerouslyDisableSandbox: true - 并且策略允许
allowUnsandboxedCommands
dangerouslyDisableSandbox,提醒这是例外路径,不应当成为默认习惯。
完整执行链路
可以把整个过程拆成两段来看:启动期先把沙箱准备好,命令期再决定“这条命令要不要进去”。启动期链路
命令期链路
典型 Bash 执行链路如下:Shell.exec()。它会在真正 spawn(...) 之前调用 SandboxManager.wrapWithSandbox(...),把原始命令改写成底层 runtime 可执行的沙箱命令串。命令结束后如果本次是 sandboxed execution,再调用 cleanupAfterCommand() 清理运行时残留。
其中有两个容易混淆的判定点:
判定点 A:要不要进沙箱
这是shouldUseSandbox() 的职责。
它回答的是:
这条命令要不要被 OS 级沙箱包起来执行?
判定点 B:这条命令要不要弹权限确认
这是权限系统和 Bash 权限检查的职责。 它回答的是:这条命令在应用层看来,是这两个判定点是并列协作的,不是互相替代的。allow、ask还是deny?
默认沙箱到底限制了什么
沙箱运行时配置最终由convertToSandboxRuntimeConfig() 生成。它会把项目自己的设置、权限规则和安全加固逻辑,转换成底层运行时需要的配置。
这一步很关键,因为这个项目的沙箱配置不是一份静态表,而是从 Claude Code 自己的权限系统里“翻译”出来的。
这些限制是怎么从权限系统推导出来的
WebFetch(domain:...)和sandbox.network.allowedDomains会被合并成网络白名单Edit(...)/Read(...)这类权限规则会被翻译成文件系统读写限制sandbox.filesystem.allowWrite/allowRead/denyWrite/denyRead会继续叠加到最终 runtime 配置上
文件系统默认写入范围
默认allowWrite 只有两类:
- 当前工作目录
. - Claude 的临时目录
- 工作区内的构建、测试、生成临时文件通常能正常运行
- 根路径如
/etc/...、/usr/...、/var/...默认不在写白名单里
文件系统额外写入来源
额外允许写入的路径,主要来自这些来源:sandbox.filesystem.allowWriteEdit(...)规则推导出的路径/add-dir或--add-dir增加的目录- git worktree 主仓库所需路径
强制 deny 的路径
即使有别的配置,项目还会额外加固一些高风险路径,例如:- settings 文件
.claude/skills- 一些 bare git repo 相关路径
网络限制
网络白名单来自两部分:sandbox.network.allowedDomainsWebFetch(domain:...)这类权限规则
autoAllowBashIfSandboxed 的真实意义
这是沙箱设计里最值得注意的开关之一。
它表达的是这样一个信任假设:
如果命令已经被 OS 级沙箱约束在安全边界内,那么应用层就没有必要再对大量低风险 Bash 命令逐条弹确认框。因此,当这个开关开启时:
- 命令会先检查显式
deny/ask规则 - 如果没有命中这些硬规则
- 且命令确实会在沙箱里执行
- 那么 BashTool 可以直接自动允许它运行
- 命中了
excludedCommands - 显式使用了
dangerouslyDisableSandbox: true - 当前平台根本不支持沙箱
ask 规则,因为它们没有拿到 OS 级约束带来的那层安全兜底。
这也是沙箱存在的一个核心产品价值:不是让更多危险操作通过,而是让更多受限范围内的常规命令可以无感运行。
为什么“沙箱把 /etc 拦了”反而说明它有用
前面的“四个核心价值”解释的是原理,这里把结论再落回最常见的直觉疑问上:为什么一个默认不让你写 /etc 的系统,反而更值得信任?
因为 Claude Code 日常最常跑的不是系统管理命令,而是开发命令。例如:
npm testnpm installcargo buildpytestrggit status
autoAllowBashIfSandboxed、提高自动化程度。
所以这个问题的正确落点不是“它为什么不帮我改 /etc”,而是“它能不能在不碰 /etc 的前提下,让大量正常开发命令更安全、更顺滑地运行”。从这个角度看,/etc 默认写不了并不是缺点,而是整个自动化体验成立的前提。
平台差异
macOS
- 底层使用
sandbox-exec - 路径和网络规则通过 Seatbelt profile 落地
- 属于原生 OS 级进程隔离
Linux
- 底层使用
bubblewrap + seccomp - 会建立 mount / PID / network 等隔离
- Linux 上对 glob 路径的支持比 macOS 弱一些
- 某些运行后残留需要在
cleanupAfterCommand()中清理
WSL
- 只支持 WSL2
- WSL1 视为不支持平台
Windows 原生
- 原生 PowerShell/Bash 不支持这个沙箱体系
- 因此只能依赖权限系统和工具级检查
- Windows 原生:通常不走
- Linux/macOS/WSL2:shell 才可能走
工作区内外:应用层与沙箱层如何配合
工作区内路径
工作区内路径通常有两层保护:- 应用层权限检查
- 沙箱默认允许写当前工作目录
工作区外路径
工作区外路径则更严格:- 应用层通常会视为高风险,要求确认或阻止
- 即使应用层允许,如果不在沙箱白名单里,运行时也会失败
Linux 根路径 /etc/...
对于 Linux 上的根路径文件,通常会出现两种情况:
- shell 路径:命令会进沙箱,但沙箱默认没有
/etc写权限,所以运行时被拦 - 文件工具路径:不走沙箱,而是在应用层直接被文件权限检查拦住
用户真的会看到什么
被拦截并不是同一种体验,至少有三类。1. 执行前的权限确认
如果应用层在执行前就判定为ask,用户会看到标准权限对话框:
- Bash 权限确认
- FileEdit / FileWrite 权限确认
- 其他工具自己的权限确认 UI
2. 执行中的沙箱违规
如果命令已经进入沙箱,运行时才触发违规:- 命令会失败
- stderr 会被附加
<sandbox_violations>标签供模型理解 - UI 会清理这些标签再显示给用户
- 同时
SandboxViolationStore会记录违规事件
- 命令失败本身
- 以及“最近有多少次 sandbox blocked”之类的界面提示
3. 网络越界请求
网络是个特例。 当沙箱外的 host 访问需要额外确认时,项目会弹出一个专门的网络授权对话框,例如:Network request outside of sandbox
为什么文件系统越界通常不弹“再放行一次”
这是一个非常有意的设计选择。 对文件系统来说,项目更倾向于:- 执行前在应用层 ask
- 或者执行后让命令直接因沙箱失败
- 边界更稳定
- 用户心智更清晰
- 不容易把 shell 运行时逐步升级成越来越宽松的环境
常见误区
误区 1:沙箱会保护所有文件修改
不是。它主要保护 shell 子进程。 直接文件编辑工具走的是应用层权限系统,不是 shell 沙箱。误区 2:只要启用了沙箱,就不会再需要权限系统
不是。沙箱只限制进程能力,不负责解释用户意图、路径安全语义、工具模式、审批体验。 项目之所以还保留复杂的allow / ask / deny 体系,就是因为两者职责不同。
误区 3:如果某个危险操作被沙箱拦住,就说明应用层检查没价值
不是。应用层检查的价值在于:- 更早提示
- 更好的用户体验
- 更细的语义判断
- 对不走 shell 的工具同样生效
推荐的阅读路径
如果你想继续顺着源码深入,推荐按下面顺序看:packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.tssrc/utils/Shell.tssrc/utils/sandbox/sandbox-adapter.tssrc/utils/permissions/permissions.tspackages/builtin-tools/src/tools/BashTool/bashPermissions.tssrc/utils/permissions/pathValidation.tssrc/utils/permissions/filesystem.ts
FAQ
Q1:Linux 下 echo hi > /etc/hosts 会怎样?
如果是 BashTool:
- 通常会进沙箱
- 默认沙箱不允许写
/etc - 所以命令会在运行时失败
- 不进沙箱
- 通常会在应用层文件权限检查里先被拦下
Q2:Windows 下改 C:\Windows\System32\drivers\etc\hosts 会怎样?
在 Windows 原生环境里,通常没有这套 shell 沙箱兜底,所以主要依赖应用层权限系统和工具自己的检查逻辑。
Q3:既然沙箱这么强,为什么还保留 dangerouslyDisableSandbox?
因为有些真实开发任务确实需要越过默认边界,例如:
- 访问未加入白名单的工具链目录
- 调试系统级环境
- 做管理员明确允许的例外操作
Q4:什么时候最能感受到沙箱的价值?
当你开启autoAllowBashIfSandboxed 时最明显。
这时大量工作区内命令可以少弹窗甚至不弹窗,但即使模型偶尔给出过界命令,系统级写入和网络能力仍然被边界限制住。