乐于分享
好东西不私藏

CC 源码学习(2):安全设计与安全防范

CC 源码学习(2):安全设计与安全防范

看到 Claude Code 这类高权限 Agent 工具时,人们关心的除了它能做什么,还有它会不会失控:

  • 它居然能执行 Shell,那怎么防止乱跑命令?
  • 它能读写文件,那怎么防止误改或越界?
  • 它还支持插件、MCP、远程配置,这些不都是风险入口吗?
  • 如果模型本身不稳定,项目层到底做了什么来兜底?

这些问题本质上都在问一件事:

一个高权限 Agent 工具,如何在“能力很强”的同时,尽量保持“可控”?

Claude Code 的答案不是靠单点防御,而是用了比较典型的分层防护思路:

  1. 默认不直接信任输入
  2. 高风险能力必须通过统一工具层暴露
  3. 工具执行前进入权限判断与审批链路
  4. 文件、Shell、网络等关键资源再做专门限制
  5. 对配置、插件、MCP、受管设置增加额外治理
  6. 在运行时引入 sandbox、策略、托管配置等“硬边界”

所以这一篇我们不只是看“有没有权限弹窗”,而是看它如何把安全塞进主链路。

先说结论:Claude Code 的安全不是外挂,而是默认执行路径

在很多 demo 级 Agent 项目里,主链路通常是:

用户输入 -> 模型 -> 工具执行 -> 返回结果

但在 Claude Code 里,更接近:

用户输入 -> 命令/提示词处理 -> 模型生成 tool_use -> 权限规则判断 -> 分类器/Hook/策略补充判断 -> 用户审批或自动拒绝 -> 沙箱/路径/网络等运行时限制 -> 工具真正执行 -> 结果回流模型

也就是说,安全相关逻辑不是“出问题后再拦”,而是在执行前就进入链路。

这点很重要,因为真正危险的不是“模型会不会犯错”,而是“模型犯错时系统有没有足够多层的刹车”。

第一层:统一工具入口,先把能力收口

Claude Code 的第一个安全前提,不是在 permissions/ 目录里,而是在整个工具架构本身。

前一篇提到过,模型不是直接操作外部世界,而是通过 src/tools.ts 注册出来的工具集合行动。真正的能力暴露要先经过:

  • 工具注册
  • 工具 schema
  • 工具执行上下文
  • 工具权限上下文

这部分核心落在:

  • src/tools.ts
  • src/Tool.ts

这件事为什么重要?

因为只要能力是“统一工具化”的,系统就可以在同一个拦截点上统一处理:

  • 权限审批
  • 输入校验
  • 执行前说明
  • 运行状态展示
  • 审计与日志

如果能力是散落的内部函数调用,就很难建立一致的安全边界。

换句话说,Claude Code 的安全能力,首先建立在“架构可治理”这件事上。

第二层:权限系统是工具执行前的总闸门

最关键的权限核心之一在 src/utils/permissions/permissions.ts

这个文件可以看成是“工具调用决策器”,它会基于上下文决定某个操作到底应该:

  • allow
  • deny
  • ask

而且它不是简单写死几条规则,而是组合了多种来源:

  • 设置文件里的 allow / deny / ask 规则
  • policySettings 这类托管策略
  • 会话级规则
  • 命令或 CLI 注入的规则
  • hook 返回结果
  • 分类器结果
  • 当前 permission mode

这意味着 Claude Code 的权限判断不是“单一 if/else”,而是一套可叠加、可来源分层、可解释的决策系统。

为什么这层设计值得注意

因为真实产品里的权限判断很少只有一种来源。

举个例子,同一个操作可能同时受到:

  • 用户个人设置
  • 项目级设置
  • 企业托管策略
  • 当前会话模式
  • 自动审批系统

的共同影响。

Claude Code 在 permissions.ts 里做的,就是把这些异构来源收敛成统一决策结果,并且还能生成解释信息,比如:

  • 是哪条规则触发的
  • 是哪种 mode 导致需要审批
  • 是 hook 拦截了
  • 还是 classifier 判定有风险

这说明它不仅在“拦”,还在努力把“为什么拦”讲清楚。

一个最小可用版的权限总闸门,大概长这样

type PermissionBehavior = "allow" | "deny" | "ask";interface PermissionDecision {  behavior: PermissionBehavior;  reason: string;}interface ToolCall {  toolName: string;  input: unknown;}interface PermissionContext {  allowRules: string[];  denyRules: string[];  autoMode: boolean;  hook?: (call: ToolCall) => Promise<PermissionDecision | null>;  classifier?: (call: ToolCall) => Promise<"safe" | "unsafe" | "unknown">;}function matchRule(rule: string, call: ToolCall): boolean {  if (rule === "*") return true;  if (rule === call.toolName) return true;  return false;}export async function decideToolPermission(  call: ToolCall,  ctx: PermissionContext,): Promise<PermissionDecision> {  // deny 优先  for (const rule of ctx.denyRules) {    if (matchRule(rule, call)) {      return { behavior: "deny", reason: `matched deny rule: ${rule}` };    }  }  // allow 命中直接放行  for (const rule of ctx.allowRules) {    if (matchRule(rule, call)) {      return { behavior: "allow", reason: `matched allow rule: ${rule}` };    }  }  // hook 二次裁决  if (ctx.hook) {    const hookDecision = await ctx.hook(call);    if (hookDecision) return hookDecision;  }  // auto mode 下走分类器  if (ctx.autoMode && ctx.classifier) {    const result = await ctx.classifier(call);    if (result === "unsafe") {      return { behavior: "deny", reason: "blocked by classifier" };    }    if (result === "unknown") {      return { behavior: "ask", reason: "classifier uncertain" };    }    return { behavior: "allow", reason: "classifier marked safe" };  }  // 默认 ask  return { behavior: "ask", reason: "no matching rule" };}

这里最重要的不是 allow / deny / ask 这三个返回值,而是:

所有工具能力都必须先经过同一条决策链,而不是各写各的权限判断。

只有这样,hook、classifier、托管策略、会话模式这些安全能力才能叠加。

第三层:不是所有“允许规则”都真的安全

很多系统做到 allow/deny/ask 就停了,但 Claude Code 还多走了一步:它会识别“看起来是允许,实际上过宽”的规则。

src/utils/permissions/dangerousPatterns.ts 就很有代表性。

这个文件维护了一批危险的 Shell 前缀模式,例如:

  • python
  • node
  • deno
  • bash
  • sh
  • eval
  • exec
  • ssh

为什么这些模式危险?

因为你如果写出类似:

Bash(python:*)

表面上是在“允许 Python 命令”,实际上几乎等于允许任意代码执行。对于自动模式、批量模式、低干预模式来说,这种规则会让权限系统形同虚设。

所以 Claude Code 不是把“用户配了 allow”就盲信,而是会把过于危险的规则识别出来,并在某些模式下剥离或限制。

这个限制写成代码,大概会长这样

const DANGEROUS_EXEC_PREFIXES = [  "python",  "python3",  "node",  "deno",  "tsx",  "ruby",  "perl",  "php",  "lua",  "npx",  "bunx",  "bash",  "sh",  "zsh",  "pwsh",  "powershell",  "cmd /c",  "eval",  "exec",  "ssh",];function normalizeRule(rule: string): string {  return rule.trim().toLowerCase();}function isDangerousShellAllowRule(rule: string): boolean {  const normalized = normalizeRule(rule);  const m =    normalized.match(/^bash\((.+)\)$/) ||    normalized.match(/^powershell\((.+)\)$/);  if (!m) return false;  const inner = m[1].trim();  if (inner === "*" || inner === "*:*") return true;  return DANGEROUS_EXEC_PREFIXES.some((prefix) => {    return (      inner === prefix ||      inner === `${prefix}:*` ||      inner.startsWith(`${prefix} `) ||      inner.startsWith(`${prefix}-`) ||      inner.startsWith(`${prefix}/`)    );  });}export function stripDangerousAllowRules(  allowRules: string[],  mode: "default" | "acceptEdits" | "auto",): string[] {  if (mode !== "auto") return allowRules;  return allowRules.filter((rule) => !isDangerousShellAllowRule(rule));}

这类限制的关键不是“禁止 Bash”,而是防止看似精细的规则,最终退化成“给解释器开了任意代码执行入口”。

这类设计很成熟,因为它承认了一个现实:

配置本身也可能成为风险来源。

第四层:权限判断不只靠静态规则,还结合 Hook 和分类器

在 src/hooks/useCanUseTool.tsx 和 src/utils/permissions/permissions.ts 这条链路里,可以看到 Claude Code 的权限决策不是纯静态的。

它还会结合:

  • hook
  • classifier
  • 不同运行角色下的权限处理器

例如:

  • 交互式场景走 interactiveHandler
  • coordinator / swarm worker 有单独处理逻辑
  • 分类器可能把某个命令标成需要审批

这里体现出一个很关键的安全思想:

静态规则适合兜底,动态判断适合补盲区

因为规则擅长处理的是:

  • 哪个工具允许
  • 哪个路径允许
  • 哪个域名允许

但动态行为中的危险,常常来自“具体这次做了什么”。比如同样是 Bash,命令内容完全可能天差地别。

所以 Claude Code 在主链路里给了分类器和 hook 插口,用来处理:

  • 特定命令语义风险
  • 特定会话态下的补充判断
  • 自动模式下的拒绝逻辑

即使在这个外部泄露版本里,bashClassifier.ts 是一个 stub,也依然能看出架构上已经为这类安全增强预留了明确位置。

这一点很重要,因为说明安全能力不是散落补丁,而是被当成一等公民设计的。

第五层:文件系统访问不是“能读写就行”,而是单独做路径安全校验

Claude Code 对文件系统的防护非常像真实工程系统,而不是单纯“提示一下用户”。

关键可以看:

  • src/utils/permissions/pathValidation.ts
  • src/utils/permissions/filesystem.ts

在路径校验里,它至少考虑了这些问题:

  • 路径是否命中 deny 规则
  • 是否属于允许工作目录
  • 是否属于内部可编辑/可读取路径
  • 是否涉及危险目录或敏感路径
  • 是否存在路径穿越风险
  • 是否需要结合 sandbox 可写白名单
  • Windows / UNC 路径等平台特性是否会带来额外风险

尤其值得注意的是它的处理顺序。

比如在写操作中,它会先检查:

  1. deny 规则
  2. 内部允许编辑路径
  3. 综合安全检查
  4. 工作目录或 allow 规则

这个顺序说明它不是简单的“只要在工作目录下就能写”,而是先挡掉本不该碰的敏感区域。

一个常见的路径安全校验,可以写成这样

import path from "node:path";type FileOp = "read" | "write";interface PathPolicy {  workspaceRoots: string[];  denyPaths: string[];  allowWritePaths: string[];}function isSubPath(target: string, base: string): boolean {  const rel = path.relative(base, target);  return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));}function containsPathTraversal(p: string): boolean {  return p.split(/[\\/]+/).includes("..");}function containsVulnerableUncPath(p: string): boolean {  return /^\\\\[^\\]+\\[^\\]+/.test(p);}export function validatePathAccess(  rawPath: string,  cwd: string,  op: FileOp,  policy: PathPolicy,): { allowed: boolean; resolvedPath: string; reason?: string } {  const clean = rawPath.replace(/^['"]|['"]$/g, "");  const expanded = clean.startsWith("~")    ? clean.replace(/^~(?=$|[\\/])/, process.env.HOME || "")    : clean;  // 1. 先挡 UNC 路径  if (containsVulnerableUncPath(expanded)) {    return {      allowed: false,      resolvedPath: expanded,      reason: "UNC network path requires manual approval",    };  }  // 2. 再挡路径穿越  if (containsPathTraversal(expanded)) {    return {      allowed: false,      resolvedPath: expanded,      reason: "path traversal detected",    };  }  const resolved = path.resolve(cwd, expanded);  // 3. deny 优先  for (const denied of policy.denyPaths) {    if (isSubPath(resolved, denied)) {      return {        allowed: false,        resolvedPath: resolved,        reason: `matched deny path: ${denied}`,      };    }  }  // 4. 必须在工作区内  const inWorkspace = policy.workspaceRoots.some((root) =>    isSubPath(resolved, root),  );  if (!inWorkspace) {    return {      allowed: false,      resolvedPath: resolved,      reason: "path is outside workspace roots",    };  }  // 5. 写操作额外收紧  if (op === "write") {    const canWrite = policy.allowWritePaths.some((root) =>      isSubPath(resolved, root),    );    if (!canWrite) {      return {        allowed: false,        resolvedPath: resolved,        reason: "write path is not in write allowlist",      };    }  }  return { allowed: true, resolvedPath: resolved };}

这背后体现的是典型的越权防护思路:

  • 工作目录不是绝对安全区
  • 可编辑不代表所有文件都可编辑
  • 平台兼容不能牺牲安全判断

对于 Agent 工具来说,这一点尤其重要,因为文件写入通常是最常见、也最容易造成真实破坏的动作。

第六层:Sandbox 不是点缀,而是运行时硬限制

如果说权限审批是“软决策”,那 sandbox 就更接近“硬限制”。

核心文件之一是 src/utils/sandbox/sandbox-adapter.ts

这个文件做的事情非常关键:把 Claude Code 自己的设置体系,转换成 sandbox runtime 能真正执行的约束配置。

它会处理的内容包括:

  • 文件系统读写限制
  • 网络访问限制
  • 允许域名/拒绝域名
  • 允许写入目录 / 拒绝写入目录
  • 配置路径解析和规范化

其中有几个细节非常值得注意。

1)把 WebFetch 权限规则转换成网络白名单

也就是说,权限规则不是只用来“弹窗提示”,还会真正落成沙箱网络策略。

2)默认阻止写入 settings 相关文件

这能避免通过改设置文件完成 sandbox escape 或权限绕过。

3)支持“只允许托管域名”或“只允许托管读路径”

这说明企业/组织策略不是 UI 层建议,而是能下沉到执行边界。

一个最小版的“规则转沙箱配置”,可以这么写

interface Settings {  permissions?: {    allow?: string[];    deny?: string[];  };  sandbox?: {    filesystem?: {      allowRead?: string[];      allowWrite?: string[];      denyWrite?: string[];    };    network?: {      allowedDomains?: string[];      deniedDomains?: string[];    };  };}interface SandboxRuntimeConfig {  filesystem: {    allowRead: string[];    allowWrite: string[];    denyWrite: string[];  };  network: {    allowedDomains: string[];    deniedDomains: string[];  };}function parseWebFetchRule(rule: string): { effect: "allow" | "deny"; domain: string } | null {  const trimmed = rule.trim();  const deny = trimmed.startsWith("!");  const body = deny ? trimmed.slice(1) : trimmed;  const m = body.match(/^WebFetch\(([^)]+)\)$/i);  if (!m) return null;  return {    effect: deny ? "deny" : "allow",    domain: m[1].trim().toLowerCase(),  };}export function toSandboxRuntimeConfig(settings: Settings): SandboxRuntimeConfig {  const allowRead = [...(settings.sandbox?.filesystem?.allowRead ?? [])];  const allowWrite = [...(settings.sandbox?.filesystem?.allowWrite ?? [])];  const denyWrite = [...(settings.sandbox?.filesystem?.denyWrite ?? [])];  const allowedDomains = [...(settings.sandbox?.network?.allowedDomains ?? [])];  const deniedDomains = [...(settings.sandbox?.network?.deniedDomains ?? [])];  for (const rule of settings.permissions?.allow ?? []) {    const parsed = parseWebFetchRule(rule);    if (parsed?.effect === "allow") {      allowedDomains.push(parsed.domain);    }  }  for (const rule of settings.permissions?.deny ?? []) {    const parsed = parseWebFetchRule(rule);    if (parsed?.effect === "deny") {      deniedDomains.push(parsed.domain);    }  }  // 关键:默认禁止改配置文件  denyWrite.push(    ".claude/settings.json",    ".claude/settings.local.json",    ".mcp.json",  );  return {    filesystem: {      allowRead: [...new Set(allowRead)],      allowWrite: [...new Set(allowWrite)],      denyWrite: [...new Set(denyWrite)],    },    network: {      allowedDomains: [...new Set(allowedDomains)],      deniedDomains: [...new Set(deniedDomains)],    },  };}

这类设计非常有现实意义,因为很多情况下用户点了允许,并不代表系统就该完全放开。

真正稳的做法,是让审批和沙箱同时存在,让它们变成双保险。

第七层:配置本身也被当成不可信输入

很多系统在安全设计上容易忽略一个点:

配置文件也是输入,而且可能是危险输入。

Claude Code 在这方面处理得比较认真,相关文件包括:

  • src/utils/settings/validation.ts
  • src/utils/managedEnv.ts

配置校验

validation.ts 用 Zod 对 settings 做严格校验,并且把错误格式化成更易读的提示。

这里的意义不只是“防止 JSON 写错”,更重要的是:

  • 避免无效配置悄悄生效
  • 避免非法字段混入系统
  • 让配置变更具备更高可预测性

环境变量不是随便从任何设置源都能注入

managedEnv.ts 更值得单独说。

它明确区分了两类来源:

  • 可以在 trust dialog 前生效的可信设置源
  • 只能在 trust 建立后才能完整生效的设置源

特别是项目级设置不会在一开始就被无条件信任,因为项目目录里的配置可能来自他人提交,存在把请求导向恶意服务、篡改运行环境的风险。

这个限制很适合用代码讲清楚

type SettingSource =  | "userSettings"  | "flagSettings"  | "policySettings"  | "projectSettings"  | "localSettings";const TRUSTED_SOURCES = new Set<SettingSource>([  "userSettings",  "flagSettings",  "policySettings",]);const SAFE_PROJECT_ENV_KEYS = new Set([  "HTTP_PROXY",  "HTTPS_PROXY",  "NO_PROXY",  "TERM",  "COLORTERM",]);export function applyEnvFromSettings(  source: SettingSource,  env: Record<string, string> | undefined,  trustedEstablished: boolean,) {  if (!env) return;  // trust 建立前:只有可信来源可以完整注入  if (!trustedEstablished) {    if (!TRUSTED_SOURCES.has(source)) return;    for (const [k, v] of Object.entries(env)) {      process.env[k] = v;    }    return;  }  // trust 建立后:项目级来源只允许有限白名单  if (!TRUSTED_SOURCES.has(source)) {    for (const [k, v] of Object.entries(env)) {      if (SAFE_PROJECT_ENV_KEYS.has(k)) {        process.env[k] = v;      }    }    return;  }  // 可信来源正常应用  for (const [k, v] of Object.entries(env)) {    process.env[k] = v;  }}

这个设计说明项目非常清楚:

“本地文件”不等于“可信文件”,尤其是 project-scoped 配置。

这类意识在普通工程项目里常常会被忽略,但在 Agent 产品里非常关键,因为配置可以直接影响:

  • API 请求目的地
  • 身份认证方式
  • 代理与证书行为
  • 子进程环境

第八层:插件系统不是简单“装了就跑”,还做了策略和清单校验

插件和扩展能力天然会放大攻击面,所以 Claude Code 没有把插件当成普通静态资源处理。

相关可以重点看:

  • src/utils/plugins/validatePlugin.ts
  • src/utils/plugins/pluginPolicy.ts

1)插件清单校验

validatePlugin.ts 会对 plugin.jsonmarketplace.json 等内容做解析和校验,重点包括:

  • JSON / YAML 结构是否合法
  • 是否命中 schema
  • 是否存在路径穿越问题
  • 某些字段是否出现在错误位置

尤其是对 .. 这类路径穿越模式的显式检查,非常说明问题。

因为插件最危险的地方之一,就是借“声明式配置”把实际加载路径偷偷带出插件目录。

一个最小版的插件清单校验,可以这样写

interface ValidationError {  path: string;  message: string;}function hasPathTraversal(p: string): boolean {  return p.split(/[\\/]+/).includes("..");}function checkPathTraversal(  value: unknown,  fieldPath: string,  errors: ValidationError[],) {  if (typeof value !== "string") return;  if (hasPathTraversal(value)) {    errors.push({      path: fieldPath,      message: `Path contains ".." and may be a path traversal attempt: ${value}`,    });  }}export function validatePluginManifest(manifest: Record<string, unknown>) {  const errors: ValidationError[] = [];  for (const key of ["commands", "agents", "skills"]) {    const val = manifest[key];    const arr = Array.isArray(val) ? val : val ? [val] : [];    arr.forEach((item, i) => {      checkPathTraversal(item, `${key}[${i}]`, errors);    });  }  return {    success: errors.length === 0,    errors,  };}

2)插件策略控制

pluginPolicy.ts 则体现了另外一层安全能力:组织策略可以直接禁用某些插件。

这意味着插件是否可安装、可启用,不完全由用户个人决定,还会受托管策略约束。

对于企业场景来说,这是非常必要的,因为插件往往意味着:

  • 新命令入口
  • 新工具入口
  • 新 MCP 服务接入
  • 新的外部依赖与执行逻辑

如果没有策略层,插件生态很容易变成安全短板。

第九层:MCP 不是“外接能力越多越好”,而是要做配置治理与去重控制

Claude Code 对 MCP 的处理,也体现出明显的安全与治理意识。

相关入口之一是 src/services/mcp/config.ts

这个模块里可以看到几类很有代表性的处理:

  • 区分不同 scope 的 MCP 配置
  • 结合 settings、plugin、enterprise config 做合并
  • 写 .mcp.json 时采用临时文件 + datasync + 原子 rename
  • 对 server config 做 schema 校验
  • 对插件注入的 MCP server 做去重
  • 支持 enterprise 托管 MCP 配置

为什么这也属于安全设计

因为 MCP 服务本质上是在给系统新增能力和外部连接。

风险主要不在“有没有 MCP”,而在:

  • MCP 从哪里来
  • 配置是否可靠
  • 会不会与已有服务冲突
  • 企业是否能强制管理

一个很值得写进文里的细节,是原子写配置

import { open, rename, unlink, chmod, stat } from "node:fs/promises";export async function atomicWriteJson(  targetPath: string,  data: unknown,): Promise<void> {  let mode: number | undefined;  try {    const s = await stat(targetPath);    mode = s.mode;  } catch {    // 文件不存在时忽略  }  const tempPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`;  const handle = await open(tempPath, "w", mode ?? 0o644);  try {    await handle.writeFile(JSON.stringify(data, null, 2), "utf8");    await handle.datasync();  } finally {    await handle.close();  }  try {    if (mode !== undefined) {      await chmod(tempPath, mode);    }    await rename(tempPath, targetPath);  } catch (err) {    try {      await unlink(tempPath);    } catch {}    throw err;  }}

原子写 `.mcp.json“ 这种细节也很值得关注,因为它降低了配置写坏、半写入、并发破坏等问题。

这些问题表面上像“健壮性”,但在安全里也同样重要,因为损坏配置往往会造成绕过、降级或异常 fallback。

第十层:认证信息和敏感凭据也做了分层存储

Claude Code 在凭据处理上也有比较清楚的层次。

可以看:

  • src/utils/secureStorage/index.ts
  • src/utils/sessionIngressAuth.ts
  • src/bridge/jwtUtils.ts

1)secure storage 分平台处理

在 macOS 上优先使用 keychain,不行再 fallback;其他平台则有单独实现路径。

虽然这里并不代表“所有平台都达到了同样强度”,但至少能看出设计方向是:

  • 凭据不应该只是一段普通文本配置
  • 应该优先借助平台安全能力保存

2)session ingress token 有明确优先级和回退路径

sessionIngressAuth.ts 里能看到 token 获取顺序:

  1. 环境变量
  2. 文件描述符
  3. well-known file

同时还区分了:

  • session key 走 Cookie + Organization Header
  • JWT 走 Bearer Token

这说明它不是把所有 token 一股脑拼接出去,而是对不同类型凭据有明确协议处理。

第十一层:组织策略和受管设置可以把安全要求“下压”到客户端

Claude Code 还有一个非常成熟的方向,就是把安全要求放进:

  • policySettings
  • remote managed settings

这一类托管配置里。

你从下面这些模块都能看到这种痕迹:

  • src/entrypoints/init.ts
  • src/services/policyLimits/index.ts
  • src/utils/managedEnv.ts
  • src/utils/plugins/pluginPolicy.ts
  • src/utils/sandbox/sandbox-adapter.ts

这说明它不只是“给个人开发者一个本地工具”,而是在按企业级工具做设计。

企业环境里,很多限制不能靠用户自觉,例如:

  • 哪些插件能用
  • 哪些域名能访问
  • 哪些路径能读写
  • 哪些 env 可以覆盖
  • 哪些 MCP 配置允许接入

Claude Code 的做法,是让这些约束能以策略形式从上游下发,并且进入本地运行时真正生效。

这套安全设计的核心思想是什么

如果把上面这些点收束一下,我觉得 Claude Code 的安全设计主要体现了 5 个思想。

1)不信任模型

模型有能力,但不拥有最终执行权。

它想执行工具,必须通过:

  • 权限系统
  • 规则系统
  • 审批系统
  • 沙箱系统

2)不信任配置

不是所有本地配置都默认安全,尤其是项目级设置、插件清单、MCP 配置、环境变量注入点。

3)不信任扩展

插件、MCP、远程连接都被当成高风险扩展能力处理,而不是“越多越好”。

4)不只靠一层拦截

权限、分类器、hook、路径校验、sandbox、托管策略,这些是叠加的,不是互相替代的。

5)安全和工程治理是绑在一起的

很多设计看起来像“工程细节”,例如原子写配置、严格 schema、分层设置源、配置优先级控制,但这些恰恰是安全真正落地的基础。

十三、如果你想复用这些思路,可以先学什么

如果你想自己做类似的 Agent / CLI 框架,我建议优先借鉴下面几类做法:

  1. 所有外部能力统一工具化,不要让模型随意碰系统接口。
  2. 在工具调用前建立统一权限决策层,不要把审批逻辑散在各工具里。
  3. 对文件、Shell、网络分别做专项风险校验,不要指望一条总规则解决全部问题。
  4. 配置、插件、扩展协议都当成潜在攻击面处理。
  5. 审批和沙箱同时存在,软控制和硬限制都要有。
  6. 给企业托管策略预留位置,否则产品一旦进入组织环境就很难补救。

总结

Claude Code 的安全设计,不是“加了几个确认框”,而是把安全嵌进了整个运行时:

  • 工具能力先统一收口
  • 工具调用先过权限总闸门
  • 规则之外再加 hook 和分类器
  • 文件、Shell、网络分别做专门限制
  • 运行时用 sandbox 做硬隔离
  • 配置、插件、MCP、环境变量都做治理
  • 企业策略还能继续向下压实

所以它真正想解决的,不是“让模型绝对不犯错”,而是:

当模型可能犯错、配置可能被污染、扩展可能带来风险时,系统是否还有足够多层机制把问题挡在执行之前,或者至少把影响范围压小。

如果只从概念上讲,Agent 的安全设计很容易变成空话。真正能落地的安全,通常都长成一些很“不优雅”的代码:

  • 识别并剥掉危险 allow 规则
  • 所有工具统一走 allow / deny / ask 决策
  • 文件路径先做 UNC、穿越、越界校验
  • 把 WebFetch 规则真正转换成 sandbox 域名白名单
  • 区分哪些配置来源能在 trust 之前生效
  • 插件和 MCP 配置先做结构与路径安全检查
  • 写关键配置时用原子写而不是直接覆盖

这些代码不华丽,但它们才是“模型可能犯错时,系统还能不能稳住”的关键。

这也是为什么 Claude Code 这类项目值得研究。它给出的不是抽象安全口号,而是一套比较工程化、可落地的 Agent 安全实现路径。

往期推荐

CC 源码学习(1):框架设计与整体架构一个安全、简单的 SSH 管理方式 secssh每次给交换机做配置都像重来一遍?试试这个小工具Markdown 转公众号排版工具:md2wechat