Sandbox 机制分析
本文只分析当前仓库源码里能直接确认的 sandbox 机制,不追踪外部依赖 @anthropic-ai/sandbox-runtime 的内部实现。结论先说:
- 仓库内确实存在完整的 sandbox 接入链。
- sandbox 不是单个布尔开关,而是“配置 + 权限判定 + Shell 包装 + 网络授权回调 + UI/SDK/swarm 协同”的组合机制。
- 真正执行 OS 级隔离的底层能力来自外部依赖
@anthropic-ai/sandbox-runtime;本仓库负责的是配置生成、启停条件、调用时机、权限回调和结果治理。
1. 仓库里是否有 sandbox
有,而且是一级能力,不是实验性残留代码。
直接证据包括:
package.json依赖声明了@anthropic-ai/sandbox-runtime。src/entrypoints/sandboxTypes.ts定义了完整的sandbox配置 schema。src/utils/sandbox/sandbox-adapter.ts封装了仓库自己的SandboxManager,作为 Claude Code 和外部 runtime 之间的适配层。src/tools/BashTool/shouldUseSandbox.ts决定某条 Bash 命令是否应该进入 sandbox。src/utils/Shell.ts真正执行命令前会调用SandboxManager.wrapWithSandbox(...)。src/commands/sandbox-toggle/*暴露了/sandbox命令和/sandbox exclude ...用户入口。src/screens/REPL.tsx、src/cli/structuredIO.ts存在 sandbox 网络授权回调,说明运行时会把网络访问决策回传到 UI/SDK。
2. 总体架构
可以把仓库内的 sandbox 机制拆成 7 层:
- 配置层
src/entrypoints/sandboxTypes.ts定义设置结构。 - 启用判定层
src/utils/sandbox/sandbox-adapter.ts判断平台、依赖、策略是否允许启用。 - Bash 入沙盒判定层
src/tools/BashTool/shouldUseSandbox.ts决定单条命令要不要走 sandbox。 - 权限交互层
src/tools/BashTool/bashPermissions.ts决定 auto-allow / ask / deny / override 行为。 - 运行时包装层
src/utils/Shell.ts在spawn前把命令包进 sandbox 运行字符串。 - 网络授权层
src/screens/REPL.tsx、src/cli/structuredIO.ts、src/utils/swarm/permissionSync.ts处理网络访问授权。 - UI/命令入口层
/sandbox、/sandbox exclude、/add-dir、bridge--sandbox暴露给用户或远程会话。
flowchart TD
A[settings / flags / commands] --> B[SandboxManager.isSandboxingEnabled]
B --> C[shouldUseSandbox(input)]
C --> D[exec in src/utils/Shell.ts]
D --> E[SandboxManager.wrapWithSandbox]
E --> F[@anthropic-ai/sandbox-runtime]
F --> G[violation / network ask callback]
G --> H[REPL local dialog]
G --> I[SDK structuredIO can_use_tool]
G --> J[swarm mailbox request/response]
3. 配置层:SandboxSettingsSchema
源码位置:src/entrypoints/sandboxTypes.ts
这里是仓库内 sandbox 配置的单一来源。主要字段如下。
3.1 顶层开关
enabled是否开启 sandbox。failIfUnavailable当用户显式启用 sandbox 但平台或依赖不满足时,是否直接拒绝启动。autoAllowBashIfSandboxed开启后,Bash 命令在“会进入 sandbox”的前提下可走自动放行逻辑。allowUnsandboxedCommands是否允许通过dangerouslyDisableSandbox退回到非 sandbox 执行。excludedCommands用户指定哪些命令模式不要进 sandbox。
3.2 网络配置:sandbox.network
allowedDomains允许访问的域名列表。allowManagedDomainsOnly如果为真,只接受 managed/policy settings 里的允许域名和 WebFetch 域规则。allowUnixSockets允许的 Unix socket 路径。allowAllUnixSockets允许所有 Unix socket。allowLocalBinding是否允许本地绑定。httpProxyPortHTTP 代理端口。socksProxyPortSOCKS 代理端口。
3.3 文件系统配置:sandbox.filesystem
allowWrite额外允许写入的路径。denyWrite额外拒绝写入的路径。denyRead额外拒绝读取的路径。allowRead在denyRead区域内重新放行的路径。allowManagedReadPathsOnly只接受 policy settings 提供的读路径白名单。
3.4 其他控制项
ignoreViolations忽略特定 violation。enableWeakerNestedSandbox允许更弱的嵌套 sandbox。enableWeakerNetworkIsolation放宽部分网络隔离,注释里明确写了“降低安全性”。ripgrep为 sandbox 内的 ripgrep 提供 command/args。
4. 实现主链:sandbox-adapter.ts
源码位置:src/utils/sandbox/sandbox-adapter.ts
这个文件是核心适配层。它没有自己做 OS 级隔离,但它决定“外部 runtime 会拿到什么配置、何时初始化、何时刷新、如何做仓库特有的安全补丁”。
4.1 适配器职责
SandboxManager 对外暴露了一组仓库级接口:
initializeisSandboxingEnabledgetSandboxUnavailableReasonwrapWithSandboxrefreshConfigcleanupAfterCommandcheckDependenciessetSandboxSettingsgetExcludedCommandsgetFsReadConfiggetFsWriteConfiggetNetworkRestrictionConfig
其中很多方法是“仓库自实现 + 底层 runtime 转发”的混合体。
4.2 配置转换:convertToSandboxRuntimeConfig()
这个函数把 Claude Code 自己的 settings/permission 体系转换为 sandbox runtime 可消费的配置。
转换内容包括:
- 从
permissions.allow和permissions.deny中提取WebFetch(domain:...)规则,合并成allowedDomains/deniedDomains。 - 当
allowManagedDomainsOnly开启时,只读policySettings里的域名允许列表。 - 初始化默认可写路径:
.和 Claude 临时目录getClaudeTempDir()。 - 把所有 settings 文件路径加入
denyWrite。 - 把 managed settings drop-in 目录加入
denyWrite。 - 当当前 cwd 与原始 cwd 不同时,补充当前目录下
.claude/settings.json和.claude/settings.local.json的写保护。 - 无条件保护
.claude/skills,避免通过技能目录写入绕过安全边界。 - 为 git bare-repo 逃逸场景做防御:
存在的
HEAD、objects、refs、hooks、config直接加入denyWrite; 不存在的路径记到bareGitRepoScrubPaths,命令后做同步清理。 - 如果检测到 git worktree,允许写主仓库路径,避免 worktree 下 git 锁文件失败。
- 把
permissions.additionalDirectories和运行期--add-dir//add-dir注入的额外目录加入allowWrite。 - 把
Edit(...)/Read(...)权限规则转换为allowWrite/denyWrite/denyRead。 - 把
sandbox.filesystem.*的路径规则转成 runtime 的文件系统限制。 - 为 sandbox 生成 ripgrep 配置。
4.3 路径解析细节
这里刻意区分了两套路径语义:
- 权限规则路径
通过
resolvePathPatternForSandbox()解析,//path表示绝对路径,/path表示相对 settings 根目录。 sandbox.filesystem.*路径 通过resolveSandboxFilesystemPath()解析,/path直接按绝对路径处理,./path或裸路径相对 settings 根目录。
也就是说,Edit(/foo) 和 sandbox.filesystem.allowWrite: ["/foo"] 在仓库里不是同一种语义。
4.4 启用条件
isSandboxingEnabled() 需要同时满足:
- 当前平台受支持。
checkDependencies()没有错误。- 平台在
enabledPlatforms白名单内。 sandbox.enabled === true。
只要任何一项不满足,就不会真正启用 sandbox。
4.5 不可用原因
getSandboxUnavailableReason() 用来回答“用户明明开了 sandbox,为什么没生效”。
它会区分:
- WSL1 不支持。
- 当前平台不支持。
enabledPlatforms排除了当前平台。- 依赖缺失。
print.ts 和 REPL.tsx 会在启动时调用它:
- 如果
failIfUnavailable=true,直接拒绝启动。 - 否则给出 warning,并明确说明“命令将不受 sandbox 保护”。
4.6 初始化与动态刷新
initialize() 做的事:
- 先检查 sandbox 是否启用。
- 只初始化一次,用
initializationPromise防止竞态。 - 解析 git worktree 主仓路径。
- 生成 runtime config。
- 调用
BaseSandboxManager.initialize(runtimeConfig, wrappedCallback)。 - 订阅 settings 变化,发生变化时调用
BaseSandboxManager.updateConfig(newConfig)。
refreshConfig() 是同步刷新入口,供权限或工作目录变化后立即更新 runtime 配置,避免下一条命令命中旧配置。
/add-dir 的实现就会在更新目录后立刻调用它。
4.7 命令后清理
cleanupAfterCommand() 不是简单转发。它会:
- 先调用底层
BaseSandboxManager.cleanupAfterCommand()。 - 再执行仓库自己的
scrubBareGitRepoFiles()。
后者专门清理 sandbox 命令期间可能被种下的 bare git repo 痕迹,避免后续非 sandbox git 调用被利用。
5. Bash 何时进入 sandbox
源码位置:src/tools/BashTool/shouldUseSandbox.ts
判定逻辑非常直接:
SandboxManager.isSandboxingEnabled()为假,直接不进 sandbox。- 如果调用参数里有
dangerouslyDisableSandbox=true,且策略允许非 sandbox 命令,则不进 sandbox。 - 没有命令文本,不进 sandbox。
- 命中
sandbox.excludedCommands,不进 sandbox。 - 否则进入 sandbox。
excludedCommands 只是用户体验层便利功能,不是安全边界。源码注释明确写了:真正的安全控制仍然是 permission system。
5.1 excludedCommands 的匹配方式
containsExcludedCommand() 会做两件事:
- 拆分 compound command,逐个子命令匹配。
- 去掉前导环境变量和安全 wrapper,再匹配 exact/prefix/wildcard 规则。
这意味着下面这些都可能绕开 sandbox:
- 用户显式配置的命令模式。
/sandbox exclude "npm run test:*"加进去的模式。
但仓库作者把它定义为“选择不进 sandbox 的功能”,不是漏洞。
6. 权限交互:auto-allow / ask / deny / override
源码位置:src/tools/BashTool/bashPermissions.ts
sandbox 和 Bash 权限系统是耦合的,不是两条平行链路。
6.1 auto-allow 模式
当前提同时成立时:
SandboxManager.isSandboxingEnabled()SandboxManager.isAutoAllowBashIfSandboxedEnabled()shouldUseSandbox(input)
会进入 checkSandboxAutoAllow(...)。
它的规则是:
- 如果 full command 命中显式 deny,返回
deny。 - 如果子命令命中显式 deny,返回
deny。 - 如果子命令命中 ask,返回
ask。 - 如果 full command 命中 ask,返回
ask。 - 上面都没有命中,则直接
allow,原因写成Auto-allowed with sandbox (autoAllowBashIfSandboxed enabled)。
也就是说,auto-allow 不是“所有命令都默默放行”,而是“没有显式 ask/deny 规则时,允许它在 sandbox 内执行”。
6.2 dangerouslyDisableSandbox
BashTool 的输入 schema 明确暴露了:
dangerouslyDisableSandbox?: boolean
但是它是否生效,取决于 allowUnsandboxedCommands:
- 如果
allowUnsandboxedCommands=true,shouldUseSandbox()会因为 override 返回false。 - 如果
allowUnsandboxedCommands=false,这个参数在策略上被禁用,命令仍必须进入 sandbox。
src/tools/BashTool/prompt.ts 也把这层约束写进了给模型的工具提示:
- 默认所有命令都应该先在 sandbox 中执行。
- 只有用户明确要求绕过,或出现明显的 sandbox 失败证据时,才应使用
dangerouslyDisableSandbox: true。 - 如果策略禁止,则“所有命令必须运行在 sandbox 内”。
6.3 UI 上如何展示
src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx 会计算:
- sandbox 是否启用。
- 该命令是否会进入 sandbox。
如果启用了 sandbox 但当前命令没有进入,会在权限框标题上显示成 unsandboxed bash。
7. 运行时包装:Shell.exec()
源码位置:src/utils/Shell.ts
这里是“是否进入 sandbox”真正落地到进程执行的地方。
7.1 包装时机
BashTool.call() 最终会进入 runShellCommand(),然后调用:
exec(command, ..., { shouldUseSandbox: shouldUseSandbox(input) })
exec() 里如果 shouldUseSandbox 为真,就会:
- 计算 sandbox 专用临时目录。
- 调用
provider.buildExecCommand(...)生成命令字符串。 - 调用
SandboxManager.wrapWithSandbox(commandString, sandboxBinShell, ...)。 - 再用包装后的命令字符串执行
spawn(...)。
7.2 $TMPDIR
Shell.ts 会给 sandbox 命令准备专用临时目录,并把这个目录传进 shell provider。
BashTool/prompt.ts 也明确要求模型在 sandbox 模式下只使用 $TMPDIR,不要直接写 /tmp。
7.3 PowerShell 特殊处理
源码专门处理了 sandbox 下的 PowerShell:
- 不是直接把
pwsh塞进 sandbox。 - 会先由 provider 构造
pwsh -NoProfile -NonInteractive -EncodedCommand ...。 - sandbox 的内层 shell 改成
/bin/sh。
这样做是为了避免 PowerShell profile 在 sandbox 中加载,导致延迟、杂音输出或卡住。
7.4 命令结束后的清理
shellCommand.result.then(...) 里有一个关键分支:
- 如果本次命令用了 sandbox,先执行
SandboxManager.cleanupAfterCommand()。
源码注释写得很清楚:Linux 下 bwrap 可能会在宿主机留下 0 字节挂载点文件,所以必须在命令结束后同步清理。
8. 网络授权链
sandbox 不只限制文件系统,也会限制网络访问。仓库内的设计是“底层 runtime 发现某个 host 不在允许范围内时,向上层要一次决策”。
8.1 REPL 本地交互
源码位置:src/screens/REPL.tsx
REPL 初始化 sandbox 时会传入 sandboxAskCallback。
普通本地场景下:
- 把请求加入
sandboxPermissionRequestQueue。 - 本地 UI 弹出授权对话框。
- 用户允许或拒绝后,resolve 对应 promise。
如果启用了 bridge,还会额外把请求发到 remote control 侧,走 can_use_tool 风格的控制请求。
8.2 SDK / print 模式
源码位置:src/cli/structuredIO.ts、src/cli/print.ts
structuredIO.createSandboxAskCallback() 会把网络授权请求转成一个 synthetic tool:
- tool name:
SANDBOX_NETWORK_ACCESS_TOOL_NAME - protocol subtype:
can_use_tool - description:
Allow network connection to <host>?
也就是说,SDK host 不需要额外实现一套 sandbox 协议,而是复用既有权限请求协议。
8.3 swarm worker 转发
源码位置:
src/utils/swarm/permissionSync.tssrc/hooks/useSwarmPermissionPoller.tssrc/utils/teammateMailbox.ts
当当前 agent 是 swarm worker 时:
- worker 生成
requestId。 - 通过 mailbox 发送
sandbox_permission_request给 leader。 - leader 审批后,再通过 mailbox 发回
sandbox_permission_response。 - worker 本地 registry 根据
requestId找到 callback,resolve 这次网络访问是否允许。
消息结构在 teammateMailbox.ts 中有明确 schema:
sandbox_permission_request包含requestId、workerId、workerName、hostPattern.host。sandbox_permission_response包含requestId、host、allow。
8.4 managed-only 域名策略
sandbox-adapter.ts 在 initialize() 里会包一层 wrappedCallback:
- 如果
shouldAllowManagedSandboxDomainsOnly()为真,直接拒绝所有运行期 ask,不再把问题转给用户。
也就是说,这个策略不是“UI 提示用户只可选 managed 域名”,而是直接在回调入口处短路拒绝。
9. 用户侧触发方式
这里把仓库内明确存在的触发入口单列出来。
9.1 settings
直接来自 settings.json / settings.local.json / policy / flags 的字段:
sandbox.enabledsandbox.failIfUnavailablesandbox.autoAllowBashIfSandboxedsandbox.allowUnsandboxedCommandssandbox.excludedCommandssandbox.network.*sandbox.filesystem.*
这是最基础的触发入口。
9.2 /sandbox
源码位置:src/commands/sandbox-toggle/*
/sandbox 提供交互式设置界面,可以切换:
disabledregularauto-allow
还可以配置 overrides:
Allow unsandboxed fallbackStrict sandbox mode
9.3 /sandbox exclude "pattern"
会调用 addToExcludedCommands(...),把模式写入本地 settings 的 sandbox.excludedCommands。
后续 shouldUseSandbox() 再遇到匹配命令时,就不会进入 sandbox。
9.4 /add-dir
源码位置:src/commands/add-dir/add-dir.tsx
该命令会:
- 把目录加入工具工作目录权限。
- 更新 bootstrap state 中的额外目录。
- 立即调用
SandboxManager.refreshConfig()。
所以 /add-dir 不只是工具层放行,也会同步扩展 Bash sandbox 的可写目录。
9.5 dangerouslyDisableSandbox
这是单次 Bash 调用参数级触发方式,不是全局设置。
前提是:
- 该参数被传入。
allowUnsandboxedCommands=true。
满足时,这一条命令不进 sandbox。
9.6 bridge --sandbox 与 CLAUDE_CODE_FORCE_SANDBOX
这条链属于远程会话层,不是本地 BashTool 自己的判定逻辑。
src/bridge/bridgeMain.ts解析--sandbox/--no-sandbox。src/bridge/sessionRunner.ts在启用时给子进程注入CLAUDE_CODE_FORCE_SANDBOX=1。
这表示“远程 session 启动时,带上强制 sandbox 的环境参数”,而不是给 BashTool 新加一套独立权限系统。
10. 使用触发矩阵
| 场景 | 是否进 sandbox | 仓库内依据 |
|---|---|---|
| 普通 Bash,sandbox 启用,未命中排除 | 是 | shouldUseSandbox() 返回 true |
Bash 带 dangerouslyDisableSandbox=true,且允许 fallback | 否 | allowUnsandboxedCommands=true 时 override 生效 |
Bash 带 dangerouslyDisableSandbox=true,但 strict mode | 是 | override 被策略禁用 |
命中 excludedCommands | 否 | containsExcludedCommand() |
| auto-allow 开启,且命令会进入 sandbox | 通常直接 allow 后在 sandbox 内执行 | checkSandboxAutoAllow() |
| auto-allow 开启,但命中 ask/deny 规则 | ask 或 deny | 同上 |
| 网络访问非白名单 host | 触发 ask callback | REPL / SDK / swarm |
allowManagedDomainsOnly=true | 直接拒绝 ask callback | wrappedCallback 短路 |
| worker 模式下网络访问受限 host | 请求转发给 leader | mailbox sandbox_permission_request/response |
bridge --sandbox | 远程子 session 带 sandbox 环境 | CLAUDE_CODE_FORCE_SANDBOX |
11. 远程 bridge 的 sandbox
这部分容易和本地 Bash sandbox 混淆,单独说明。
源码位置:
src/bridge/bridgeMain.tssrc/bridge/types.tssrc/bridge/sessionRunner.tssrc/bridge/bridgeUI.ts
仓库内可以确认的行为是:
- remote control 启动参数里有
sandbox: boolean。 - CLI 支持
--sandbox。 - bridge UI 会显示
Sandbox: Enabled。 - session runner 会把该值转成子进程环境变量
CLAUDE_CODE_FORCE_SANDBOX=1。
仓库内不能确认的是:
- 这个环境变量在子进程更深层到底如何影响底层 runtime。
因此这部分在本文中只归纳为“远程会话启动参数”,不把它误写成 BashTool 的另一套 sandbox 判定链。
12. 仓库边界:哪些能确认,哪些不能
当前仓库源码能确认:
- sandbox 的配置结构。
- sandbox 是否启用的判定条件。
- BashTool 何时进入 sandbox。
- 命令执行前如何包裹到 sandbox runtime。
- 网络访问受限时如何把授权请求回传到 REPL / SDK / swarm。
- sandbox 命令结束后有哪些仓库自定义清理逻辑。
当前仓库源码不能确认:
@anthropic-ai/sandbox-runtime内部怎样调用 bwrap、seccomp、代理、socket 拦截或 macOS 沙箱机制。- 底层 runtime 如何具体实现 violation 检测和 host 级网络阻断。
CLAUDE_CODE_FORCE_SANDBOX在子进程更深处的最终解释逻辑。
所以如果问题是“仓库里有没有 sandbox,以及它是如何接入和触发的”,当前文档已经完整回答。
如果问题升级成“底层 OS 级隔离到底怎么实现”,就必须继续分析外部依赖包,而这已经超出本仓库源码范围。
13. 总结
仓库内的 sandbox 机制可以概括成一句话:
Claude Code 在仓库内部并不自己实现 OS 级沙盒,而是围绕
@anthropic-ai/sandbox-runtime建了一层完整的接入与治理框架,负责配置转换、启用判定、Bash 入沙盒决策、运行时包装、网络授权回调、swarm 转发,以及命令后的安全清理。
从调用链上看,最关键的主路径是:
settings -> SandboxManager.isSandboxingEnabled -> shouldUseSandbox -> Shell.exec -> wrapWithSandbox -> sandbox runtime -> violation/ask callback -> UI/SDK/swarm response
从使用方式上看,最关键的触发入口是:
- settings 中的
sandbox.* /sandbox/sandbox exclude .../add-dir- Bash 参数
dangerouslyDisableSandbox - remote bridge 的
--sandbox
这说明 sandbox 在本仓库里不是孤立组件,而是权限系统、Shell 系统、会话系统、远程控制系统共同参与的一条主干能力链。