安全设计

本文总结仓库中“安全层”的实际实现方式。这里的“安全层”不是单独一个模块,而是一套横切系统,覆盖:

  • 工具是否对模型可见
  • 工具调用前是否允许执行
  • 文件/命令/远端桥接是否触达敏感边界
  • 自动模式是否会绕过人工确认
  • 共享记忆是否会把秘密同步出去

核心代码主要分布在:

  • src/utils/permissions/*
  • src/services/tools/*
  • src/tools.ts
  • src/utils/permissions/filesystem.ts
  • src/utils/permissions/yoloClassifier.ts
  • src/bridge/*
  • src/services/teamMemorySync/*

1. 总体设计思路

这套安全设计不是“工具内部各写一套校验”,而是平台统一裁决,分成几层:

  1. 能力暴露层 在模型看到工具列表之前,先按 deny rule 过滤工具,直接缩小能力边界。
  2. 规则判定层 用统一的 ToolPermissionContext、allow/deny/ask 规则和模式状态做首轮裁决。
  3. 工具专属安全层 每个工具在 checkPermissions() 里补充自己的内容级安全检查,例如文件路径、shell 子命令、sandbox 相关判断。
  4. 自动化安全层 auto mode 下再接一层分类器,避免“规则允许但语义危险”的动作直接落地。
  5. 交互审批层 无法自动放行时,进入交互式确认、远端桥接确认,或 headless 下拒绝。
  6. 执行后治理层 Pre/Post hook、拒绝 hook、审计日志、拒绝计数、提示建议,形成闭环。

换句话说,它不是“只有执行前问一次用户”,而是“先缩工具池,再做规则判定,再做语义安全,再决定是否要问用户”。

2. 核心数据结构

2.1 ToolPermissionContext

定义见:

  • src/Tool.ts
  • src/types/permissions.ts

它是整套权限系统的运行时上下文,核心字段包括:

  • mode 当前权限模式,如 default、acceptEdits、plan、bypassPermissions、auto。
  • alwaysAllowRules / alwaysDenyRules / alwaysAskRules 按来源拆分的规则集合。
  • additionalWorkingDirectories 额外加入权限范围的工作目录。
  • isBypassPermissionsModeAvailable 是否允许进入 bypass 模式。
  • isAutoModeAvailable auto mode 是否可用。
  • shouldAvoidPermissionPrompts 当前上下文是否不能弹权限框,例如后台 agent。
  • awaitAutomatedChecksBeforeDialog 是否在显示权限框前先等自动检查完成。
  • strippedDangerousRules 进入 auto mode 时临时剥离的危险 allow 规则。

2.2 规则模型

定义见:

  • src/types/permissions.ts
  • src/utils/permissions/permissionRuleParser.ts

规则由三部分组成:

  • source 来源,例如 userSettings、projectSettings、localSettings、cliArg、session、policySettings。
  • ruleBehavior allow / deny / ask
  • ruleValue 形如 toolName + ruleContent

规则字符串支持:

  • 整个工具级别规则,如 Bash
  • 带内容的规则,如 Bash(npm publish:*)
  • 转义括号内容
  • legacy tool 名映射,避免工具改名后旧规则失效

3. 第一层:先过滤“模型看得到什么”

实现见:

  • src/tools.ts
  • src/utils/permissions/permissions.ts

关键点:

  • filterToolsByDenyRules() 会在工具暴露给模型前,直接删除被 deny rule 命中的工具。
  • 这不仅作用于内建工具,也作用于 MCP 工具。
  • 对 MCP 还支持 server 级别屏蔽,例如 mcp__server 或 mcp__server__*。

这意味着安全边界并不只在“调用时”判断,而是更早在“能力暴露阶段”就生效。模型从一开始就看不到明确被禁用的能力。

4. 第二层:统一权限决策链

主流程在:

  • src/services/tools/toolExecution.ts
  • src/utils/permissions/permissions.ts
  • src/hooks/useCanUseTool.tsx

4.1 调用顺序

一次工具调用的大致顺序是:

  1. 解析输入并做 backfillObservableInput
  2. 执行 PreToolUse hooks
  3. 合并 hook 给出的 updatedInput / permissionResult
  4. 调 hasPermissionsToUseTool()
  5. 若需要,进入 auto classifier 或交互审批
  6. 允许后再真正执行工具
  7. 执行 PostToolUse / PostToolUseFailure hooks

4.2 hasPermissionsToUseToolInner() 的裁决顺序

关键顺序很清晰:

  1. 整个工具 deny rule
  2. 整个工具 ask rule
  3. 工具自己的 checkPermissions()
  4. 工具级 deny 结果
  5. 必须用户交互的工具保持 ask
  6. 内容级 ask rule 保持 ask
  7. safetyCheck 保持 ask
  8. 若是 bypassPermissions,且未命中前面的强约束,则 allow
  9. 若整个工具有 allow rule,则 allow
  10. 剩余的 passthrough 统一转成 ask

这套顺序说明两件事:

  • deny、ask、safetyCheck 的优先级高于 bypass 模式。
  • allow rule 不是无条件生效,它排在工具专属安全检查之后。

5. 第三层:文件系统安全

实现主要在:

  • src/utils/permissions/filesystem.ts
  • src/utils/permissions/pathValidation.ts
  • src/tools/FileReadTool/FileReadTool.ts
  • src/tools/FileEditTool/FileEditTool.ts
  • src/tools/FileWriteTool/FileWriteTool.ts

5.1 设计原则

文件权限不是简单看“是不是在 cwd 下”,而是同时考虑:

  • 工作目录与附加目录
  • symlink 解析后的真实路径
  • read/edit 独立规则
  • acceptEdits 模式
  • sandbox 写白名单
  • Claude 自身配置目录与敏感目录
  • Windows/UNC 特殊路径绕过
  • 内部运行目录的受控豁免

5.2 路径检查顺序

写权限 checkWritePermissionForTool() 的顺序大致是:

  1. edit deny rule
  2. 内部可写路径豁免
  3. .claude/** 的 session 级特殊 allow 规则
  4. checkPathSafetyForAutoEdit()
  5. edit ask rule
  6. acceptEdits + 工作目录内 自动允许
  7. edit allow rule
  8. 默认 ask

读权限 checkReadPermissionForTool() 则是:

  1. UNC 与可疑 Windows 路径拦截
  2. read deny rule
  3. read ask rule
  4. “有 edit 权限则可读”
  5. 工作目录内可读
  6. 内部可读路径豁免
  7. read allow rule
  8. 默认 ask

5.3 敏感路径与绕过防护

checkPathSafetyForAutoEdit() 会拒绝自动编辑这些对象:

  • .git、.vscode、.idea、.claude
  • .gitconfig、.gitmodules、shell rc 文件等
  • Claude 自己的 settings / commands / agents / skills 目录
  • 可疑 Windows 路径模式 例如 ADS、8.3 短文件名、长路径前缀、尾随点/空格、设备名、UNC

而且它不是只检查原路径,还会检查 getPathsForPermissionCheck() 得到的解析路径,避免通过 symlink 绕过。

5.4 工作目录模型

工作目录范围由:

  • 原始 cwd
  • additionalWorkingDirectories

共同决定。

pathInAllowedWorkingPath() 会对输入路径和工作目录都做对称解析,再判断是否位于允许范围内。这解决了:

  • symlink cwd
  • macOS /tmp / /private/tmp
  • 大小写不敏感文件系统

带来的假阴性或绕过问题。

5.5 内部路径豁免

为了让系统自身可运行,代码显式放行一批“受控内部路径”,例如:

  • 当前 session 的 plan 文件
  • scratchpad
  • session memory
  • tool results
  • project temp 目录
  • agent memory / auto memory
  • tasks / teams / bundled skills 提取目录

这些豁免都是明确列举的,不是“凡是 .claude 下都放行”。

6. 第四层:Shell 与命令安全

实现见:

  • src/utils/permissions/shellRuleMatching.ts
  • src/utils/permissions/dangerousPatterns.ts
  • src/utils/permissions/permissionSetup.ts
  • src/utils/permissions/permissions.ts

6.1 规则匹配能力

shell 规则支持三类匹配:

  • exact
  • legacy prefix,如 npm:*
  • wildcard,如 git *

这让“命令级 allow/ask/deny”可以细到具体子命令,而不是只能对整个 Bash/PowerShell 开大口子。

6.2 auto mode 对危险 allow 规则做“去武器化”

进入 auto mode 时,stripDangerousPermissionsForAutoMode() 会主动剥离危险 allow 规则,防止它们绕过分类器。

典型危险规则包括:

  • Bash(*)
  • Bash(python:*)
  • Bash(node:*)
  • PowerShell(*)
  • PowerShell(iex:*)
  • PowerShell(Start-Process:*)
  • 任意 Agent(...) allow

离开 auto mode 时,再通过 restoreDangerousPermissions() 恢复。

这说明 auto mode 不是简单“把默认模式改个名字”,而是重新收紧了一次权限上下文。

6.3 auto classifier 的位置

实现见:

  • src/utils/permissions/yoloClassifier.ts
  • src/utils/permissions/classifierDecision.ts
  • src/utils/permissions/denialTracking.ts

auto mode 的判定大致分三层:

  1. 快速放行路径
    • 如果在 acceptEdits 语义下本来就会允许,则不走分类器
    • 某些安全工具在 allowlist 中,直接放行
  2. 分类器判定
    • classifyYoloAction() 对动作和上下文做语义安全判断
    • XML classifier 支持两阶段模式:先 fast,再在需要时进入 thinking
  3. 失败保护
    • transcript 太长时回退到手动审批
    • 分类器不可用时可 fail closed 或 fail open,受 gate 控制
    • 连续拒绝和总拒绝次数过高时,退回人工审阅

因此 auto mode 的本质是“规则层 + 语义分类层”的叠加,不是彻底自动放行。

6.4 sandbox 也是权限链的一部分

Bash ask rule 在特定条件下可被 sandbox 自动放行逻辑覆盖:

  • sandbox 已启用
  • 配置允许 “sandboxed bash auto allow”
  • 当前命令确实会在 sandbox 中执行

也就是说,sandbox 不是独立附加功能,而是会直接参与权限决策。

7. 第五层:交互审批、Hook 与 headless 约束

实现见:

  • src/hooks/useCanUseTool.tsx
  • src/hooks/toolPermission/handlers/interactiveHandler.ts
  • src/services/tools/toolHooks.ts
  • src/utils/permissions/permissions.ts

7.1 交互审批

当结果是 ask 时:

  • 主线程会进入交互式权限框
  • 可以被 classifier/hook 在后台抢先放行
  • 也可以转发给 bridge/channel 做远端确认

7.2 headless / 后台 agent

如果 shouldAvoidPermissionPrompts = true,系统不会假装“等用户确认”,而是:

  1. 先给 PermissionRequest hook 一次机会
  2. 如果 hook 没有明确 allow/deny
  3. 直接 auto deny

这避免了后台 agent 卡死在无 UI 的审批点。

7.3 Hook 位置

Hook 不是可有可无的扩展,而是嵌在安全链里:

  • PreToolUse 可以修改输入、阻断执行、返回权限结果
  • PermissionRequest 可以替代用户审批
  • PermissionDenied 可以在 auto deny 后决定是否允许重试
  • PostToolUse / PostToolUseFailure 做后处理与治理

这使得权限系统是“平台统一规则 + 可插拔策略”的组合。

8. 第六层:远端桥接与鉴权

实现见:

  • src/bridge/jwtUtils.ts
  • src/bridge/trustedDevice.ts
  • src/bridge/workSecret.ts
  • src/bridge/bridgePermissionCallbacks.ts
  • src/remote/remotePermissionBridge.ts

8.1 远端权限请求不会绕过本地审批模型

远端会话需要审批时,bridge 会把请求转换成统一的权限请求结构,再由本地同一套 ToolUseConfirm 流程处理。对于本地不认识的远端工具,还会创建最小 stub,走 fallback permission request。

因此远端工具不是另开一条“弱化版审批通道”,而是尽量复用本地审批体系。

8.2 会话令牌与刷新

createTokenRefreshScheduler() 会根据 JWT exp 提前刷新 session token,避免长会话过期失效。它带有:

  • refresh buffer
  • 失败重试
  • generation 防抖,避免旧刷新任务污染新会话

8.3 trusted device

bridge 的 elevated 会话还引入 trusted device token:

  • token 存在 secure storage
  • 受 gate 控制是否启用
  • /login 后尽早 enrollment
  • 支持清缓存和账号切换场景

这相当于对 OAuth 之外再补一层“设备信任”。

8.4 work secret 与 session 绑定

decodeWorkSecret() 会校验版本和必要字段; sameSessionId() 会在不同 tagged id 之间比较底层 session body,防止桥接过程把别的 session 误当成当前 session。

9. 第七层:共享记忆的秘密防泄漏

实现见:

  • src/services/teamMemorySync/secretScanner.ts
  • src/services/teamMemorySync/teamMemSecretGuard.ts
  • src/tools/FileWriteTool/FileWriteTool.ts
  • src/tools/FileEditTool/FileEditTool.ts

设计要点:

  • 在写入 team memory 前,先在本地扫描内容
  • 使用高置信度 secret 规则子集
  • 命中后直接拒绝写入
  • 扫描发生在上传前,避免秘密离开本机

team memory sync 本身还要求:

  • first-party OAuth
  • 指定 scope
  • repo 级作用域
  • delta upload / checksum 同步

所以这里既有“共享数据面”的权限约束,也有“内容面”的秘密扫描。

10. 这套安全层的几个关键特点

10.1 不是单点校验,而是分层收敛

同一个危险动作通常会经过多层约束:

  • 工具暴露过滤
  • 规则 deny/ask
  • 工具专属安全检查
  • auto classifier
  • 交互审批或 headless 拒绝

10.2 默认保守,但允许局部放宽

系统允许:

  • 按工具授权
  • 按命令前缀授权
  • 按目录授权
  • 按 session 临时授权

但又尽量避免“一把梭”授权在 auto mode 下失控,所以会剥离危险 allow 规则、限制 .claude、限制敏感路径、限制 headless prompt。

10.3 明确区分“平台内部路径”和“用户项目路径”

很多内部文件被显式豁免,是为了保证 agent runtime 本身能工作;但这些豁免是精确列举,不会泛化成对整个配置目录的长期开放。

10.4 兼顾安全与可用性

典型例子有:

  • sandbox 白名单参与写权限自动放行
  • read 权限可从 edit 权限推导
  • 给权限对话框生成 suggestions
  • auto mode 中对安全工具走 allowlist,减少分类器成本

这说明它并不是“安全优先到不可用”,而是在严格边界内做效率优化。

11. 结论

这个仓库的安全层本质上是一个“平台级权限与安全编排系统”,而不是零散的工具内校验。它的核心设计可以概括为:

  • 用统一的 ToolPermissionContext 和规则体系收拢权限状态
  • 在工具暴露前先裁剪能力边界
  • 在执行前按规则、路径、命令、模式做多阶段检查
  • 在 auto mode 下额外引入语义分类器,并主动剥离会绕过分类器的危险 allow 规则
  • 对远端桥接、共享记忆、敏感路径、Windows/UNC 特殊路径做专项防护
  • 用 Hook、建议、审计、拒绝计数把安全机制接入完整执行链路

因此,这套安全设计的重点不是“绝对禁止”,而是把“可授权的范围、不可绕过的边界、自动化的上限”清楚分层,并且把这些层真正落实到了工具池、权限判定、文件系统、自动模式、远端桥接和数据同步的代码里。