Claude Code 的沙箱系统是一个企业级安全隔离解决方案,用于在受控环境中执行潜在不可信的命令。该系统基于 `@anthropic-ai/sandbox-runtime` 包构建,提供了文件系统隔离、网络访问控制、权限管理等多层安全防护机制,代码中大量的注释都指向具体的 GitHub Issue(如 #30067、#34044、#29316),说明每个复杂逻辑背后都有真实的问题驱动。

核心包 sandbox-runtime 

  • GitHub: https://github.com/anthropic-experimental/sandbox-runtime
  • npm 包名@anthropic-ai/sandbox-runtime
  • 可以作为独立工具使用(srt 命令)
  • 也可以作为库集成到其他项目中(Claude Code 中集成)
  • 用途: 为AI代理(agents)、MCP服务器、bash命令等提供轻量级沙箱隔离

一个Anthropic开发的沙箱运行时系统(Sandbox Runtime),专门为Claude Code设计,用于在操作系统级别对任意进程实施安全限制,这个沙箱系统通过操作系统级别的强制访问控制多层防御机制精细的权限管理,为AI代理提供了安全可靠的最小权限执行环境。

核心架构

双重隔离模型

┌─────────────────────────────────
│           沙箱运行时 (Host)              
│  ┌──────────┐    ┌───────────────── 
│  │ HTTP代理  │    │   SOCKS5代理      
│  │ (过滤HTTP) │    │  (过滤TCP/SSH等) 
│  └────┬─────┘    └────────┬────────
│                         │            
│  ┌────▼───────────────────▼─────
│  │     网络过滤层 (Domain Allowlist)  
│  └───────────────────────────────
└─────────────────────────────────
           │  Unix Socket / TCP
┌──────────▼─────────────────────
│          沙箱进程 (Sandboxed)       
│  ┌─────────────────────────────── 
│  │  文件系统隔离 (OS级强制访问控制)  
│  └────────────────────────────────
│  ┌────────────────────────────────
│  │  网络命名空间隔离 (Linux)        
│  │  或端口限制 (macOS)              
│  └────────────────────────────────
└──────────────────────────────────

平台特定实现

平台 文件系统隔离 网络隔离
macOS sandbox-exec + Seatbelt配置文件 限制只能连接到localhost代理端口
Linux bubblewrap (bwrap) + 绑定挂载 --unshare-net 隔离网络命名空间 + socat桥接

🔐 安全机制详解

1. 文件系统隔离

1. sandbox-schemas.ts - 双层权限模型

读取权限 (Deny-then-Allow模式):

  • 默认允许所有读取
  • 可拒绝大范围区域(如/Users),再允许特定路径(如.
  • allowRead 优先级高于 denyRead

写入权限 (Allow-Only模式):

  • 默认拒绝所有写入
  • 必须显式允许路径(如./tmp
  • denyWrite 优先级高于 allowWrite

权限类型 模式 默认行为 配置接口
读取 Deny-then-Allow 开放(空deny = 允许所有) denyOnly[] + allowWithinDeny[]
写入 Allow-then-Deny 封闭(空allow = 拒绝所有) allowOnly[] + denyWithinAllow[]
网络 Allow-only 封闭(空allow = 拒绝所有) allowedHosts[] + deniedHosts[]

关键区别:读取和写入采用相反的默认策略,这是因为:

  • 读取:用户通常希望访问大多数文件,只需保护敏感区域

  • 写入:危险操作,默认应拒绝,仅开放必要路径

强制保护路径 (Mandatory Deny Paths):

// 始终阻止写入的关键文件
.bashrc, .bash_profile, .zshrc, .gitconfig
.mcp.json, .ripgreprc

// 始终阻止写入的目录
.vscode/, .idea/, .claude/commands/, .git/hooks/

2. 网络隔离

源码位置http-proxy.ts 和 socks-proxy.ts

// ============ HTTP代理 (拦截HTTP/HTTPS流量) ============
// 文件: src/sandbox/http-proxy.ts

export function createHttpProxyServer(options: HttpProxyServerOptions): Server {
  const server = createServer()

  // 处理HTTPS的CONNECT请求 (隧道模式)
  server.on('connect', async (req, socket, head) => {
    const target = parseConnectTarget(req.url!)
    const { hostname, port } = target

    // 🔒 关键: 调用过滤器验证域名是否在白名单中
    const allowed = await options.filter(port, hostname, socket)
    if (!allowed) {
      // 拒绝访问,返回403
      socket.end('HTTP/1.1 403 Forbidden\r\n...Connection blocked by network allowlist')
      return
    }

    // 决定上游路由: MITM代理 > 父代理 > 直连
    const mitmSocketPath = options.getMitmSocketPath?.(hostname)
    let upstream: Socket
    if (mitmSocketPath) {
      // 路由到MITM代理进行检查
      upstream = await openConnectTunnel({
        dial: () => connect({ path: mitmSocketPath }),
        destHost: hostname,
        destPort: port,
      })
    } else {
      upstream = await dialDirect(hostname, port)
    }

    // 建立隧道,双向pipe数据流
    socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
    upstream.pipe(socket)
    socket.pipe(upstream)
  })

  // 处理普通HTTP请求
  server.on('request', async (req, res) => {
    const url = new URL(req.url!)
    const hostname = stripBrackets(url.hostname)
    
    // 🔒 同样需要过滤器验证
    const allowed = await options.filter(port, hostname, req.socket)
    if (!allowed) {
      res.writeHead(403)
      res.end('Connection blocked by network allowlist')
      return
    }
    // ...转发请求
  })
}

// ============ SOCKS5代理 (处理其他TCP连接) ============
// 文件: src/sandbox/socks-proxy.ts

export function createSocksProxyServer(options: SocksProxyServerOptions): SocksProxyWrapper {
  const socksServer = createServer()

  // 🔒 规则验证器 - 在建立连接前检查域名
  socksServer.setRulesetValidator(async conn => {
    const hostname = conn.destAddress
    const port = conn.destPort

    // 安全检查: 拒绝包含控制字符的主机名
    if (!isValidHost(hostname)) {
      logForDebugging(`Rejecting malformed SOCKS host: ${JSON.stringify(hostname)}`)
      return false
    }

    // 调用过滤器验证域名白名单
    const allowed = await options.filter(port, hostname)
    return allowed
  })

  // 连接处理器 - 建立TCP隧道
  socksServer.setConnectionHandler((conn, sendStatus) => {
    const host = conn.destAddress
    const port = conn.destPort

    // 直连或通过父代理连接
    const open = options.parentProxy
      ? connectViaParentProxy(parentUrl, host, port)
      : dialDirect(host, port)

    open.then(upstream => {
      sendStatus('REQUEST_GRANTED')  // SOCKS5成功响应
      upstream.pipe(conn.socket)     // 双向转发
      conn.socket.pipe(upstream)
    })
  })
}

代理架构:

  • HTTP代理: 拦截HTTP/HTTPS流量,验证域名白名单
  • SOCKS5代理: 处理其他TCP连接(SSH、数据库等)
  • MITM代理支持: 可路由特定域名到自定义MITM代理进行检查

域名过滤逻辑:

源码位置sandbox-manager.ts

// 文件: src/sandbox/sandbox-manager.ts

/**
 * 域名过滤核心逻辑
 * 检查顺序: 拒绝列表 → 允许列表 → 用户回调 → 默认拒绝
 */
async function filterNetworkRequest(
  port: number,
  host: string,
  sandboxAskCallback?: SandboxAskCallback,
): Promise<boolean> {
  if (!config) {
    return false  // 无配置,默认拒绝
  }

  // 🛡️ 安全措施1: 拒绝包含控制字符的主机名
  // 防止 evil.com\x00.allowed.com 这样的攻击
  if (!isValidHost(host)) {
    logForDebugging(`Denying malformed host: ${JSON.stringify(host)}:${port}`)
    return false
  }

  // 🛡️ 安全措施2: 主机名规范化
  // 防止 2852039166 (=169.254.169.254) 绕过IP拒绝列表
  const canonicalHost = canonicalizeHost(host) ?? host

  // ✅ 步骤1: 检查拒绝列表 (优先级最高)
  for (const deniedDomain of config.network.deniedDomains) {
    if (matchesDomainPattern(canonicalHost, deniedDomain)) {
      logForDebugging(`Denied by config rule: ${host}:${port}`)
      return false  // 匹配拒绝规则,立即拒绝
    }
  }

  // ✅ 步骤2: 检查允许列表
  for (const allowedDomain of config.network.allowedDomains) {
    if (matchesDomainPattern(canonicalHost, allowedDomain)) {
      logForDebugging(`Allowed by config rule: ${host}:${port}`)
      return true  // 匹配允许规则,放行
    }
  }

  // ✅ 步骤3: 调用用户授权回调 (动态决策)
  if (!sandboxAskCallback) {
    logForDebugging(`No matching config rule, denying: ${host}:${port}`)
    return false  // 无回调,默认拒绝
  }

  const userAllowed = await sandboxAskCallback({ host, port })
  if (userAllowed) {
    logForDebugging(`User allowed: ${host}:${port}`)
    return true  // 用户允许,放行
  } else {
    logForDebugging(`User denied: ${host}:${port}`)
    return false  // 用户拒绝
  }
}

/**
 * 域名模式匹配 - 支持通配符
 */
function matchesDomainPattern(hostname: string, pattern: string): boolean {
  const h = hostname.toLowerCase()
  
  // 支持 *.example.com 通配符模式
  if (pattern.startsWith('*.')) {
    // 🛡️ 安全措施: 不对IP地址应用通配符匹配
    // 防止 ::ffff:1.2.3.4%x.allowed.com 绕过检查
    if (isIP(stripBrackets(h))) return false
    
    const baseDomain = pattern.substring(2).toLowerCase()
    return h.endsWith('.' + baseDomain)  // 后缀匹配
  }

  // 精确匹配
  return h === pattern.toLowerCase()
}
// 检查顺序:拒绝列表 → 允许列表 → 用户回调
1. 检查 deniedDomains (优先拒绝)
2. 检查 allowedDomains (允许访问)
3. 调用 sandboxAskCallback (请求用户授权)
4. 默认拒绝

安全措施:

源码位置parent-proxy.ts

// 文件: src/sandbox/parent-proxy.ts

/**
 * 🛡️ 安全措施1: 主机名验证
 * 目的: 阻止控制字符 (CRLF注入、空字节DNS截断)
 */
export function isValidHost(h: string): boolean {
  if (!h || h.length > 255) return false
  const bare = stripBrackets(h)
  
  // 拒绝IPv6 zone标识符 (包含%)
  // 防止 ::ffff:1.2.3.4%x.allowed.com 绕过白名单
  if (bare.includes('%')) return false
  
  // 如果是IP地址,直接通过
  if (isIP(bare)) return true
  
  // DNS标签字符集验证 (只允许字母、数字、点、下划线、连字符)
  return /^[A-Za-z0-9._-]+$/.test(bare)
}

/**
 * 🛡️ 安全措施2: 主机名规范化
 * 目的: 统一格式,防止绕过检查
 */
export function canonicalizeHost(h: string): string | undefined {
  try {
    const bare = stripBrackets(h)
    // WHATWG URL解析器自动规范化:
    // - inet_aton简写: 127.1 → 127.0.0.1
    // - 十进制简写: 2130706433 → 127.0.0.1
    // - 十六进制/八进制: 0x7f.0.0.1 → 127.0.0.1
    // - IPv6压缩: 0:0:0:0:0:0:0:1 → ::1
    const bracketed = isIP(bare) === 6 ? `[${bare}]` : bare
    const out = new URL(`http://${bracketed}/`).hostname
    return stripBrackets(out).replace(/\.$/, '')
  } catch {
    return undefined
  }
}
  • 主机名规范化(防止2852039166绕过IP拒绝)
  • 控制字符检测(防止evil.com\x00.allowed.com攻击)
  • 通配符支持(*.example.com),但禁止过宽模式(*.com

源码位置sandbox-config.ts

// 文件: src/sandbox/sandbox-config.ts

/**
 * 🛡️ 安全措施3: 域名模式验证 (Zod Schema)
 * 目的: 防止配置过于宽通的通配符
 */
const domainPatternSchema = z.string().refine(
  val => {
    // 拒绝协议、路径、端口
    if (val.includes('://') || val.includes('/') || val.includes(':')) {
      return false
    }

    // 允许localhost
    if (val === 'localhost') return true

    // 允许 *.example.com 形式的通配符
    if (val.startsWith('*.')) {
      const domain = val.slice(2)
      // 🛡️ 必须有至少一个点,防止 *.com 这样过于宽泛的模式
      if (!domain.includes('.') || domain.startsWith('.') || domain.endsWith('.')) {
        return false
      }
      // 必须有至少两部分 (如 example.com)
      const parts = domain.split('.')
      return parts.length >= 2 && parts.every(p => p.length > 0)
    }

    // 拒绝其他任何通配符使用
    if (val.includes('*')) {
      return false
    }

    // 普通域名必须有至少一个点
    return val.includes('.') && !val.startsWith('.') && !val.endsWith('.')
  },
  {
    message: 'Invalid domain pattern. Overly broad patterns like "*.com" are not allowed'
  }
)

3. Unix Socket限制(Linux)

双层架构设计

源码位置linux-sandbox-utils.ts

// 文件: src/sandbox/linux-sandbox-utils.ts

/**
 * 在Linux上包装命令,应用沙箱限制
 * 
 * 架构设计 (双层隔离):
 * 
 * Stage 1: 外层bwrap - 网络和文件系统隔离 (不应用seccomp)
 *   ├─ bubblewrap启动,隔离网络命名空间 (--unshare-net)
 *   ├─ 应用PID命名空间隔离 (--unshare-pid和--proc)
 *   ├─ 应用文件系统限制 (只读挂载、绑定挂载等)
 *   └─ socat进程启动并连接Unix socket桥接 (可以调用socket(AF_UNIX, ...))
 *
 * Stage 2: apply-seccomp - 嵌套PID命名空间 + seccomp过滤器
 *   ├─ apply-seccomp创建嵌套的user+PID+mount命名空间
 *   ├─ 在嵌套命名空间内,apply-seccomp成为PID 1 (不可dump的init/reaper)
 *   ├─ 分叉后,设置PR_SET_NO_NEW_PRIVS,通过prctl(PR_SET_SECCOMP)应用seccomp
 *   ├─ 执行用户命令,seccomp激活 (无法创建新的Unix socket)
 *   └─ 用户命令无法看到或ptrace bwrap/bash/socat (独立的PID命名空间)
 */
export async function wrapCommandWithSandboxLinux(
  params: LinuxSandboxParams,
): Promise<string> {
  const {
    command,
    needsNetworkRestriction,
    httpSocketPath,
    socksSocketPath,
    httpProxyPort,
    socksProxyPort,
    readConfig,
    writeConfig,
    allowAllUnixSockets,
    seccompConfig,
  } = params

  const bwrapArgs: string[] = ['--new-session', '--die-with-parent']
  let applySeccompPrefix: string | undefined

  try {
    // ========== SECCOMP过滤器 (阻止Unix Socket创建) ==========
    // apply-seccomp包装工作负载并应用内置的BPF过滤器
    // 该过滤器阻止 socket(AF_UNIX, ...) 系统调用
    if (!allowAllUnixSockets) {
      // 解析apply-seccomp二进制路径
      applySeccompPrefix = resolveApplySeccompPrefix(
        seccompConfig?.applyPath,
        seccompConfig?.argv0,
      )

      if (!applySeccompPrefix) {
        logForDebugging(
          '[Sandbox Linux] apply-seccomp binary not available - unix socket blocking disabled',
          { level: 'warn' }
        )
      } else {
        logForDebugging('[Sandbox Linux] Applying seccomp filter for Unix socket blocking')
      }
    }

    // ========== 网络限制 ==========
    if (needsNetworkRestriction) {
      // 总是unshare网络命名空间以隔离网络访问
      // 这会移除所有网络接口,有效阻止所有网络
      bwrapArgs.push('--unshare-net')

      // 如果提供了代理socket,将它们绑定到沙箱内
      if (httpSocketPath && socksSocketPath) {
        // 验证socket文件仍然存在
        if (!fs.existsSync(httpSocketPath)) {
          throw new Error(`Linux HTTP bridge socket does not exist: ${httpSocketPath}`)
        }
        if (!fs.existsSync(socksSocketPath)) {
          throw new Error(`Linux SOCKS bridge socket does not exist: ${socksSocketPath}`)
        }

        // 将两个socket绑定到沙箱内
        bwrapArgs.push('--bind', httpSocketPath, httpSocketPath)
        bwrapArgs.push('--bind', socksSocketPath, socksSocketPath)

        // 添加代理环境变量
        const proxyEnv = generateProxyEnvVars(3128, 1080)
        bwrapArgs.push(
          ...proxyEnv.flatMap((env: string) => {
            const firstEq = env.indexOf('=')
            const key = env.slice(0, firstEq)
            const value = env.slice(firstEq + 1)
            return ['--setenv', key, value]
          })
        )
      }
    }

    // ========== 文件系统限制 ==========
    const fsArgs = await generateFilesystemArgs(readConfig, writeConfig, ...)
    bwrapArgs.push(...fsArgs)

    // 总是绑定/dev
    bwrapArgs.push('--dev', '/dev')

    // ========== PID命名空间隔离 ==========
    // 重要: 这些必须在文件系统绑定之后,以便嵌套bwrap工作
    bwrapArgs.push('--unshare-pid')
    if (!enableWeakerNestedSandbox) {
      // 挂载新的/proc (安全模式)
      bwrapArgs.push('--proc', '/proc')
    } else {
      // 在Docker环境中的弱沙箱模式
      bwrapArgs.push('--unshare-user', '--bind', '/proc', '/proc')
    }

    // ========== 命令构建 ==========
    const shellName = binShell || 'bash'
    const shell = whichSync(shellName)
    if (!shell) {
      throw new Error(`Shell '${shellName}' not found in PATH`)
    }
    bwrapArgs.push('--', shell, '-c')

    // 如果有网络限制,通过buildSandboxCommand路由命令
    // 这样socat在应用seccomp之前启动
    if (needsNetworkRestriction && httpSocketPath && socksSocketPath) {
      const sandboxCommand = buildSandboxCommand(
        httpSocketPath,
        socksSocketPath,
        command,
        applySeccompPrefix,  // 👈 seccomp前缀传递到这里
        shell,
      )
      bwrapArgs.push(sandboxCommand)
    } else if (applySeccompPrefix) {
      const applySeccompCmd =
        applySeccompPrefix + shellquote.quote([shell, '-c', command])
      bwrapArgs.push(applySeccompCmd)
    } else {
      bwrapArgs.push(command)
    }

    const wrappedCommand = shellquote.quote(['bwrap', ...bwrapArgs])
    return wrappedCommand

  } catch (error) {
    if (activeSandboxCount > 0) {
      activeSandboxCount--
    }
    throw error
  }
}

/**
 * 构建沙箱内执行的命令
 * 在端口3128设置HTTP代理,在端口1080设置SOCKS代理
 */
function buildSandboxCommand(
  httpSocketPath: string,
  socksSocketPath: string,
  userCommand: string,
  applySeccompPrefix: string | undefined,  // 👈 seccomp前缀
  shell?: string,
): string {
  const shellPath = shell || 'bash'
  
  // socat命令: 在沙箱内启动监听器,转发到Unix socket
  const socatCommands = [
    `socat TCP-LISTEN:3128,fork,reuseaddr UNIX-CONNECT:${httpSocketPath} >/dev/null 2>&1 &`,
    `socat TCP-LISTEN:1080,fork,reuseaddr UNIX-CONNECT:${socksSocketPath} >/dev/null 2>&1 &`,
    'trap "kill %1 %2 2>/dev/null; exit" EXIT',
  ]

  // 👇 关键: apply-seccomp在socat之后运行
  // 这样socat仍然可以创建Unix socket
  if (applySeccompPrefix) {
    const applySeccompCmd =
      applySeccompPrefix + shellquote.quote([shellPath, '-c', userCommand])
    
    // 脚本顺序:
    // 1. 启动socat监听器 (可以创建Unix socket)
    // 2. 通过apply-seccomp执行用户命令 (seccomp激活,阻止创建新socket)
    const innerScript = [...socatCommands, applySeccompCmd].join('\n')
    return `${shellPath} -c ${shellquote.quote([innerScript])}`
  } else {
    // 没有seccomp,直接执行用户命令
    const innerScript = [
      ...socatCommands,
      `eval ${shellquote.quote([userCommand])}`,
    ].join('\n')
    return `${shellPath} -c ${shellquote.quote([innerScript])}`
  }
}

/**
 * 解析apply-seccomp前缀
 * 返回可以直接用于shell的字符串
 */
function resolveApplySeccompPrefix(
  applyPath: string | undefined,
  argv0: string | undefined,
): string | undefined {
  if (argv0) {
    // argv0模式: apply-seccomp作为multicall二进制文件
    // 通过ARGV0环境变量分发
    if (!applyPath) {
      throw new Error('seccompConfig.argv0 requires seccompConfig.applyPath')
    }
    return `ARGV0=${shellquote.quote([argv0])} ${shellquote.quote([applyPath])} `
  }
  
  // 标准模式: 直接使用apply-seccomp二进制文件
  const binary = getApplySeccompBinaryPath(applyPath)
  return binary ? `${shellquote.quote([binary])} ` : undefined
}

使用seccomp BPF过滤器在系统调用级别阻止socket(AF_UNIX, ...):

外层bwrap (网络+文件系统隔离)
  └─ socat桥接进程 (允许Unix socket)
      └─ apply-seccomp (嵌套PID命名空间)
          └─ 用户命令 (seccomp激活,阻止Unix socket创建)
apply-seccomp 内层(系统调用过滤)

核心问题:网络代理需要创建 Unix socket 进行通信,但安全要求阻止用户代码创建任意 Unix socket。

解决方案(来自代码注释):

"apply-seccomp creates a nested user+PID+mount namespace... becomes PID 1 (non-dumpable init/reaper)... applies seccomp via prctl(PR_SET_SECCOMP)... Execs user command with seccomp active (cannot create new Unix sockets)"

 * 核心安全问题与解决方案:
 * 
 * 问题: 网络代理需要创建Unix socket进行通信,
 *       但安全要求阻止用户代码创建任意Unix socket。
 * 
 * 传统方案的困境:
 * - 如果在bwrap层应用seccomp → socat无法创建socket桥接 → 网络代理失效
 * - 如果不应用seccomp → 用户代码可以创建任意Unix socket → 安全风险
 * 
 * 创新解决方案: 嵌套命名空间 + 延迟seccomp应用
 * 
 * 1. Stage 1 (bwrap): 
 *    - 创建隔离环境 (网络、PID、文件系统)
 *    - 启动socat桥接进程 (无seccomp限制)
 *    - socat成功创建Unix socket并连接到主机代理
 * 
 * 2. Stage 2 (apply-seccomp):
 *    - 创建嵌套的user+PID+mount命名空间
 *    - 在嵌套命名空间中应用seccomp BPF过滤器
 *    - 执行用户命令 (受到seccomp限制)
 * 
 * 精妙之处:
 * ✓ 用户代码无法创建新的Unix socket (seccomp阻止)
 * ✓ 用户代码可以继承使用Stage 1已创建的socket FD
 * ✓ 用户代码无法看到或ptrace bwrap/socat进程 (PID隔离)
 * ✓ socat可以正常通信 (在seccomp之前启动)
 * ✓ 攻击面最小化 (用户进程完全隔离)

┌────────────────────────────
│  Host Linux                             
│  ┌──────────────────────────
│  │  bwrap (Stage 1)               
│  │  - 网络命名空间已隔离          
│  │  - socat 启动(可创建 socket)   
│  │  ┌──────────────────────── 
│  │  │ apply-seccomp (Stage 2) 
│  │  │ - 嵌套 PID 命名空间      
│  │  │ - seccomp-BPF 过滤      
│  │  │ - 用户代码运行于此        
│  │  │ - 无法创建新 Unix socket 
│  │  │ - 但继承的 socket FD 可用 
│  │  └────────────────────────  
│  └──────────────────────────
└────────────────────────────

这种设计的精妙之处在于:用户代码无法创建新的 Unix socket,但可以使用 Stage 1 已创建的 socket 与代理通信

┌──────────────────────────────────────────────────
│  Host Linux                                     
│                                                    
│  ┌──────────────────────────────────────────────
│  │  Stage 1: bwrap (外层沙箱)                  
│  │                                          
│  │  参数:                                      
│  │  --unshare-net       # 隔离网络命名空间       
│  │  --unshare-pid       # 隔离PID命名空间        
│  │  --proc /proc        # 挂载新的/proc         
│  │  --bind socket socket # 绑定Unix socket      
│  │  --ro-bind / /       # 只读根文件系统        
│  │                                              
│  │  ┌────────────────────────────────────────
│  │  │  Shell脚本执行:                      
│  │  │                                      
│  │  │  1. socat TCP-LISTEN:3128 ...          
│  │  │     UNIX-CONNECT:/tmp/http.sock &     
│  │  │     ↑ 可以创建Unix socket (无seccomp)    
│  │  │                                     
│  │  │  2. socat TCP-LISTEN:1080 ...          
│  │  │     UNIX-CONNECT:/tmp/socks.sock &      
│  │  │     ↑ 可以创建Unix socket (无seccomp)   
│  │  │                                       
│  │  │  3. apply-seccomp bash -c '用户命令'    
│  │  │     ┌───────────────────────────────
│  │  │     │ Stage 2: apply-seccomp      
│  │  │     │                            
│  │  │     │ - 创建嵌套user+PID+mount ns   
│  │  │     │ - 成为PID 1 (不可dump)       
│  │  │     │ - prctl(PR_SET_SECCOMP)     
│  │  │     │ - 应用BPF过滤器:              
│  │  │     │   阻止 socket(AF_UNIX, ...)   
│  │  │     │                              
│  │  │     │  ┌────────────────────────
│  │  │     │  │ 用户命令执行             
│  │  │     │  │                         
│  │  │     │  │ ✓ 可以连接继承的socket  
│  │  │     │  │ ✗ 不能创建新Unix socket 
│  │  │     │  │ ✗ 不能看到外层进程    
│  │  │     │  │ (独立PID命名空间)      
│  │  │     │  └─────────────────────────
│  │  │     └──────────────────────────────
│  │  └───────────────────────────────────────
│  └────────────────────────────────────────────
└──────────────────────────────────────────────────

Linux 网络桥接架构

Linux 使用 bwrap --unshare-net 完全隔离网络命名空间后,沙箱内进程失去所有网络访问能力。但为了支持受控的网络访问,需要建立从沙箱到主机代理的通信通道。

双层 Socat 桥接方案

源码位置linux-sandbox-utils.ts

// 文件: src/sandbox/linux-sandbox-utils.ts

/**
 * 初始化 Linux 网络桥接
 * 
 * 架构设计:
 * 主机侧: socat监听Unix socket → 转发到主机代理(TCP)
 * 沙箱侧: socat监听TCP端口 → 转发到Unix socket(通过bind挂载)
 * 
 * 数据流: 沙箱进程 → TCP:3128 → socat → Unix Socket → socat → TCP:代理端口
 */
export async function initializeLinuxNetworkBridge(
  httpProxyPort: number,
  socksProxyPort: number,
): Promise<LinuxNetworkBridgeContext> {
  // 生成唯一的socket文件名
  const socketId = randomBytes(8).toString('hex')
  const httpSocketPath = join(tmpdir(), `claude-http-${socketId}.sock`)
  const socksSocketPath = join(tmpdir(), `claude-socks-${socketId}.sock`)

  // ========== 主机侧: 启动 HTTP 桥接 ==========
  // socat: Unix socket监听 → TCP转发到主机HTTP代理
  const httpSocatArgs = [
    `UNIX-LISTEN:${httpSocketPath},fork,reuseaddr`,  // 监听Unix socket
    `TCP:localhost:${httpProxyPort},keepalive,keepidle=10,keepintvl=5,keepcnt=3`,  // 转发到TCP
  ]
  
  const httpBridgeProcess = spawn('socat', httpSocatArgs, { stdio: 'ignore' })

  // ========== 主机侧: 启动 SOCKS 桥接 ==========
  const socksSocatArgs = [
    `UNIX-LISTEN:${socksSocketPath},fork,reuseaddr`,
    `TCP:localhost:${socksProxyPort},keepalive,keepidle=10,keepintvl=5,keepcnt=3`,
  ]
  
  const socksBridgeProcess = spawn('socat', socksSocatArgs, { stdio: 'ignore' })

  // 等待socket文件创建完成
  const maxAttempts = 5
  for (let i = 0; i < maxAttempts; i++) {
    if (fs.existsSync(httpSocketPath) && fs.existsSync(socksSocketPath)) {
      break
    }
    await new Promise(resolve => setTimeout(resolve, i * 100))
  }

  return {
    httpSocketPath,
    socksSocketPath,
    httpBridgeProcess,
    socksBridgeProcess,
    httpProxyPort,
    socksProxyPort,
  }
}

沙箱内的桥接配置 (同文件 buildSandboxCommand 函数):

/**
 * 构建沙箱内执行的命令
 * 在沙箱内部启动socat监听器,连接到绑定的Unix socket
 */
function buildSandboxCommand(
  httpSocketPath: string,
  socksSocketPath: string,
  userCommand: string,
  applySeccompPrefix: string | undefined,
  shell?: string,
): string {
  const shellPath = shell || 'bash'
  
  // 沙箱内的socat命令: TCP监听 → Unix socket连接
  const socatCommands = [
    // HTTP代理: 监听3128端口,转发到Unix socket
    `socat TCP-LISTEN:3128,fork,reuseaddr UNIX-CONNECT:${httpSocketPath} >/dev/null 2>&1 &`,
    
    // SOCKS代理: 监听1080端口,转发到Unix socket  
    `socat TCP-LISTEN:1080,fork,reuseaddr UNIX-CONNECT:${socksSocketPath} >/dev/null 2>&1 &`,
    
    // 退出时清理socat进程
    'trap "kill %1 %2 2>/dev/null; exit" EXIT',
  ]

  // 在socat之后执行用户命令(可能带seccomp限制)
  if (applySeccompPrefix) {
    const applySeccompCmd =
      applySeccompPrefix + shellquote.quote([shellPath, '-c', userCommand])
    const innerScript = [...socatCommands, applySeccompCmd].join('\n')
    return `${shellPath} -c ${shellquote.quote([innerScript])}`
  } else {
    const innerScript = [
      ...socatCommands,
      `eval ${shellquote.quote([userCommand])}`,
    ].join('\n')
    return `${shellPath} -c ${shellquote.quote([innerScript])}`
  }
}

1.3 环境变量注入

源码位置sandbox-utils.ts

// 文件: src/sandbox/sandbox-utils.ts

/**
 * 生成代理环境变量,确保沙箱内所有网络流量经过代理
 */
export function generateProxyEnvVars(
  httpProxyPort?: number,
  socksProxyPort?: number,
): string[] {
  const envVars: string[] = [`SANDBOX_RUNTIME=1`, `TMPDIR=${tmpdir}`]

  // 设置NO_PROXY排除本地和私有网络
  const noProxyAddresses = [
    'localhost', '127.0.0.1', '::1', '*.local', '.local',
    '169.254.0.0/16',  // Link-local
    '10.0.0.0/8',      // Private network
    '172.16.0.0/12',   // Private network
    '192.168.0.0/16',  // Private network
  ].join(',')
  envVars.push(`NO_PROXY=${noProxyAddresses}`)

  // HTTP/HTTPS代理 (端口3128)
  if (httpProxyPort) {
    envVars.push(`HTTP_PROXY=http://localhost:${httpProxyPort}`)
    envVars.push(`HTTPS_PROXY=http://localhost:${httpProxyPort}`)
    envVars.push(`http_proxy=http://localhost:${httpProxyPort}`)
    envVars.push(`https_proxy=http://localhost:${httpProxyPort}`)
  }

  // SOCKS5代理 (端口1080)
  if (socksProxyPort) {
    // 使用socks5h://确保DNS解析也通过代理
    envVars.push(`ALL_PROXY=socks5h://localhost:${socksProxyPort}`)
    envVars.push(`all_proxy=socks5h://localhost:${socksProxyPort}`)

    // Git SSH通过代理
    const platform = getPlatform()
    if (platform === 'macos') {
      // macOS: 使用nc的SOCKS5支持
      envVars.push(
        `GIT_SSH_COMMAND=ssh -o ProxyCommand='nc -X 5 -x localhost:${socksProxyPort} %h %p'`
      )
    } else if (platform === 'linux' && httpProxyPort) {
      // Linux: 使用socat通过HTTP代理隧道
      envVars.push(
        `GIT_SSH_COMMAND=ssh -o ProxyCommand='socat - PROXY:localhost:%h:%p,proxyport=${httpProxyPort}'`
      )
    }

    // 其他工具代理配置
    envVars.push(`FTP_PROXY=socks5h://localhost:${socksProxyPort}`)
    envVars.push(`GRPC_PROXY=socks5h://localhost:${socksProxyPort}`)
    envVars.push(`DOCKER_HTTP_PROXY=http://localhost:${httpProxyPort || socksProxyPort}`)
    
    // Google Cloud SDK特殊配置
    if (httpProxyPort) {
      envVars.push(`CLOUDSDK_PROXY_TYPE=https`)
      envVars.push(`CLOUDSDK_PROXY_ADDRESS=localhost`)
      envVars.push(`CLOUDSDK_PROXY_PORT=${httpProxyPort}`)
    }
  }

  return envVars
}

二、macOS Seatbelt 沙箱实现

2.1 SBPL配置文件生成

源码位置macos-sandbox-utils.ts

// 文件: src/sandbox/macos-sandbox-utils.ts

/**
 * 生成完整的macOS Seatbelt配置文件 (SBPL - Sandbox Profile Language)
 */
function generateSandboxProfile({
  readConfig,
  writeConfig,
  httpProxyPort,
  socksProxyPort,
  needsNetworkRestriction,
  allowUnixSockets,
  allowAllUnixSockets,
  allowLocalBinding,
  logTag,
}: {...}): string {
  const profile: string[] = [
    '(version 1)',
    // 🔒 默认拒绝所有操作 (白名单模式)
    `(deny default (with message "${logTag}"))`,
    '',
    '; Essential permissions - 基于Chrome沙箱策略',
    '(allow process-exec)',      // 允许执行进程
    '(allow process-fork)',      // 允许fork
    '(allow process-info* (target same-sandbox))',  // 只允许查看沙箱内进程
    '(allow signal (target same-sandbox))',         // 只允许向沙箱内进程发信号
    '',
    '; Mach IPC - 仅允许特定的系统服务',
    '(allow mach-lookup',
    '  (global-name "com.apple.audio.systemsoundserver")',
    '  (global-name "com.apple.system.logger")',
    '  (global-name "com.apple.securityd.xpc")',
    // ... 更多系统服务
    ')',
  ]

  // ========== 网络规则 ==========
  profile.push('; Network')
  if (!needsNetworkRestriction) {
    profile.push('(allow network*)')  // 不限制网络
  } else {
    // 允许本地绑定 (如果配置)
    if (allowLocalBinding) {
      // 使用"*:*"而非"localhost:*"以支持IPv6双栈
      profile.push('(allow network-bind (local ip "*:*"))')
      profile.push('(allow network-inbound (local ip "*:*"))')
      profile.push('(allow network-outbound (local ip "*:*"))')
    }

    // Unix Socket控制
    if (allowAllUnixSockets) {
      profile.push('(allow system-socket (socket-domain AF_UNIX))')
      profile.push('(allow network-bind (local unix-socket (path-regex #"^/")))')
      profile.push('(allow network-outbound (remote unix-socket (path-regex #"^/")))')
    } else if (allowUnixSockets && allowUnixSockets.length > 0) {
      profile.push('(allow system-socket (socket-domain AF_UNIX))')
      for (const socketPath of allowUnixSockets) {
        const normalizedPath = normalizePathForSandbox(socketPath)
        profile.push(
          `(allow network-bind (local unix-socket (subpath ${escapePath(normalizedPath)})))`
        )
        profile.push(
          `(allow network-outbound (remote unix-socket (subpath ${escapePath(normalizedPath)})))`
        )
      }
    }

    // 允许连接到HTTP代理
    if (httpProxyPort !== undefined) {
      profile.push(
        `(allow network-bind (local ip "localhost:${httpProxyPort}"))`,
        `(allow network-inbound (local ip "localhost:${httpProxyPort}"))`,
        `(allow network-outbound (remote ip "localhost:${httpProxyPort}"))`
      )
    }

    // 允许连接到SOCKS代理
    if (socksProxyPort !== undefined) {
      profile.push(
        `(allow network-bind (local ip "localhost:${socksProxyPort}"))`,
        `(allow network-inbound (local ip "localhost:${socksProxyPort}"))`,
        `(allow network-outbound (remote ip "localhost:${socksProxyPort}"))`
      )
    }
  }

  // ========== 文件读取规则 ==========
  profile.push('; File read')
  profile.push(...generateReadRules(readConfig, logTag, writeConfig?.allowOnly))

  // ========== 文件写入规则 ==========
  profile.push('; File write')
  profile.push(...generateWriteRules(writeConfig, logTag))

  return profile.join('\n')
}

2.2 文件读取规则生成

/**
 * 生成文件读取规则
 * 策略: 默认允许 → 拒绝特定路径 → 重新允许被拒绝路径中的子路径
 * 
 * Seatbelt规则: 后面的规则优先级更高
 */
function generateReadRules(
  config: FsReadRestrictionConfig | undefined,
  logTag: string,
  writeAllowPaths?: string[],
): string[] {
  if (!config) {
    return [`(allow file-read*)`]  // 无限制
  }

  const rules: string[] = []

  // 1. 默认允许所有读取
  rules.push(`(allow file-read*)`)

  // 2. 拒绝特定路径
  for (const pathPattern of config.denyOnly || []) {
    const normalizedPath = normalizePathForSandbox(pathPattern)

    if (containsGlobChars(normalizedPath)) {
      // 使用正则匹配glob模式
      const regexPattern = globToRegex(normalizedPath)
      rules.push(
        `(deny file-read*`,
        `  (regex ${escapePath(regexPattern)})`,
        `  (with message "${logTag}"))`
      )
    } else {
      // 使用子路径匹配
      rules.push(
        `(deny file-read*`,
        `  (subpath ${escapePath(normalizedPath)})`,
        `  (with message "${logTag}"))`
      )
    }
  }

  // 3. 重新允许被拒绝区域中的特定路径 (allowRead优先于denyRead)
  for (const pathPattern of config.allowWithinDeny || []) {
    const normalizedPath = normalizePathForSandbox(pathPattern)

    if (containsGlobChars(normalizedPath)) {
      const regexPattern = globToRegex(normalizedPath)
      rules.push(
        `(allow file-read*`,
        `  (regex ${escapePath(regexPattern)})`,
        `  (with message "${logTag}"))`
      )
    } else {
      rules.push(
        `(allow file-read*`,
        `  (subpath ${escapePath(normalizedPath)})`,
        `  (with message "${logTag}"))`
      )
    }
  }

  // 4. 阻止通过mv/rename绕过读取限制
  rules.push(...generateMoveBlockingRules(config.denyOnly || [], logTag))

  // 5. 对写入允许的路径,重新允许删除操作
  if (writeAllowPaths && writeAllowPaths.length > 0) {
    for (const pathPattern of writeAllowPaths) {
      const normalizedPath = normalizePathForSandbox(pathPattern)
      if (containsGlobChars(normalizedPath)) {
        const regexPattern = globToRegex(normalizedPath)
        rules.push(
          `(allow file-write-unlink`,
          `  (regex ${escapePath(regexPattern)})`,
          `  (with message "${logTag}"))`
        )
      } else {
        rules.push(
          `(allow file-write-unlink`,
          `  (subpath ${escapePath(normalizedPath)})`,
          `  (with message "${logTag}"))`
        )
      }
    }
  }

  return rules
}

2.3 命令包装

/**
 * 使用sandbox-exec包装macOS命令
 */
export function wrapCommandWithSandboxMacOS(params: MacOSSandboxParams): string {
  const { command, needsNetworkRestriction, httpProxyPort, socksProxyPort, ... } = params

  // 如果没有限制,直接返回原命令
  if (!needsNetworkRestriction && !hasReadRestrictions && !hasWriteRestrictions) {
    return command
  }

  // 生成日志标签 (用于违规追踪)
  const logTag = generateLogTag(command)

  // 生成Seatbelt配置文件
  const profile = generateSandboxProfile({ ... })

  // 生成代理环境变量
  const proxyEnvArgs = generateProxyEnvVars(httpProxyPort, socksProxyPort)

  // 包装命令: env设置环境变量 + sandbox-exec应用配置
  const wrappedCommand = shellquote.quote([
    'env',
    ...proxyEnvArgs,
    'sandbox-exec',
    '-p',           // 内联配置文件
    profile,        // SBPL配置
    shell,          // 用户shell (bash/zsh)
    '-c',
    command,        // 用户命令
  ])

  return wrappedCommand
}

三、配置系统 (Zod验证)

3.1 域名模式验证

源码位置sandbox-config.ts

// 文件: src/sandbox/sandbox-config.ts

/**
 * 域名模式验证Schema
 * 防止配置过于宽泛的通配符
 */
const domainPatternSchema = z.string().refine(
  val => {
    // 拒绝协议、路径、端口
    if (val.includes('://') || val.includes('/') || val.includes(':')) {
      return false
    }

    // 允许localhost
    if (val === 'localhost') return true

    // 允许 *.example.com 形式的通配符
    if (val.startsWith('*.')) {
      const domain = val.slice(2)
      // 🛡️ 安全: 必须有至少一个点,防止 *.com 这样过于宽泛的模式
      if (!domain.includes('.') || domain.startsWith('.') || domain.endsWith('.')) {
        return false
      }
      // 必须至少两部分 (如 example.com)
      const parts = domain.split('.')
      return parts.length >= 2 && parts.every(p => p.length > 0)
    }

    // 拒绝其他任何通配符
    if (val.includes('*')) return false

    // 普通域名必须有至少一个点
    return val.includes('.') && !val.startsWith('.') && !val.endsWith('.')
  },
  {
    message: 'Invalid domain pattern. Overly broad patterns like "*.com" are not allowed'
  }
)

3.2 完整配置Schema

/**
 * 网络配置Schema
 */
export const NetworkConfigSchema = z.object({
  allowedDomains: z.array(domainPatternSchema)
    .describe('允许的域名列表 (如 ["github.com", "*.npmjs.org"])'),
  deniedDomains: z.array(domainPatternSchema)
    .describe('拒绝的域名列表'),
  allowUnixSockets: z.array(z.string()).optional()
    .describe('macOS: 允许的Unix socket路径。Linux上被忽略(seccomp无法按路径过滤)'),
  allowAllUnixSockets: z.boolean().optional()
    .describe('如果为true,允许所有Unix socket(两平台都禁用阻塞)'),
  allowLocalBinding: z.boolean().optional()
    .describe('是否允许绑定本地端口 (默认: false)'),
  httpProxyPort: z.number().int().min(1).max(65535).optional()
    .describe('使用外部HTTP代理的端口,而非启动本地代理'),
  socksProxyPort: z.number().int().min(1).max(65535).optional()
    .describe('使用外部SOCKS代理的端口,而非启动本地代理'),
  mitmProxy: MitmProxyConfigSchema.optional()
    .describe('MITM代理配置,将匹配的路由到上游MITM代理'),
  parentProxy: ParentProxyConfigSchema.optional()
    .describe('上游HTTP代理,用于企业代理环境'),
})

/**
 * 文件系统配置Schema
 */
export const FilesystemConfigSchema = z.object({
  denyRead: z.array(filesystemPathSchema)
    .describe('拒绝读取的路径'),
  allowRead: z.array(filesystemPathSchema).optional()
    .describe('在被拒绝区域中重新允许读取的路径 (优先级高于denyRead)'),
  allowWrite: z.array(filesystemPathSchema)
    .describe('允许写入的路径'),
  denyWrite: z.array(filesystemPathSchema)
    .describe('拒绝写入的路径 (优先级高于allowWrite)'),
  allowGitConfig: z.boolean().optional()
    .describe('允许写入.git/config文件 (默认: false)'),
})

/**
 * 主配置Schema
 */
export const SandboxRuntimeConfigSchema = z.object({
  network: NetworkConfigSchema,
  filesystem: FilesystemConfigSchema,
  ignoreViolations: IgnoreViolationsConfigSchema.optional(),
  enableWeakerNestedSandbox: z.boolean().optional()
    .describe('启用较弱的嵌套沙箱模式 (用于Docker环境)'),
  enableWeakerNetworkIsolation: z.boolean().optional()
    .describe('允许访问com.apple.trustd.agent (macOS,用于Go程序TLS验证)'),
  ripgrep: RipgrepConfigSchema.optional(),
  mandatoryDenySearchDepth: z.number().int().min(1).max(10).optional()
    .describe('Linux上搜索危险文件的最大目录深度 (默认: 3)'),
  allowPty: z.boolean().optional()
    .describe('允许伪终端(pty)操作 (仅macOS)'),
  seccomp: SeccompConfigSchema.optional()
    .describe('自定义seccomp二进制路径 (仅Linux)'),
})

四、安全加固机制

4.1 危险文件保护

源码位置sandbox-utils.ts

// 文件: src/sandbox/sandbox-utils.ts

/**
 * 危险文件列表 - 可用于代码执行或数据泄露
 */
export const DANGEROUS_FILES = [
  '.gitconfig', '.gitmodules',    // Git配置
  '.bashrc', '.bash_profile',     // Bash配置
  '.zshrc', '.zprofile', '.profile',  // Zsh配置
  '.ripgreprc',                   // Ripgrep配置
  '.mcp.json',                    // MCP服务器配置
] as const

/**
 * 危险目录列表
 */
export const DANGEROUS_DIRECTORIES = ['.git', '.vscode', '.idea'] as const

/**
 * 获取危险目录列表 (排除.git,改为保护特定子路径)
 */
export function getDangerousDirectories(): string[] {
  return [
    ...DANGEROUS_DIRECTORIES.filter(d => d !== '.git'),
    '.claude/commands',
    '.claude/agents',
  ]
}

4.2 符号链接攻击防护

/**
 * 检查符号链接解析是否跨越预期路径边界
 * 
 * 攻击场景:
 * 1. .claude -> /tmp/decoy (符号链接)
 * 2. 用户允许写入 .claude/settings.json
 * 3. 实际写入 /tmp/decoy/settings.json
 * 4. 攻击者删除符号链接,创建真实 .claude 目录
 * 5. 现在 .claude/settings.json 指向恶意文件
 * 
 * 防护: 检测并阻止这种边界跨越
 */
export function isSymlinkOutsideBoundary(
  originalPath: string,
  resolvedPath: string,
): boolean {
  const normalizedOriginal = path.normalize(originalPath)
  const normalizedResolved = path.normalize(resolvedPath)

  // 相同路径 - 安全
  if (normalizedResolved === normalizedOriginal) {
    return false
  }

  // 处理macOS /tmp -> /private/tmp 的规范解析 (系统级符号链接)
  if (
    normalizedOriginal.startsWith('/tmp/') &&
    normalizedResolved === '/private' + normalizedOriginal
  ) {
    return false  // 允许
  }

  // 如果解析到根目录或单组件路径 - 危险
  if (normalizedResolved === '/') return true
  const resolvedParts = normalizedResolved.split('/').filter(Boolean)
  if (resolvedParts.length <= 1) return true

  // 如果原始路径以解析路径开头 - 解析路径是祖先,危险
  if (normalizedOriginal.startsWith(normalizedResolved + '/')) {
    return true
  }

  // 严格检查: 只允许在预期路径树内的解析
  const resolvedStartsWithOriginal = normalizedResolved.startsWith(
    normalizedOriginal + '/'
  )
  const resolvedIsSame = normalizedResolved === normalizedOriginal

  if (!resolvedIsSame && !resolvedStartsWithOriginal) {
    return true  // 解析超出边界
  }

  return false
}

4.3 非存在路径保护

源码位置linux-sandbox-utils.ts

/**
 * 找到第一个不存在的路径组件
 * 
 * 攻击场景: ~/.bashrc 不存在,攻击者可以创建它
 * 解决方案:
 * 1. 找到第一个不存在的路径组件
 * 2. 用 --ro-bind /dev/null <路径> 挂载
 * 3. 阻止 mkdir 创建父目录
 * 
 * 示例: 阻止 ~/.config/malware/script.sh
 * - ~/.config 存在,~/.config/malware 不存在
 * - 挂载 /dev/null 到 ~/.config/malware
 * - 现在无法创建 malware 目录
 */
function findFirstNonExistentComponent(targetPath: string): string {
  const parts = targetPath.split(path.sep)
  let currentPath = ''

  for (const part of parts) {
    if (!part) continue
    const nextPath = currentPath + path.sep + part
    if (!fs.existsSync(nextPath)) {
      return nextPath  // 返回第一个不存在的组件
    }
    currentPath = nextPath
  }

  return targetPath
}

4.4 符号链接替换攻击防护

/**
 * 查找路径中是否在允许写入范围内存在符号链接
 * 
 * 攻击场景:
 * 1. .claude -> /tmp/decoy (符号链接)
 * 2. 攻击者删除符号链接,创建真实 .claude 目录
 * 3. 写入恶意内容
 * 
 * 防护: 在符号链接处挂载 /dev/null 阻止替换
 */
function findSymlinkInPath(
  targetPath: string,
  allowedWritePaths: string[],
): string | null {
  const parts = targetPath.split(path.sep)
  let currentPath = ''

  for (const part of parts) {
    if (!part) continue
    const nextPath = currentPath + path.sep + part

    try {
      const stats = fs.lstatSync(nextPath)
      if (stats.isSymbolicLink()) {
        // 检查此符号链接是否在允许写入范围内
        const isWithinAllowedPath = allowedWritePaths.some(
          allowedPath =>
            nextPath.startsWith(allowedPath + '/') || nextPath === allowedPath
        )
        if (isWithinAllowedPath) {
          return nextPath  // 发现符号链接,需要保护
        }
      }
    } catch {
      break  // 路径不存在
    }
    currentPath = nextPath
  }

  return null
}

五、上游代理支持 (Parent Proxy)

5.1 NO_PROXY解析

源码位置parent-proxy.ts

// 文件: src/sandbox/parent-proxy.ts

/**
 * 解析NO_PROXY规则
 * 支持: 通配符(*)、主机名后缀、CIDR范围
 */
function parseNoProxy(raw: string): NoProxyRules {
  const rules: NoProxyRules = {
    all: false,
    suffixes: [],
    cidr: new BlockList(),
  }

  for (let entry of raw.split(',')) {
    entry = entry.trim()
    if (!entry) continue
    
    // "*" 匹配所有 - 完全绕过代理
    if (entry === '*') {
      rules.all = true
      continue
    }

    // CIDR范围匹配 (如 192.168.0.0/16)
    const slash = entry.indexOf('/')
    if (slash !== -1) {
      const ip = entry.slice(0, slash)
      const prefixStr = entry.slice(slash + 1)
      const fam = isIP(ip)
      if (fam && prefixStr !== '' && /^\d+$/.test(prefixStr)) {
        const prefix = Number(prefixStr)
        const max = fam === 6 ? 128 : 32
        if (prefix >= 0 && prefix <= max) {
          try {
            rules.cidr.addSubnet(ip, prefix, fam === 6 ? 'ipv6' : 'ipv4')
          } catch {
            // 无效CIDR - 忽略
          }
          continue
        }
      }
      continue
    }

    // 主机名后缀匹配
    let v = entry.toLowerCase()
    // 处理 [ipv6]:port 格式
    const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(v)
    if (bracketed) v = bracketed[1]!
    // 去除开头的 *.
    if (v.startsWith('*.')) v = v.slice(1)
    // 去除端口号 (除非是IP字面量)
    const bareFam = isIP(v)
    if (!bareFam) {
      const colon = v.lastIndexOf(':')
      if (colon !== -1 && /^\d+$/.test(v.slice(colon + 1))) {
        v = v.slice(0, colon)
      }
    } else {
      // IP字面量 - 作为精确匹配添加到CIDR列表
      try {
        rules.cidr.addAddress(v, bareFam === 6 ? 'ipv6' : 'ipv4')
        continue
      } catch {
        // 回退到后缀匹配
      }
    }
    rules.suffixes.push(v)
  }

  return rules
}

5.2 NO_PROXY匹配逻辑

/**
 * 判断是否应该绕过父代理直接连接
 * 遵循golang语义: .example.com 和 example.com 都匹配 foo.example.com
 */
export function shouldBypassParentProxy(
  resolved: ResolvedParentProxy,
  host: string,
): boolean {
  const h = stripBrackets(host.toLowerCase().replace(/\.$/, ''))

  // 总是绕过回环地址
  if (h === 'localhost') return true
  const fam = isIP(h)
  if (fam) {
    if (LOOPBACK.check(h, fam === 6 ? 'ipv6' : 'ipv4')) return true
  }

  // "*" 匹配所有
  if (resolved.noProxy.all) return true

  // CIDR匹配
  if (fam) {
    if (resolved.noProxy.cidr.check(h, fam === 6 ? 'ipv6' : 'ipv4')) return true
  }

  // 后缀匹配
  for (const v of resolved.noProxy.suffixes) {
    if (v.startsWith('.')) {
      // .example.com 匹配 foo.example.com 和 example.com
      if (h === v.slice(1) || h.endsWith(v)) return true
    } else {
      // example.com 匹配 example.com 和 foo.example.com (golang语义)
      if (h === v || h.endsWith('.' + v)) return true
    }
  }
  return false
}

// 回环地址BlockList (127.0.0.0/8, ::1, ::ffff:127.0.0.0/104)
const LOOPBACK = (() => {
  const bl = new BlockList()
  bl.addSubnet('127.0.0.0', 8, 'ipv4')
  bl.addAddress('::1', 'ipv6')
  bl.addSubnet('::ffff:127.0.0.0', 104, 'ipv6')  // IPv4映射的IPv6回环
  return bl
})()

5.3 CONNECT隧道

/**
 * 通用CONNECT隧道: 通过代理建立TCP连接
 * 支持Unix socket、TCP、TLS传输
 */
export function openConnectTunnel(opts: ConnectTunnelOptions): Promise<Socket> {
  const { destHost, destPort } = opts

  // 🛡️ CRLF注入防护: 验证目标主机名
  const bare = stripBrackets(destHost)
  if (!isValidHost(bare)) {
    return Promise.reject(
      new Error(`Invalid destination host for CONNECT: ${JSON.stringify(destHost)}`)
    )
  }

  const authority = isIP(bare) === 6 ? `[${bare}]:${destPort}` : `${bare}:${destPort}`

  return new Promise((resolve, reject) => {
    const sock = opts.dial()
    let settled = false

    sock.setTimeout(opts.timeoutMs ?? CONNECT_TIMEOUT_MS, () =>
      fail(new Error('CONNECT handshake timed out'))
    )

    sock.once(opts.readyEvent, () => {
      // 发送CONNECT请求
      sock.write(
        `CONNECT ${authority} HTTP/1.1\r\n` +
        `Host: ${authority}\r\n` +
        (opts.authHeader ? `Proxy-Authorization: ${opts.authHeader}\r\n` : '') +
        '\r\n'
      )

      // 读取响应
      let buf = ''
      const onData = (chunk: Buffer) => {
        buf += chunk.toString('latin1')
        const end = buf.indexOf('\r\n\r\n')
        if (end === -1) {
          // 限制响应头大小,防止恶意代理
          if (buf.length > 16 * 1024)
            fail(new Error('CONNECT response header too large'))
          return
        }

        sock.pause()
        sock.removeListener('data', onData)

        const statusLine = buf.slice(0, buf.indexOf('\r\n'))
        // 检查2xx状态码
        if (!/^HTTP\/1\.[01] 2\d\d(?:\s|$)/.test(statusLine)) {
          return fail(new Error(`Proxy refused CONNECT: ${statusLine.trim()}`))
        }

        // 将剩余数据推回流中
        const rest = buf.slice(end + 4)
        if (rest.length) sock.unshift(Buffer.from(rest, 'latin1'))

        settled = true
        resolve(sock)  // 隧道建立成功
      }
      sock.on('data', onData)
    })
  })
}

六、代理实现 (HTTP & SOCKS5)

6.1 HTTP代理核心逻辑

源码位置http-proxy.ts

// 文件: src/sandbox/http-proxy.ts

export function createHttpProxyServer(options: HttpProxyServerOptions): Server {
  const server = createServer()

  // ========== 处理HTTPS CONNECT请求 ==========
  server.on('connect', async (req, socket, head) => {
    const target = parseConnectTarget(req.url!)
    if (!target) {
      socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
      return
    }
    const { hostname, port } = target

    // 🔒 域名过滤检查
    const allowed = await options.filter(port, hostname, socket)
    if (!allowed) {
      socket.end(
        'HTTP/1.1 403 Forbidden\r\n' +
        'X-Proxy-Error: blocked-by-allowlist\r\n' +
        '\r\n' +
        'Connection blocked by network allowlist'
      )
      return
    }

    // 决定上游路由: MITM代理 > 父代理 > 直连
    const mitmSocketPath = options.getMitmSocketPath?.(hostname)
    const parentUrl = !mitmSocketPath && options.parentProxy &&
      !shouldBypassParentProxy(options.parentProxy, hostname)
        ? selectParentProxyUrl(options.parentProxy, { isHttps: true })
        : undefined

    let upstream: Socket
    if (mitmSocketPath) {
      // 路由到MITM代理进行流量检查
      upstream = await openConnectTunnel({
        dial: () => connect({ path: mitmSocketPath }),
        readyEvent: 'connect',
        destHost: hostname,
        destPort: port,
      })
    } else if (parentUrl) {
      // 通过父代理隧道
      upstream = await connectViaParentProxy(parentUrl, hostname, port)
    } else {
      // 直连
      upstream = await dialDirect(hostname, port)
    }

    // 建立双向数据流
    socket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
    if (head.length) upstream.write(head)  // 转发CONNECT请求中的数据
    upstream.pipe(socket)
    socket.pipe(upstream)
  })

  // ========== 处理普通HTTP请求 ==========
  server.on('request', async (req, res) => {
    const url = new URL(req.url!)
    const hostname = stripBrackets(url.hostname)
    const port = url.port ? parseInt(url.port, 10) : 
      url.protocol === 'https:' ? 443 : 80

    // 🔒 域名过滤检查
    const allowed = await options.filter(port, hostname, req.socket)
    if (!allowed) {
      res.writeHead(403, { 'X-Proxy-Error': 'blocked-by-allowlist' })
      res.end('Connection blocked by network allowlist')
      return
    }

    // 重构绝对URI,防止URL解析器差异绕过
    const absUrl = `${url.protocol}//${url.host}${url.pathname}${url.search}`

    // 决定上游路由
    const mitmSocketPath = options.getMitmSocketPath?.(hostname)
    let proxyReq
    if (mitmSocketPath) {
      // 通过MITM代理
      const mitmAgent = new Agent({ socketPath: mitmSocketPath })
      proxyReq = httpRequest({
        agent: mitmAgent,
        path: absUrl,
        method: req.method,
        headers: fwdHeaders,
      }, proxyRes => {
        res.writeHead(proxyRes.statusCode!, stripHopByHop(proxyRes.headers))
        proxyRes.pipe(res)
      })
    } else {
      // 直连或通过父代理
      const requestFn = url.protocol === 'https:' ? httpsRequest : httpRequest
      proxyReq = requestFn({
        hostname,
        port,
        path: url.pathname + url.search,
        method: req.method,
        headers: fwdHeaders,
      }, proxyRes => {
        res.writeHead(proxyRes.statusCode!, stripHopByHop(proxyRes.headers))
        proxyRes.pipe(res)
      })
    }

    req.pipe(proxyReq)
  })

  return server
}

6.2 SOCKS5代理实现

源码位置socks-proxy.ts

// 文件: src/sandbox/socks-proxy.ts

export function createSocksProxyServer(options: SocksProxyServerOptions): SocksProxyWrapper {
  const socksServer = createServer()

  // ========== 规则验证器 (连接前检查) ==========
  socksServer.setRulesetValidator(async conn => {
    const hostname = conn.destAddress
    const port = conn.destPort

    // 🛡️ 安全检查: SOCKS5 DOMAINNAME是原始字节串,零验证
    // 拒绝控制字符(空字节、CRLF),防止绕过允许列表匹配
    if (!isValidHost(hostname)) {
      logForDebugging(`Rejecting malformed SOCKS host: ${JSON.stringify(hostname)}`)
      return false
    }

    // 🔒 域名过滤检查
    const allowed = await options.filter(port, hostname)
    return allowed
  })

  // ========== 连接处理器 (建立TCP隧道) ==========
  socksServer.setConnectionHandler((conn, sendStatus) => {
    const host = conn.destAddress
    const port = conn.destPort

    // SOCKS是不透明TCP隧道,语义等同于HTTP CONNECT
    // 总是优先使用HTTPS_PROXY
    const parentUrl = options.parentProxy && 
      !shouldBypassParentProxy(options.parentProxy, host)
        ? selectParentProxyUrl(options.parentProxy, { isHttps: true })
        : undefined

    const open = parentUrl
      ? connectViaParentProxy(parentUrl, host, port)
      : dialDirect(host, port)

    open.then(upstream => {
      sendStatus('REQUEST_GRANTED')  // SOCKS5成功响应
      upstream.pipe(conn.socket)     // 双向数据转发
      conn.socket.pipe(upstream)
    }).catch(err => {
      if (!clientGone) {
        sendStatus('HOST_UNREACHABLE')
      }
    })
  })

  return { server: socksServer, getPort, listen, close, unref }
}

七、Seccomp BPF过滤器生成

7.1 架构支持检测

源码位置generate-seccomp-filter.ts

// 文件: src/sandbox/generate-seccomp-filter.ts

/**
 * 映射Node.js process.arch到vendor目录架构名称
 * 返回null表示不支持的架构
 */
function getVendorArchitecture(): string | null {
  const arch = process.arch
  switch (arch) {
    case 'x64':
    case 'x86_64':
      return 'x64'
    case 'arm64':
    case 'aarch64':
      return 'arm64'
    case 'ia32':
    case 'x86':
      // ❌ 不支持32位x86
      // 原因: socketcall()系统调用无法被当前seccomp过滤器处理
      // 在32位x86上,所有socket操作都通过socketcall()多路复用
      // 需要复杂的BPF逻辑检查socketcall的子函数参数
      logForDebugging(
        `[SeccompFilter] 32-bit x86 (ia32) not supported - missing socketcall() blocking`
      )
      return null
    default:
      logForDebugging(`[SeccompFilter] Unsupported architecture: ${arch}`)
      return null
  }
}

7.2 二进制文件查找

/**
 * 获取apply-seccomp二进制文件路径
 * 预编译的BPF过滤器按架构组织:
 * - vendor/seccomp/{x64,arm64}/apply-seccomp
 */
export function getApplySeccompBinaryPath(seccompBinaryPath?: string): string | null {
  // 0. 检查显式指定路径
  if (seccompBinaryPath && fs.existsSync(seccompBinaryPath)) {
    return seccompBinaryPath
  }

  const arch = getVendorArchitecture()
  if (!arch) return null

  // 1-3. 检查本地路径 (bundled/package/dist)
  for (const binaryPath of getLocalSeccompPaths('apply-seccomp')) {
    if (fs.existsSync(binaryPath)) {
      return binaryPath
    }
  }

  // 4. 回退: 检查全局npm安装 (用于原生构建)
  for (const globalBase of getGlobalNpmPaths()) {
    const binaryPath = join(globalBase, 'vendor', 'seccomp', arch, 'apply-seccomp')
    if (fs.existsSync(binaryPath)) {
      return binaryPath
    }
  }

  return null
}

八、挂载点清理机制 (Linux特有)

8.1 问题背景

源码位置linux-sandbox-utils.ts

// 文件: src/sandbox/linux-sandbox-utils.ts

/**
 * 跟踪bwrap为非存在deny路径创建的挂载点
 * 
 * 问题: bwrap执行 --ro-bind /dev/null /nonexistent/path 时
 *       会在主机文件系统创建空文件作为挂载点
 *       这些文件在bwrap退出后仍然存在
 */
const bwrapMountPoints: Set<string> = new Set()

/**
 * 活动沙箱计数器
 * 延迟清理策略: 确保没有运行中的沙箱使用这些挂载点
 * 防止删除挂载点导致运行中沙箱的bind mount失效
 */
let activeSandboxCount = 0

8.2 延迟清理实现

/**
 * 清理bwrap创建的挂载点文件
 * 
 * 每次调用递减activeSandboxCount
 * 只有计数器归零时才真正删除文件
 */
export function cleanupBwrapMountPoints(opts?: { force?: boolean }): void {
  if (!opts?.force) {
    if (activeSandboxCount > 0) {
      activeSandboxCount--
    }
    // 延迟清理: 还有活动沙箱
    if (activeSandboxCount > 0) {
      logForDebugging(
        `[Sandbox Linux] Deferring cleanup — ${activeSandboxCount} sandbox(es) active`
      )
      return
    }
  } else {
    activeSandboxCount = 0  // 强制清理
  }

  // 清理所有挂载点
  for (const mountPoint of bwrapMountPoints) {
    try {
      const stat = fs.statSync(mountPoint)
      
      // 只删除bwrap创建的空文件
      if (stat.isFile() && stat.size === 0) {
        fs.unlinkSync(mountPoint)
        logForDebugging(`Cleaned up bwrap mount point: ${mountPoint}`)
      } else if (stat.isDirectory()) {
        // 空目录挂载点
        const entries = fs.readdirSync(mountPoint)
        if (entries.length === 0) {
          fs.rmdirSync(mountPoint)
        }
      }
    } catch {
      // 忽略清理错误
    }
  }
  bwrapMountPoints.clear()
}

九、Git工作树支持

9.1 问题检测

源码位置linux-sandbox-utils.ts

/**
 * 检查路径的任何现有组件是否是文件(非目录)
 * 
 * Git工作树场景:
 * - .git是文件 (内容: gitdir: /path/.../.git/worktrees/xxx)
 * - .git/hooks永远不可能存在
 * - 在文件上挂载目录会导致bwrap失败
 */
function hasFileAncestor(targetPath: string): boolean {
  const parts = targetPath.split(path.sep)
  let currentPath = ''

  for (const part of parts) {
    if (!part) continue
    const nextPath = currentPath + path.sep + part
    try {
      const stat = fs.statSync(nextPath)
      if (stat.isFile() || stat.isSymbolicLink()) {
        return true  // 发现文件祖先,路径无法创建
      }
    } catch {
      break  // 路径不存在
    }
    currentPath = nextPath
  }

  return false
}

9.2 应用逻辑

// 在linuxGetMandatoryDenyPaths函数中
const dotGitPath = path.resolve(cwd, '.git')
let dotGitIsDirectory = false
try {
  dotGitIsDirectory = fs.statSync(dotGitPath).isDirectory()
} catch {
  // .git不存在
}

if (dotGitIsDirectory) {
  // 标准git仓库: 保护.git/hooks和.git/config
  denyPaths.push(path.resolve(cwd, '.git/hooks'))
  if (!allowGitConfig) {
    denyPaths.push(path.resolve(cwd, '.git/config'))
  }
} else {
  // Git工作树或无git: 不保护.git路径
  // 避免在文件上尝试挂载目录
}

十、容器环境适配

10.1 Docker环境检测

源码位置linux-sandbox-utils.ts

// 文件: src/sandbox/linux-sandbox-utils.ts

// ========== PID命名空间隔离 ==========
bwrapArgs.push('--unshare-pid')

if (!enableWeakerNestedSandbox) {
  // 安全模式: 挂载新的/proc
  bwrapArgs.push('--proc', '/proc')
} else {
  // 🐳 Docker容器弱沙箱模式
  // 
  // 问题: 
  // - 容器通常没有CAP_SYS_ADMIN
  // - 但EUID=0导致bwrap尝试直接clone()而非创建user namespace
  // - 结果: EPERM错误
  //
  // 解决方案:
  // - --unshare-user: 强制使用user namespace路径
  // - --bind /proc /proc: apply-seccomp需要写/proc/self/setgroups
  bwrapArgs.push('--unshare-user', '--bind', '/proc', '/proc')
}

十一、配置示例

11.1 严格模式 (零信任)

const strictConfig: SandboxRuntimeConfig = {
  network: {
    allowedDomains: [],           // 无网络访问
    deniedDomains: [],
    allowLocalBinding: false,
    allowAllUnixSockets: false,
  },
  filesystem: {
    denyRead: ['/'],              // 拒绝所有读取
    allowRead: ['/usr', '/bin', process.cwd()],  // 允许系统和CWD
    allowWrite: [process.cwd()],  // 只允许写入CWD
    denyWrite: ['.git', 'node_modules'],
    allowGitConfig: false,
  },
}

11.2 开发模式

const devConfig: SandboxRuntimeConfig = {
  network: {
    allowedDomains: ['registry.npmjs.org', '*.github.com'],
    allowLocalBinding: true,       // 允许本地开发服务器
    allowUnixSockets: ['/var/run/docker.sock'],
  },
  filesystem: {
    denyRead: ['~/.ssh', '~/.aws'],
    allowWrite: [process.cwd(), '/tmp', '~/.npm'],
    denyWrite: ['.git/hooks'],
    allowGitConfig: true,
  },
  allowPty: true,                  // 允许伪终端
}

十二、核心包 sandbox-runtime 总结

特性 实现方案 源码位置
跨平台一致性 统一配置接口,底层适配bubblewrap(Linux)和Seatbelt(macOS) sandbox-manager.ts
嵌套命名空间 apply-seccomp在socat之后应用seccomp,解决socket创建冲突 linux-sandbox-utils.ts#L614-L642
非存在路径保护 预创建/dev/null挂载点阻止未来攻击 linux-sandbox-utils.ts#L767-L824
符号链接追踪 isSymlinkOutsideBoundary防止symlink绕过 sandbox-utils.ts#L89-L195
延迟挂载点清理 activeSandboxCount计数器防止并发竞态 linux-sandbox-utils.ts#L331-L372
智能路径展开 Linux上expandGlobPattern自动展开glob sandbox-utils.ts#L467-L521
CRLF注入防护 isValidHost严格验证主机名 parent-proxy.ts#L449-L458
Git工作树感知 hasFileAncestor检测.git是文件还是目录 linux-sandbox-utils.ts#L108-L129
主机名规范化 canonicalizeHost统一格式防止绕过 parent-proxy.ts#L471-L483
NO_PROXY兼容 parseNoProxy实现curl/golang语义 parent-proxy.ts#L115-L180

这些特性在 Claude Code 中都得到了实际工程应用,具体表现如下:

1️⃣ 跨平台一致性 - 统一配置接口

源码位置src/utils/sandbox/sandbox-adapter.ts

// 将 Claude Code 设置转换为 sandbox-runtime 统一配置格式
// 屏蔽了底层平台差异(Linux bubblewrap vs macOS sandbox-exec)
export function convertToSandboxRuntimeConfig(settings: SettingsJson): SandboxRuntimeConfig {
  return {
    network: {
      allowedDomains,      // 允许的网络域名
      deniedDomains,       // 拒绝的网络域名
      allowUnixSockets,    // Unix socket 白名单
    },
    filesystem: {
      denyRead,           // 拒绝读取的路径
      allowRead,          // 允许读取的路径
      allowWrite,         // 允许写入的路径
      denyWrite,          // 拒绝写入的路径
    },
    ripgrep: ripgrepConfig,  // 沙箱内搜索工具配置
  }
}

工程价值: 上层代码无需关心平台差异,一套配置同时支持 macOS (Seatbelt) 和 Linux (bubblewrap)


2️⃣ 嵌套命名空间 - apply-seccomp 分层隔离

依赖检查src/components/sandbox/SandboxDependenciesTab.tsx

// Linux/WSL2 需要三个关键依赖
<Text>bubblewrap (bwrap): {bwrapMissing ? '未安装' : '已安装'}</Text>
<Text>seccomp filter: {seccompMissing ? '未安装' : '已安装'}</Text>
<Text>socat: {socatMissing ? '未安装' : '已安装'}</Text>

隔离架构:

bwrap (Stage 1) - 网络命名空间隔离
  └─ socat 桥接进程 - 可创建 Unix socket
      └─ apply-seccomp (Stage 2) - 系统调用过滤
          └─ 用户代码 - 无法创建新 socket,但可用继承的 socket FD

精妙之处: 用户代码无法创建新 Unix socket(防止恶意通信),但可以使用 Stage 1 创建的 socket 与代理通信(保证正常网络功能)


3️⃣ 非存在路径保护 - /dev/null 挂载点

源码位置src/utils/sandbox/sandbox-adapter.ts:257-280

// SECURITY: 防止 Git 沙箱逃逸攻击
// 攻击者可以植入 HEAD + objects/ + refs/ 让 Claude 的 git 误判为裸仓库
const bareGitRepoFiles = ['HEAD', 'objects', 'refs', 'hooks', 'config']

for (const gitFile of bareGitRepoFiles) {
  const p = resolve(dir, gitFile)
  try {
    statSync(p)
    denyWrite.push(p)  // 文件存在:沙箱内只读绑定
  } catch {
    bareGitRepoScrubPaths.push(p)  // 文件不存在:命令后清理
  }
}

// 命令执行后立即清理植入的文件
function scrubBareGitRepoFiles(): void {
  for (const p of bareGitRepoScrubPaths) {
    try {
      rmSync(p, { recursive: true })  // 在沙箱外 git 运行前清除
    } catch { /* ENOENT 是预期情况 */ }
  }
}

工程价值: 防御 CVE 级别的 Git 沙箱逃逸攻击,双重保护(沙箱内挂载 + 沙箱外清理)


4️⃣ 符号链接追踪 - 防止 symlink 绕过

源码位置src/utils/permissions/filesystem.ts:272-365

/**
 * 获取权限检查时需要验证的所有路径
 * 包括原始路径和所有符号链接解析路径
 */
export function getPathsForPermissionCheck(inputPath: string): string[] {
  const pathSet = new Set<string>()
  pathSet.add(path)  // 总是检查原始路径

  // 追踪符号链接链,收集所有中间目标
  let currentPath = path
  const visited = new Set<string>()
  const maxDepth = 40  // 防止循环链接

  for (let depth = 0; depth < maxDepth; depth++) {
    if (visited.has(currentPath)) break
    visited.add(currentPath)

    const stats = fsImpl.lstatSync(currentPath)
    if (!stats.isSymbolicLink()) break

    const target = fsImpl.readlinkSync(currentPath)
    const absoluteTarget = nodePath.isAbsolute(target)
      ? target
      : nodePath.resolve(nodePath.dirname(currentPath), target)

    pathSet.add(absoluteTarget)  // 添加中间目标到检查列表
    currentPath = absoluteTarget
  }

  return Array.from(pathSet)
}

使用场景:

// 检查原始路径和所有 symlink 解析路径是否都在允许的工作目录内
const pathsToCheck = getPathsForPermissionCheck(filePath)
return pathsToCheck.every(p => pathInAllowedWorkingPath(p))

防止的攻击./evil.txt -> /etc/passwd 或 ./data -> /var/www/html


5️⃣ 延迟挂载点清理 - 并发安全保护

源码位置src/utils/sandbox/sandbox-adapter.ts:730-792

let initializationPromise: Promise<void> | undefined

async function initialize(): Promise<void> {
  // 防止并发初始化的竞态条件
  if (initializationPromise) {
    return initializationPromise  // 返回现有的 promise
  }

  // 同步创建 promise(在任何 await 之前)
  // 防止 wrapWithSandbox() 在 promise 赋值前被调用
  initializationPromise = (async () => {
    try {
      // 一次性检测 git worktree(会话期间缓存)
      if (worktreeMainRepoPath === undefined) {
        worktreeMainRepoPath = await detectWorktreeMainRepoPath(getCwdState())
      }

      await BaseSandboxManager.initialize(runtimeConfig, wrappedCallback)

      // 订阅设置变更以动态更新沙箱配置
      settingsSubscriptionCleanup = settingsChangeDetector.subscribe(() => {
        BaseSandboxManager.updateConfig(newConfig)
      })
    } catch (error) {
      initializationPromise = undefined  // 失败时允许重试
    }
  })()

  return initializationPromise
}

工程价值: 防止多个命令并发初始化沙箱导致的状态混乱


6️⃣ 智能路径展开 - Linux glob 警告

源码位置src/utils/sandbox/sandbox-adapter.ts:597-607

/**
 * Linux/WSL 上 bubblewrap 不支持 glob 模式
 * 返回警告让用户知道哪些规则可能无法正常工作
 */
function getLinuxGlobPatternWarnings(): string[] {
  if (platform !== 'linux' && platform !== 'wsl') return []
  if (!settings?.sandbox?.enabled) return []

  const hasGlobs = (path: string): boolean => {
    const stripped = path.replace(/\/\*\*$/, '')
    return /[*?[\]]/.test(stripped)  // 检测 glob 字符
  }

  // 检查所有权限规则中的 glob 模式
  for (const ruleString of [...permissions.allow, ...permissions.deny]) {
    if (hasGlobs(rule.ruleContent)) {
      warnings.push(ruleString)
    }
  }
  return warnings
}

工程适配: macOS 的 Seatbelt 原生支持 glob,Linux 的 bubblewrap 不支持,所以给出警告而不是静默失败


7️⃣ CRLF 注入防护 - 主机名验证

源码位置src/utils/proxy.ts 和 src/utils/managedEnvConstants.ts

/**
 * 获取 NO_PROXY 环境变量
 * 优先小写(no_proxy > NO_PROXY)
 */
export function getNoProxy(env = process.env): string | undefined {
  return env.no_proxy || env.NO_PROXY
}

/**
 * 检查 URL 是否应该绕过代理(支持 curl/golang 语义)
 * - 精确主机名匹配: "localhost"
 * - 域名后缀匹配: ".example.com"
 * - 通配符匹配: "*.example.com"
 * - IP 地址: "127.0.0.1"
 */
export function shouldBypassProxy(urlString: string, noProxy = getNoProxy()): boolean {
  // 实现 curl/golang 的 NO_PROXY 解析语义
  // 防止恶意域名绕过代理检查
}

防护场景: 防止 evil.com\x00.allowed.com 这样的控制字符注入攻击


8️⃣ Git 工作树感知 - 智能路径检测

源码位置src/utils/sandbox/sandbox-adapter.ts:422-445

/**
 * 检测 cwd 是否是 git worktree 并解析主仓库路径
 * 在 worktree 中,.git 是文件(不是目录),包含 "gitdir: ..."
 */
async function detectWorktreeMainRepoPath(cwd: string): Promise<string | null> {
  const gitPath = join(cwd, '.git')
  try {
    const gitContent = await readFile(gitPath, { encoding: 'utf8' })
    const gitdirMatch = gitContent.match(/^gitdir:\s*(.+)$/m)
    
    if (!gitdirMatch?.[1]) return null
    
    // 解析 gitdir 路径
    const gitdir = resolve(cwd, gitdirMatch[1].trim())
    
    // 格式: /path/to/main/repo/.git/worktrees/worktree-name
    const marker = `${sep}.git${sep}worktrees${sep}`
    const markerIndex = gitdir.lastIndexOf(marker)
    
    if (markerIndex > 0) {
      return gitdir.substring(0, markerIndex)  // 返回主仓库路径
    }
    return null
  } catch {
    return null  // .git 是目录(不是 worktree)
  }
}

工程应用:

// 在 worktree 中,git 操作需要写入主仓库的 .git 目录
if (worktreeMainRepoPath && worktreeMainRepoPath !== cwd) {
  allowWrite.push(worktreeMainRepoPath)  // 允许写入主仓库
}

解决的问题: Worktree 模式下 .git 是指向主仓库的文件,沙箱需要允许写入主仓库的 .git 目录(index.lock 等)


9️⃣ 主机名规范化 - 防止绕过

虽然 Claude Code 中没有直接的 canonicalizeHost 函数,但在代理配置中实现了类似的防护:

// src/upstreamproxy/upstreamproxy.ts
const NO_PROXY_LIST = [
  'localhost',
  '127.0.0.1',
  '::1',
  '*.anthropic.com',    // Bun, curl, Go (glob 匹配)
  '.anthropic.com',     // Python urllib/httpx (后缀匹配)
  'anthropic.com',      //  apex 域名后备
]

// 三种形式因为 NO_PROXY 解析在不同运行时不同
// 防止通过不同格式绕过代理规则

🔟 NO_PROXY 兼容 - curl/golang 语义

源码位置src/utils/proxy.ts:88-161

/**
 * 检查 URL 是否应该绕过代理
 * 支持 curl/golang 的 NO_PROXY 语义
 */
export function shouldBypassProxy(urlString: string, noProxy = getNoProxy()): boolean {
  const { hostname } = new URL(urlString)
  const noProxyList = (noProxy || '').split(',').map(s => s.trim())

  for (const rule of noProxyList) {
    // 精确匹配
    if (rule === hostname) return true
    
    // 域名后缀匹配 (.example.com)
    if (rule.startsWith('.') && hostname.endsWith(rule)) return true
    
    // 通配符匹配 (*.example.com)
    if (rule.startsWith('*.')) {
      const suffix = rule.slice(1)
      if (hostname.endsWith(suffix)) return true
    }
  }
  return false
}

工程价值: 兼容 curl、golang、python 等不同运行时的 NO_PROXY 解析差异,防止配置不一致导致的安全漏洞


📊 总结:

特性 实现位置 工程价值
跨平台一致性 sandbox-adapter.ts 统一接口屏蔽平台差异
嵌套命名空间 SandboxDependenciesTab.tsx 分层隔离,安全与功能兼顾
非存在路径保护 sandbox-adapter.ts:257-280 防御 Git 沙箱逃逸
符号链接追踪 filesystem.ts:272-365 防止 symlink 逃逸攻击
延迟清理 sandbox-adapter.ts:730-792 并发安全,防止竞态条件
智能路径展开 sandbox-adapter.ts:597-607 Linux 上给出 glob 警告
CRLF 防护 proxy.ts 防止控制字符注入
Git 工作树感知 sandbox-adapter.ts:422-445 正确支持 worktree 模式
主机名规范化 upstreamproxy.ts 防止格式绕过
NO_PROXY 兼容 proxy.ts:88-161 跨运行时兼容性

接下来具体深入看看Claude Code 中集成的沙箱系统架构

Claude Code 中集成的沙箱系统架构

 核心组件

┌───────────────────────────────────────────────────────────

                 Claude Code 应用层                       

├───────────────────────────────────────────────────────────

│  沙箱适配器层 (sandbox-adapter.ts)                      

│  - 设置转换                                                

│  - 权限规则解析                                            

│  - 生命周期管理                                        

├───────────────────────────────────────────────────────────

│  @anthropic-ai/sandbox-runtime                            

│  - 底层沙箱实现                                           

│  - 平台特定隔离                                          

├───────────────────────────────────────────────────────────

│  操作系统层                                                 

│  - macOS: 系统沙箱                                         

│  - Linux/WSL2: bubblewrap + seccomp                   

└───────────────────────────────────────────────────────────

核心组成文件

  1. 主要沙箱适配器:
  2. 沙箱 UI 组件:
  3. 沙箱命令和类型:
  4. 沙箱决策逻辑:

核心沙箱管理器

    sandbox-adapter.ts 是一个适配层(Adapter Layer),是把 Anthropic 官方的 @anthropic-ai/sandbox-runtime 包包装成 Claude CLI 专用的版本。简单说就是:给官方沙箱穿上 Claude CLI 的"外套",让它能识别 Claude 的配置系统、工具集成和特殊功能。


    第一部分:导入依赖(第 1-30 行)

    // 从官方沙箱包导入类型和基础类
    import type { ...各种配置类型 } from '@anthropic-ai/sandbox-runtime'
    import { 
      SandboxManager as BaseSandboxManager,  // 官方基础沙箱管理器
      SandboxRuntimeConfigSchema,             // 配置校验 Schema
      SandboxViolationStore,                  // 违规记录存储
    } from '@anthropic-ai/sandbox-runtime'

      基础沙箱引擎和类型定义,准备在此基础上加工。

    import { rmSync, statSync } from 'fs'     // 同步文件操作(删除、状态检查)
    import { readFile } from 'fs/promises'    // 异步读文件
    import { memoize } from 'lodash-es'       // 缓存函数结果(避免重复计算)
    import { join, resolve, sep } from 'path' // 路径处理工具

    文件操作、路径拼接、缓存优化的基础设施。

    // Claude CLI 内部的各种工具函数
    import { getAdditionalDirectoriesForClaudeMd, ... } from '../../bootstrap/state.js'
    import { logForDebugging } from '../debug.js'
    import { expandPath } from '../path.js'
    import { getPlatform, type Platform } from '../platform.js'
    // ... 各种 settings 相关的导入

    其他模块的状态管理、调试日志、平台检测、配置系统等功能,准备和沙箱做整合。


    第二部分:工具名称常量 & 权限规则解析(第 32-58 行)

    import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
    import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
    // ... 其他工具名称

    定义 Claude 里各种工具的名字——Bash 命令、文件编辑、文件读取、网页抓取等,后面要根据这些名字做权限判断。

    // 本地副本避免循环依赖
    function permissionRuleValueFromString(ruleString: string): PermissionRuleValue {
      const matches = ruleString.match(/^([^(]+)\(([^)]+)$/)
      if (!matches) {
        return { toolName: ruleString }  // 没有括号,整个字符串就是工具名
      }
      // 解析出 "工具名(规则内容)" 的格式
      const toolName = matches[1]
      const ruleContent = matches[2]
      return { toolName, ruleContent }
    }

    解析权限规则的字符串格式。比如 WebFetch(domain:example.com) 会被拆成:

    • 工具名:WebFetch

    • 规则内容:domain:example.com

    function permissionRuleExtractPrefix(permissionRule: string): string | null {
      const match = permissionRule.match(/^(.+):\*$/)
      return match?.[1] ?? null  // 提取 "npm run test:*" 中的 "npm run test" 部分
    }

    处理带通配符的规则,把 command:* 里的 command 部分提取出来。


    第三部分:路径解析核心逻辑(第 59-118 行)

    resolvePathPatternForSandbox - 权限规则路径解析

    export function resolvePathPatternForSandbox(pattern: string, source: SettingSource): string {
      // 处理 // 前缀 - 绝对路径(从文件系统根目录开始)
      if (pattern.startsWith('//')) {
        return pattern.slice(1)  // "//.aws/**" → "/.aws/**"
      }
    
      // 处理 / 前缀 - 相对于配置文件所在目录
      // 注意:~/path 和普通相对路径交给官方沙箱处理
      if (pattern.startsWith('/') && !pattern.startsWith('//')) {
        const root = getSettingsRootPathForSource(source)
        return resolve(root, pattern.slice(1))  // "/foo" → "${settings目录}/foo"
      }
    
      // 其他情况原样返回(~/path, ./path, path)
      return pattern
    }

    Claude 的权限规则有特殊的路径语法

    • //path → 真正的绝对路径(从系统根目录开始)

    • /path → 相对配置文件的位置(不是系统根目录)

    • ~/path → 用户主目录

    • ./pathpath → 相对当前工作目录

    关键区别:权限规则里的 /path 不是系统根目录,而是配置文件的目录!这是 Claude 特有的约定。

    resolveSandboxFilesystemPath - 沙箱文件系统路径解析

    export function resolveSandboxFilesystemPath(pattern: string, source: SettingSource): string {
      // 兼容旧写法://path → /path
      if (pattern.startsWith('//')) return pattern.slice(1)
      
      // 使用标准路径扩展(支持 ~ 展开、相对路径解析)
      return expandPath(pattern, getSettingsRootPathForSource(source))
    }

    sandbox.filesystem.* 设置项(如 allowWrite)使用标准路径语义

    • /path = 真正的绝对路径(不像权限规则那样是相对配置目录)

    • ~/path = 展开为用户主目录

    • 相对路径 = 相对于配置目录

    这是修复 #30067 的关键:之前用户写 /Users/foo/.cargo 被错误当成相对路径,现在修复了,按绝对路径处理。


    第四部分:托管域名/路径检查(第 119-140 行)

    export function shouldAllowManagedSandboxDomainsOnly(): boolean {
      return (
        getSettingsForSource('policySettings')?.sandbox?.network?.allowManagedDomainsOnly === true
      )
    }
    
    function shouldAllowManagedReadPathsOnly(): boolean {
      return (
        getSettingsForSource('policySettings')?.sandbox?.filesystem?.allowManagedReadPathsOnly === true
      )
    }

    检查是否只允许"托管"的域名或读取路径。policySettings 是企业策略配置,优先级最高,IT 管理员可以用这个锁定沙箱行为。


    第五部分:配置转换器(第 141-350 行)——核心逻辑

    convertToSandboxRuntimeConfig - 把 Claude 配置转成官方沙箱配置

    export function convertToSandboxRuntimeConfig(settings: SettingsJson): SandboxRuntimeConfig {
      const permissions = settings.permissions || {}

    负责把 Claude 的各种零散配置翻译成官方沙箱能理解的统一格式。

    网络域名处理(第 148-185 行)
      const allowedDomains: string[] = []
      const deniedDomains: string[] = []
    
      // 如果启用"仅托管域名",只从 policySettings 读取
      if (shouldAllowManagedSandboxDomainsOnly()) {
        const policySettings = getSettingsForSource('policySettings')
        for (const domain of policySettings?.sandbox?.network?.allowedDomains || []) {
          allowedDomains.push(domain)
        }
        // 也从 policySettings 的 permissions.allow 中提取 WebFetch 规则
        for (const ruleString of policySettings?.permissions?.allow || []) {
          const rule = permissionRuleValueFromString(ruleString)
          if (rule.toolName === WEB_FETCH_TOOL_NAME && rule.ruleContent?.startsWith('domain:')) {
            allowedDomains.push(rule.ruleContent.substring('domain:'.length))
          }
        }
      } else {
        // 普通模式:从用户设置读取
        for (const domain of settings.sandbox?.network?.allowedDomains || []) {
          allowedDomains.push(domain)
        }
        // 从 permissions.allow 提取 WebFetch 域名规则
        for (const ruleString of permissions.allow || []) {
          // ... 同上逻辑
        }
      }
    • 企业锁定模式:只允许 IT 管理员在 policySettings 里指定的域名,用户自己设的不管用

    • 普通模式:用户自己配置的白名单域名 + 权限规则里的 WebFetch(domain:xxx) 都有效

      // 处理拒绝域名(deny 列表)
      for (const ruleString of permissions.deny || []) {
        const rule = permissionRuleValueFromString(ruleString)
        if (rule.toolName === WEB_FETCH_TOOL_NAME && rule.ruleContent?.startsWith('domain:')) {
          deniedDomains.push(rule.ruleContent.substring('domain:'.length))
        }
      }

    黑名单逻辑——permissions.deny 里的 WebFetch(domain:evil.com) 会被提取出来,禁止访问这些域名。

    文件系统路径处理(第 186-300 行)
      // 基础可写路径:当前目录 + Claude 临时目录
      const allowWrite: string[] = ['.', getClaudeTempDir()]
      const denyWrite: string[] = []
      const denyRead: string[] = []
      const allowRead: string[] = []

    默认情况下,沙箱里的命令可以读写当前工作目录Claude 的临时文件夹(用来放 cwd 跟踪文件等)。

    安全加固 - 禁止写入配置文件(第 196-210 行)

      // 永远禁止写入 settings.json 文件,防止沙箱逃逸
      const settingsPaths = SETTING_SOURCES.map(source =>
        getSettingsFilePathForSource(source)
      ).filter((p): p is string => p !== undefined)
      denyWrite.push(...settingsPaths)  // 所有来源的配置文件路径
      denyWrite.push(getManagedSettingsDropInDir())  // 托管配置目录

     沙箱里的命令绝对不能改 Claude 的配置文件,否则可能逃逸沙箱控制。把所有 settings.json 的路径都加入黑名单。

      // 如果当前目录和原始启动目录不同,也禁止写入当前目录的配置文件
      const cwd = getCwdState()
      const originalCwd = getOriginalCwd()
      if (cwd !== originalCwd) {
        denyWrite.push(resolve(cwd, '.claude', 'settings.json'))
        denyWrite.push(resolve(cwd, '.claude', 'settings.local.json'))
      }

    用户可能用 cd 切换了目录,新目录下的 Claude 配置也要保护起来。

    保护 Skills 目录(第 214-222 行)

      // 禁止写入 .claude/skills 目录(和 commands/agents 同等权限)
      denyWrite.push(resolve(originalCwd, '.claude', 'skills'))
      if (cwd !== originalCwd) {
        denyWrite.push(resolve(cwd, '.claude', 'skills'))
      }

    Skills(技能文件)和 Commands/Agents 一样敏感,都是自动加载的高权限代码,必须保护。

    Git 裸仓库攻击防护(第 224-250 行)——高级安全逻辑

      // 安全漏洞修复:Git 的 is_git_directory() 会把包含 HEAD+objects+refs 的目录
      // 当成裸仓库。攻击者可以植入这些文件 + 带 core.fsmonitor 的配置来逃逸沙箱。
      
      bareGitRepoScrubPaths.length = 0  // 清空待清理列表
      const bareGitRepoFiles = ['HEAD', 'objects', 'refs', 'hooks', 'config']
      
      for (const dir of cwd === originalCwd ? [originalCwd] : [originalCwd, cwd]) {
        for (const gitFile of bareGitRepoFiles) {
          const p = resolve(dir, gitFile)
          try {
            statSync(p)  // 检查文件是否存在
            denyWrite.push(p)  // 存在就禁止写入(只读绑定)
          } catch {
            // 不存在就加入清理列表,命令执行后删除
            bareGitRepoScrubPaths.push(p)
          }
        }
      }

    防御特定攻击向量——

    1. 攻击者在当前目录放 HEADobjects/refs/ 等文件

    2. Git 会误以为这是裸仓库(bare repo)

    3. 配合恶意 core.fsmonitor 配置,可能让 Claude 的非沙箱化 Git 操作逃逸

    对策

    • 如果文件已存在 → 沙箱里只读挂载(denyWrite

    • 如果文件不存在 → 沙箱命令执行后立即删除scrubBareGitRepoFiles),防止非沙箱化 Git 看到

    Git Worktree 支持(第 252-258 行)

      // 如果在 git worktree 中,需要允许写入主仓库的 .git 目录(用于 index.lock 等)
      if (worktreeMainRepoPath && worktreeMainRepoPath !== cwd) {
        allowWrite.push(worktreeMainRepoPath)
      }

    Git Worktree 模式下,工作目录和 .git 目录是分开的,需要特别允许写入主仓库的 .git 目录,否则 Git 操作会失败。

    额外目录处理(第 260-268 行)

      // 处理 --add-dir CLI 参数或 /add-dir 命令添加的目录
      const additionalDirs = new Set([
        ...(settings.permissions?.additionalDirectories || []),
        ...getAdditionalDirectoriesForClaudeMd(),
      ])
      allowWrite.push(...additionalDirs)

    用户通过命令行 --add-dir/add-dir 命令额外添加的目录,也要加入白名单,让沙箱内的 Bash 命令能访问。

    遍历所有配置来源解析权限规则(第 270-300 行)

      for (const source of SETTING_SOURCES) {
        const sourceSettings = getSettingsForSource(source)
    
        // 从 permissions.allow/deny 提取文件操作规则
        if (sourceSettings?.permissions) {
          for (const ruleString of sourceSettings.permissions.allow || []) {
            const rule = permissionRuleValueFromString(ruleString)
            if (rule.toolName === FILE_EDIT_TOOL_NAME && rule.ruleContent) {
              allowWrite.push(resolvePathPatternForSandbox(rule.ruleContent, source))
            }
          }
          // ... deny 规则同理
        }
    
        // 从 sandbox.filesystem.* 提取路径(使用标准路径语义)
        const fs = sourceSettings?.sandbox?.filesystem
        if (fs) {
          for (const p of fs.allowWrite || []) {
            allowWrite.push(resolveSandboxFilesystemPath(p, source))
          }
          // ... denyWrite, denyRead, allowRead 同理
        }
      }

    Claude 的配置是分层的(local、project、policy 等),要遍历每个来源:

    1. permissions.allow/deny 提取 EditRead 规则(用权限规则路径语法)

    2. sandbox.filesystem.* 提取路径(用标准路径语法)

    注意:如果启用了 allowManagedReadPathsOnly,只有 policySettingsallowRead 才有效。

    Ripgrep 配置(第 301-308 行)
      const { rgPath, rgArgs, argv0 } = ripgrepCommand()
      const ripgrepConfig = settings.sandbox?.ripgrep ?? {
        command: rgPath,
        args: rgArgs,
        argv0,
      }

    沙箱内搜索代码需要 ripgrep,优先用用户配置,否则用 Claude 自带的。

    返回完整配置(第 310-350 行)
      return {
        network: { allowedDomains, deniedDomains, ...各种网络选项 },
        filesystem: { denyRead, allowRead, allowWrite, denyWrite },
        ignoreViolations: settings.sandbox?.ignoreViolations,
        enableWeakerNestedSandbox: settings.sandbox?.enableWeakerNestedSandbox,
        enableWeakerNetworkIsolation: settings.sandbox?.enableWeakerNetworkIsolation,
        ripgrep: ripgrepConfig,
      }
    }

    把所有收集到的配置打包成官方沙箱能理解的格式,包括网络、文件系统、违规处理、嵌套沙箱、网络隔离、搜索工具等。


    第六部分:状态管理与初始化(第 351-600 行)

    关键状态变量

    let initializationPromise: Promise<void> | undefined  // 初始化状态锁
    let settingsSubscriptionCleanup: (() => void) | undefined  // 配置变更订阅清理函数
    let worktreeMainRepoPath: string | null | undefined  // Git Worktree 主仓库路径缓存
    const bareGitRepoScrubPaths: string[] = []  // 待清理的裸仓库文件路径

    这些变量记住沙箱的各种状态——是否在初始化、如何清理监听、Git 仓库位置、需要清理的攻击文件等。

    清理裸仓库文件(第 368-380 行)

    function scrubBareGitRepoFiles(): void {
      for (const p of bareGitRepoScrubPaths) {
        try {
          rmSync(p, { recursive: true })  // 同步删除
          logForDebugging(`[Sandbox] scrubbed planted bare-repo file: ${p}`)
        } catch {
          // 文件不存在是正常的(大部分情况下都没被植入)
        }
      }
    }

    每次沙箱命令执行后,检查并删除可能被植入的裸仓库文件,防止后续非沙箱化操作被攻击。

    检测 Git Worktree(第 382-408 行)

    async function detectWorktreeMainRepoPath(cwd: string): Promise<string | null> {
      const gitPath = join(cwd, '.git')
      try {
        const gitContent = await readFile(gitPath, { encoding: 'utf8' })
        // .git 文件内容格式:gitdir: /path/to/main/repo/.git/worktrees/name
        const gitdirMatch = gitContent.match(/^gitdir:\s*(.+)$/m)
        if (!gitdirMatch?.[1]) return null
        
        const gitdir = resolve(cwd, gitdirMatch[1].trim())
        // 从路径中提取主仓库目录:/.git/worktrees/ 之前的部分
        const marker = `${sep}.git${sep}worktrees${sep}`
        const markerIndex = gitdir.lastIndexOf(marker)
        if (markerIndex > 0) {
          return gitdir.substring(0, markerIndex)
        }
        return null
      } catch {
        // .git 是目录(普通仓库)或无法读取,说明不是 worktree
        return null
      }
    }

    检测是否在 Git Worktree 中——Worktree 的 .git文件而不是目录,里面指向主仓库的位置。解析这个路径,让沙箱知道主仓库在哪里。

    依赖检查与平台支持(第 410-440 行)

    const checkDependencies = memoize((): SandboxDependencyCheck => {
      const { rgPath, rgArgs } = ripgrepCommand()
      return BaseSandboxManager.checkDependencies({ command: rgPath, args: rgArgs })
    })
    
    const isSupportedPlatform = memoize((): boolean => {
      return BaseSandboxManager.isSupportedPlatform()  // 支持 macOS、Linux、WSL2+
    })

    memoize 缓存结果,避免重复检查。检查系统是否支持沙箱(需要 bubblewrap、socat 等工具),以及平台是否兼容(WSL1 不支持)。

    各种配置读取函数(第 442-500 行)

    function getSandboxEnabledSetting(): boolean {
      try {
        const settings = getSettings_DEPRECATED()
        return settings?.sandbox?.enabled ?? false  // 默认关闭
      } catch (error) {
        return false  // 出错时保守处理,默认关闭
      }
    }
    
    function isAutoAllowBashIfSandboxedEnabled(): boolean {
      const settings = getSettings_DEPRECATED()
      return settings?.sandbox?.autoAllowBashIfSandboxed ?? true  // 默认开启
    }
    
    // ... 其他类似的配置读取函数

    沙箱是否启用、是否自动允许沙箱化的 Bash、是否允许非沙箱命令、是否强制要求沙箱等。都有安全默认值。

    平台白名单检查(第 501-530 行)

    function isPlatformInEnabledList(): boolean {
      try {
        const settings = getInitialSettings()
        const enabledPlatforms = settings?.sandbox?.enabledPlatforms
    
        if (enabledPlatforms === undefined) return true  // 未设置=全部允许
        if (enabledPlatforms.length === 0) return false    // 空数组=全部禁止
    
        const currentPlatform = getPlatform()
        return enabledPlatforms.includes(currentPlatform)  // 检查当前平台是否在白名单
      } catch (error) {
        return true  // 出错时默认允许
      }
    }

    NVIDIA 提出的需求:只想在 macOS 上启用沙箱(因为 Linux/WSL 支持较新),可以设置 enabledPlatforms: ["macos"] 来限制。

    沙箱启用综合判断(第 531-545 行)

    function isSandboxingEnabled(): boolean {
      if (!isSupportedPlatform()) return false  // 平台不支持
      if (checkDependencies().errors.length > 0) return false  // 依赖缺失
      if (!isPlatformInEnabledList()) return false  // 平台不在白名单
      return getSandboxEnabledSetting()  // 用户是否显式开启
    }

    沙箱能不能用,平台支持、依赖齐全、平台白名单、用户开启。缺一不可。

    沙箱不可用原因诊断(第 546-580 行)——重要安全功能

    function getSandboxUnavailableReason(): string | undefined {
      // 只有用户显式开启沙箱时才报告问题(避免对未使用沙箱的用户造成噪音)
      if (!getSandboxEnabledSetting()) return undefined
    
      if (!isSupportedPlatform()) {
        const platform = getPlatform()
        if (platform === 'wsl') {
          return 'sandbox.enabled is set but WSL1 is not supported (requires WSL2)'
        }
        return `sandbox.enabled is set but ${platform} is not supported`
      }
    
      if (!isPlatformInEnabledList()) {
        return `sandbox.enabled is set but ${getPlatform()} is not in sandbox.enabledPlatforms`
      }
    
      const deps = checkDependencies()
      if (deps.errors.length > 0) {
        const hint = platform === 'macos' 
          ? 'run /sandbox or /doctor for details'
          : 'install missing tools (e.g. apt install bubblewrap socat)'
        return `sandbox.enabled is set but dependencies are missing: ${deps.errors.join(', ')} · ${hint}`
      }
    
      return undefined  // 一切正常
    }

    修复 #34044 的关键——之前用户开了沙箱但依赖缺失时,系统静默失败(返回 false),用户以为自己在受保护的环境中,实际上完全没有!现在会明确告诉用户:"你开了沙箱但它跑不起来,原因是..."

    Linux 通配符警告(第 581-620 行)

    function getLinuxGlobPatternWarnings(): string[] {
      const platform = getPlatform()
      if (platform !== 'linux' && platform !== 'wsl') return []  // 只有 Linux/WSL 需要警告
    
      // 检查权限规则中的通配符(Linux 的 bubblewrap 不完全支持)
      const hasGlobs = (path: string): boolean => {
        const stripped = path.replace(/\/\*\*$/, '')  // 去掉末尾的 /**
        return /[*?[\]]/.test(stripped)  // 检查是否有 * ? [ ] 等通配符
      }
    
      // 遍历所有 allow/deny 规则,收集带通配符的文件操作规则
      for (const ruleString of [...(permissions.allow || []), ...(permissions.deny || [])]) {
        const rule = permissionRuleValueFromString(ruleString)
        if ((rule.toolName === FILE_EDIT_TOOL_NAME || rule.toolName === FILE_READ_TOOL_NAME) 
            && rule.ruleContent && hasGlobs(rule.ruleContent)) {
          warnings.push(ruleString)
        }
      }
    }

    平台差异警告——Linux/WSL 使用 bubblewrap 做沙箱,它对通配符(globs)的支持不如 macOS 完整。如果用户配置了 Edit(/foo/bar_*.txt) 这样的规则,在 Linux 上可能行为不一致,需要提醒用户。

    策略锁定检查(第 621-640 行)

    function areSandboxSettingsLockedByPolicy(): boolean {
      // 检查 flagSettings 或 policySettings 是否设置了沙箱相关配置
      const overridingSources = ['flagSettings', 'policySettings'] as const
      for (const source of overridingSources) {
        const settings = getSettingsForSource(source)
        if (settings?.sandbox?.enabled !== undefined ||
            settings?.sandbox?.autoAllowBashIfSandboxed !== undefined ||
            settings?.sandbox?.allowUnsandboxedCommands !== undefined) {
          return true  // 被高优先级配置锁定了
        }
      }
      return false
    }

    检查沙箱设置是否被企业策略命令行标志锁定。如果被锁定,用户在本地 settings.json 的修改不会生效,需要告知用户。

    设置沙箱配置(第 641-665 行)

    async function setSandboxSettings(options: { enabled?, autoAllowBashIfSandboxed?, allowUnsandboxedCommands? }): Promise<void> {
      const existingSettings = getSettingsForSource('localSettings')
      
      updateSettingsForSource('localSettings', {
        sandbox: {
          ...existingSettings?.sandbox,
          ...(options.enabled !== undefined && { enabled: options.enabled }),
          // ... 其他选项
        }
      })
    }

    提供接口让用户(或命令)修改沙箱设置,保存在本地配置中。


    第七部分:核心功能函数(第 666-780 行)

    wrapWithSandbox - 包装命令(第 676-690 行)

    async function wrapWithSandbox(
      command: string,
      binShell?: string,
      customConfig?: Partial<SandboxRuntimeConfig>,
      abortSignal?: AbortSignal,
    ): Promise<string> {
      // 如果沙箱启用,确保初始化完成
      if (isSandboxingEnabled()) {
        if (initializationPromise) {
          await initializationPromise  // 等待初始化完成
        } else {
          throw new Error('Sandbox failed to initialize.')
        }
      }
    
      return BaseSandboxManager.wrapWithSandbox(command, binShell, customConfig, abortSignal)
    }

    核心包装函数——把普通 Shell 命令包装成沙箱内运行的命令。如果沙箱还没初始化好,会等待;如果初始化失败,报错。

    initialize - 初始化沙箱(第 692-760 行)

    async function initialize(sandboxAskCallback?: SandboxAskCallback): Promise<void> {
      if (initializationPromise) return initializationPromise  // 已在初始化或完成
      if (!isSandboxingEnabled()) return  // 沙箱未启用,跳过
    
      // 包装回调,强制执行 allowManagedDomainsOnly 策略
      const wrappedCallback = sandboxAskCallback 
        ? async (hostPattern) => {
            if (shouldAllowManagedSandboxDomainsOnly()) {
              logForDebugging(`[sandbox] Blocked network request to ${hostPattern.host} (allowManagedDomainsOnly)`)
              return false  // 企业模式下直接拒绝,不询问
            }
            return sandboxAskCallback(hostPattern)  // 正常询问用户
          }
        : undefined
    
      // 同步创建 Promise,防止竞态条件
      initializationPromise = (async () => {
        try {
          // 预检测 Git Worktree(只检测一次,缓存结果)
          if (worktreeMainRepoPath === undefined) {
            worktreeMainRepoPath = await detectWorktreeMainRepoPath(getCwdState())
          }
    
          const settings = getSettings_DEPRECATED()
          const runtimeConfig = convertToSandboxRuntimeConfig(settings)
    
          // 初始化官方沙箱管理器(macOS 自动启用日志监控)
          await BaseSandboxManager.initialize(runtimeConfig, wrappedCallback)
    
          // 订阅配置变更,动态更新沙箱配置
          settingsSubscriptionCleanup = settingsChangeDetector.subscribe(() => {
            const settings = getSettings_DEPRECATED()
            const newConfig = convertToSandboxRuntimeConfig(settings)
            BaseSandboxManager.updateConfig(newConfig)
            logForDebugging('Sandbox configuration updated from settings change')
          })
        } catch (error) {
          initializationPromise = undefined  // 重置,允许重试
          logForDebugging(`Failed to initialize sandbox: ${errorMessage(error)}`)
        }
      })()
    
      return initializationPromise
    }

    初始化中枢——

    1. 防止重复初始化

    2. 企业模式下拦截所有网络询问

    3. 预检测 Git Worktree 状态

    4. 转换配置并启动官方沙箱

    5. 监听配置变更,实时更新沙箱规则(无需重启)

    其他辅助函数(第 762-780 行)

    function refreshConfig(): void {
      if (!isSandboxingEnabled()) return
      const settings = getSettings_DEPRECATED()
      BaseSandboxManager.updateConfig(convertToSandboxRuntimeConfig(settings))
    }
    
    async function reset(): Promise<void> {
      // 清理订阅、重置状态、清空缓存、重置官方沙箱
      settingsSubscriptionCleanup?.()
      settingsSubscriptionCleanup = undefined
      worktreeMainRepoPath = undefined
      bareGitRepoScrubPaths.length = 0
      checkDependencies.cache.clear?.()
      isSupportedPlatform.cache.clear?.()
      initializationPromise = undefined
      return BaseSandboxManager.reset()
    }

    refreshConfig 用于配置变更后手动刷新;reset 用于彻底重置沙箱状态(如测试场景)。


    第八部分:排除命令管理(第 782-830 行)

    export function addToExcludedCommands(command: string, permissionUpdates?): string {
      const existingSettings = getSettingsForSource('localSettings')
      const existingExcludedCommands = existingSettings?.sandbox?.excludedCommands || []
    
      // 从权限建议中提取命令模式(如 "npm run test" 从 "npm run test:*")
      let commandPattern: string = command
      if (permissionUpdates) {
        const bashSuggestions = permissionUpdates.filter(...)
        if (bashSuggestions.length > 0) {
          const firstBashRule = bashSuggestions[0]!.rules.find(...)
          if (firstBashRule?.ruleContent) {
            const prefix = permissionRuleExtractPrefix(firstBashRule.ruleContent)
            commandPattern = prefix || firstBashRule.ruleContent
          }
        }
      }
    
      // 添加到排除列表(如果不存在)
      if (!existingExcludedCommands.includes(commandPattern)) {
        updateSettingsForSource('localSettings', {
          sandbox: {
            ...existingSettings?.sandbox,
            excludedCommands: [...existingExcludedCommands, commandPattern],
          },
        })
      }
    
      return commandPattern
    }

    智能排除——用户说"以后这个命令不用沙箱了",系统会:

    1. 检查权限建议,如果有 Bash 规则(如 Bash(npm run test:*)

    2. 提取通配符前缀(npm run test)作为排除模式

    3. 保存到 excludedCommands 列表,下次运行这个命令时跳过沙箱


    第九部分:导出接口(第 832-930 行)

    export interface ISandboxManager {
      initialize(...): Promise<void>
      isSupportedPlatform(): boolean
      isPlatformInEnabledList(): boolean
      getSandboxUnavailableReason(): string | undefined
      isSandboxingEnabled(): boolean
      // ... 20+ 个其他方法
    }
    
    export const SandboxManager: ISandboxManager = {
      // 自定义实现
      initialize,
      isSandboxingEnabled,
      isSandboxEnabledInSettings: getSandboxEnabledSetting,
      isPlatformInEnabledList,
      getSandboxUnavailableReason,
      // ... 其他自定义方法
    
      // 透传给官方沙箱管理器
      getFsReadConfig: BaseSandboxManager.getFsReadConfig,
      getFsWriteConfig: BaseSandboxManager.getFsWriteConfig,
      getNetworkRestrictionConfig: BaseSandboxManager.getNetworkRestrictionConfig,
      // ... 其他透传方法
    
      // 特殊处理:清理时同时清理裸仓库文件
      cleanupAfterCommand: (): void => {
        BaseSandboxManager.cleanupAfterCommand()
        scrubBareGitRepoFiles()  // 额外的安全清理
      },
    }

    定义完整的沙箱管理接口,实现适配器模式——

    • 自定义方法:Claude CLI 特有的逻辑(配置转换、平台检查、策略锁定等)

    • 透传方法:直接调用官方沙箱的功能(文件系统限制、网络限制等)

    • 增强方法:如 cleanupAfterCommand 在官方清理后追加裸仓库文件清理


    核心沙箱管理器总结:

    维度 说明
    安全加固 多层防护:配置文件保护、Git 裸仓库攻击防护、企业策略锁定、显式不可用报告
    配置融合 把 Claude CLI 的分层配置(local/project/policy/flag)翻译成官方沙箱格式
    平台适配 处理 macOS/Linux/WSL 的差异(通配符支持、依赖检查、平台白名单)
    企业特性 policySettings 托管模式、平台白名单、只读路径限制、域名白名单
    Git 集成 Worktree 检测、裸仓库攻击防护、主仓库写入权限处理
    动态更新 配置变更实时同步到沙箱,无需重启

    沙箱 UI 组件

    这是一个基于 React (Ink) 构建的终端界面沙箱配置系统,包含 5 个核心组件

    SandboxSettings (主入口)
    ├── SandboxModeTab      - 沙箱模式选择
    ├── SandboxOverridesTab   - 覆盖策略配置  
    ├── SandboxConfigTab      - 沙箱配置展示
    ├── SandboxDependenciesTab - 依赖检查
    └── SandboxDoctorSection  - 诊断摘要

    概括如下,具体看源码不再赘述。

    SandboxSettings 作为主入口提供三态模式管理(auto-allow 自动回退、regular 严格沙箱、disabled 完全禁用),并根据依赖状态动态调整 Tab 展示(有错误时仅显示 Dependencies,有警告时全量展示,正常时显示 Mode+Overrides+Config);SandboxModeTab 负责模式选择与 Unix Socket 警告提示;SandboxOverridesTab 处理覆盖策略配置,支持 open(允许非沙箱回退)和 closed(严格模式)两种状态,并处理未启用、策略锁定等边界情况;SandboxConfigTab 以只读方式展示当前沙箱的完整配置,包括排除命令、文件系统读写限制、网络限制、Unix Socket 白名单及 Linux Glob 模式兼容性警告;SandboxDependenciesTab 实现平台差异化依赖检查,macOS 需 seatbelt+ripgrep,Linux 需 bubblewrap+socat+ripgrep+seccomp(可选),并提供对应安装命令;SandboxDoctorSection 作为诊断摘要入口,仅在支持平台且启用时展示健康状态,汇总错误警告并引导用户执行 /sandbox 命令获取帮助。

    沙箱命令和类型

    命令入口文件(sandbox-toggle.tsx),实现了 /sandbox 命令的逻辑,包含平台检测、依赖检查、权限验证和子命令处理四个核心环节。

    1. 依赖导入

    import { relative } from 'path';
    import React from 'react';
    import { getCwdState } from '../../bootstrap/state.js';
    import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js';
    import { color } from '../../ink.js';                    // 终端颜色输出
    import { getPlatform } from '../../utils/platform.js';
    import { addToExcludedCommands, SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
    import { getSettings_DEPRECATED, getSettingsFilePathForSource } from '../../utils/settings/settings.js';
    import type { ThemeName } from '../../utils/theme.js';

    使用了 Ink(React for CLI)框架,说明这是一个命令行交互式应用。


    2. 主入口函数

    export async function call(
      onDone: (result?: string) => void,    // 完成回调
      _context: unknown,                    // 未使用的上下文
      args?: string,                        // 命令参数
    ): Promise<React.ReactNode | null>

    3. 四层防御性检查(关键逻辑)

    检查层级 函数 说明
    平台支持 isSupportedPlatform() 拒绝 WSL1,仅支持 macOS/Linux/WSL2
    企业策略 isPlatformInEnabledList() 通过 enabledPlatforms 配置白名单控制
    权限锁定 areSandboxSettingsLockedByPolicy() 高优先级配置锁定本地修改
    依赖状态 checkDependencies() 检查沙箱依赖(如 Docker、nsjail 等)
    // WSL1 特殊提示
    const errorMessage = platform === 'wsl' 
      ? 'Error: Sandboxing requires WSL2. WSL1 is not supported.'
      : 'Error: Sandboxing is currently only supported on macOS, Linux, and WSL2.';

    4. 交互模式 vs 命令模式

    const trimmedArgs = args?.trim() || '';
    
    // 无参数 → 启动交互式 TUI 菜单
    if (!trimmedArgs) {
      return <SandboxSettings onComplete={onDone} depCheck={depCheck} />;
    }
    
    // 有参数 → 解析子命令
    const parts = trimmedArgs.split(' ');
    const subcommand = parts[0];

    5. 子命令实现:exclude

    目前唯一实现的子命令,用于将特定命令模式加入沙箱排除列表

    if (subcommand === 'exclude') {
      // 提取命令模式(支持引号包裹)
      const commandPattern = trimmedArgs.slice('exclude '.length).trim();
      const cleanPattern = commandPattern.replace(/^["']|["']$/g, '');  // 去引号
      
      // 写入本地配置
      addToExcludedCommands(cleanPattern);
      
      // 生成相对路径反馈(提升 UX)
      const localSettingsPath = getSettingsFilePathForSource('localSettings');
      const relativePath = localSettingsPath 
        ? relative(getCwdState(), localSettingsPath) 
        : '.claude/settings.local.json';
    }

    使用示例:

    /sandbox exclude "npm run test:*"
    /sandbox exclude 'git push origin main'

    6. 配置层级设计

    从代码中可推断配置优先级(高 → 低):

    企业策略配置 (locked by policy) 
        ↓
    enabledPlatforms 白名单
        ↓
    本地设置 (.claude/settings.local.json) ← exclude 命令写入这里
        ↓
    用户级设置

    sandboxTypes.ts

    src/entrypoints/sandboxTypes.ts 是一个 Claude Code Agent SDK 的沙箱配置类型定义文件,使用了 Zod v4 进行运行时类型校验。

    SandboxSettings (主配置)
    ├── Network 配置 (网络访问控制)
    ├── Filesystem 配置 (文件系统权限)
    └── 其他安全/行为开关

    1. 网络配置 (SandboxNetworkConfig)

    字段 用途
    allowedDomains 允许访问的域名白名单
    allowManagedDomainsOnly 强制仅使用托管设置中的域名(忽略用户/项目级配置)
    allowUnixSockets / allowAllUnixSockets Unix Socket 访问控制(macOS 专用)
    allowLocalBinding 是否允许本地端口绑定
    httpProxyPort / socksProxyPort 代理端口配置

    2. 文件系统配置 (SandboxFilesystemConfig)

    字段 用途
    allowWrite / denyWrite 写入权限的黑白名单
    allowRead / denyRead 读取权限的黑白名单
    allowManagedReadPathsOnly 强制仅使用托管策略中的读取路径

    注意allowReaddenyRead 区域内具有更高优先级(可覆盖拒绝规则)。


    3. 核心安全开关

    开关 说明
    enabled 总开关
    failIfUnavailable 沙箱启动失败时是否直接退出(而非降级到无沙箱运行)
    autoAllowBashIfSandboxed 沙箱启用时自动允许 bash 执行
    allowUnsandboxedCommands 是否允许通过 dangerouslyDisableSandbox 参数绕过沙箱
    enableWeakerNestedSandbox 允许较弱的嵌套沙箱
    enableWeakerNetworkIsolation macOS 专用:允许访问 trustd 服务(降低安全性,用于 Go 工具链的 TLS 证书验证)

    4. 特殊配置

    • excludedCommands:排除特定命令的沙箱限制

    • ripgrep:自定义 ripgrep 命令路径和参数

    • ignoreViolations:按命令记录可忽略的违规类型(Record<string, string[]>

    shouldUseSandbox.ts

    src/tools/BashTool/shouldUseSandbox.ts是一个关于 Bash 命令沙箱(Sandbox)执行策略 的 TypeScript 模块,主要功能是决定是否对用户的命令启用沙箱隔离。

    这个模块决定了何时将 bash 命令放入沙箱执行,何时允许直接执行。它包含两个主要部分:

    1. containsExcludedCommand - 检查命令是否被用户排除在沙箱之外

    2. shouldUseSandbox - 综合判断是否启用沙箱

    1. 动态命令禁用(仅针对 'ant' 用户类型)

    if (process.env.USER_TYPE === 'ant') {
      const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE(...)
      // 检查禁用子字符串和禁用命令
    }
    • 通过 GrowthBook 获取动态配置

    • 可以禁用特定子字符串或完整命令

    • 仅对 USER_TYPE=ant 的用户生效(可能是内部测试或特定用户群体)

    2. 用户配置的排除命令(主要逻辑)

    const settings = getSettings_DEPRECATED()
    const userExcludedCommands = settings.sandbox?.excludedCommands ?? []

    用户可以在设置中配置不想放入沙箱的命令列表。

    3. 命令解析与匹配策略

    关键设计:复合命令拆分

    subcommands = splitCommand_DEPRECATED(command)

    docker ps && curl evil.com 拆分成 ["docker ps", "curl evil.com"] 分别检查,防止通过复合命令绕过沙箱

    候选命令生成(迭代剥离)

    const candidates = [trimmed]
    // 迭代剥离环境变量前缀和安全包装器
    while (startIdx < candidates.length) {
      const envStripped = stripAllLeadingEnvVars(cmd, BINARY_HIJACK_VARS)
      const wrapperStripped = stripSafeWrappers(cmd)
    }

    这个循环会生成所有可能的命令变体:

    • timeout 300 FOO=bar bazel runbazel run

    • 剥离环境变量(如 FOO=bar

    • 剥离安全包装器(如 timeout 30

    匹配规则类型

    switch (rule.type) {
      case 'prefix':    // 前缀匹配,如 `bazel:*` 匹配所有 bazel 命令
      case 'exact':     // 精确匹配
      case 'wildcard':  // 通配符匹配
    }

    4. 沙箱启用决策(shouldUseSandbox

    export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
      if (!SandboxManager.isSandboxingEnabled()) return false  // 全局关闭
      if (input.dangerouslyDisableSandbox && SandboxManager.areUnsandboxedCommandsAllowed()) return false  // 显式禁用
      if (!input.command) return false  // 无命令
      if (containsExcludedCommand(input.command)) return false  // 用户排除
      return true  // 启用沙箱
    }

    安全设计要点

    设计 说明
    复合命令拆分 防止 safe_cmd && evil_cmd 绕过
    迭代剥离 处理 timeout 30 FOO=bar evil_cmd 多层包装
    候选命令集合 生成所有可能的命令变体进行匹配
    明确的非安全边界注释 excludedCommands 是便利功能,真正的安全边界是权限提示系统

    Logo

    欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

    更多推荐