Skip to content

第 7 章:权限系统

难度: 中级 | 阅读时间: 约 55 分钟


目录

  1. 简介
  2. 权限模式
  3. 权限规则系统
  4. 权限检查流程
  5. Bash 安全:AST 解析与分类器
  6. 文件权限检查
  7. 权限 UI 组件
  8. 沙箱集成
  9. 动手实践:构建权限系统
  10. 核心要点与后续章节
  11. 动手构建:权限检查集成

1. 简介

Claude Code 代表 AI 模型执行真实的工具调用——Shell 命令、文件写入、网络请求。如果没有完善的权限系统,一条错误或恶意的指令就可能删除文件、泄露数据或破坏生产环境。

权限系统位于工具调用工具执行之间。每当 Claude 想要运行一个工具时,运行时都会调用该工具的 checkPermissions(),然后将结果传入 hasPermissionsToUseTool()。最终决策只有三种行为:

行为含义
allow立即执行,无需用户提示
ask暂停并向用户请求审批
deny直接拒绝,向 Claude 返回错误

本章将逐层追踪这一决策过程——从 7 种全局模式,经过 8 个来源的分层规则,深入到 AST 级别的 Bash 分析和沙箱文件系统强制执行。

涉及的源码文件:

文件职责
src/types/permissions.ts类型定义(模式、规则、决策)
src/utils/permissions/PermissionMode.ts模式配置、显示名称、类型守卫
src/utils/permissions/permissions.ts规则查找工具函数、hasPermissionsToUseTool
src/utils/permissions/permissionRuleParser.ts规则字符串的解析与序列化
src/utils/permissions/shellRuleMatching.ts通配符/前缀规则匹配
src/utils/permissions/filesystem.ts文件路径权限检查
src/utils/permissions/dangerousPatterns.ts危险命令/解释器列表
src/utils/permissions/getNextPermissionMode.ts模式循环逻辑(Shift+Tab)
src/hooks/toolPermission/PermissionContext.tscreateResolveOncePermissionContext
src/hooks/toolPermission/handlers/interactiveHandler.ts交互式权限流程
src/tools/BashTool/bashPermissions.tsbashToolHasPermission——tree-sitter 门控
src/utils/sandbox/sandbox-adapter.tsSandboxManager 集成

2. 权限模式

权限模式是会话级别的设置,决定了当没有明确规则匹配时的默认姿态。模式定义在 src/types/permissions.ts

typescript
// src/types/permissions.ts:16-29
export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',
  'bypassPermissions',
  'default',
  'dontAsk',
  'plan',
] as const

export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]

// 内部模式新增 'auto'(ANT 专属)和 'bubble'(协调者模式)
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
export type PermissionMode = InternalPermissionMode

模式参考表

模式符号颜色行为
default中性色对未知工具询问,遵循 allow 规则
acceptEdits⏵⏵绿色自动批准工作目录内的文件编辑
plan蓝色只读规划模式,所有写操作需审批
bypassPermissions⏵⏵红色跳过所有权限检查(危险)
dontAsk⏵⏵红色将所有 ask 决策转为 deny
auto⏵⏵黄色ANT 专属:使用 AI 分类器代替用户提示
bubble内部:协调者将决策委托给父代理

显示元数据在 src/utils/permissions/PermissionMode.ts:44-91 中通过 PERMISSION_MODE_CONFIG 映射表定义,每种模式关联 titleshortTitlesymbolcolor

模式循环(Shift+Tab)

用户可以通过 Shift+Tab 循环切换模式。循环逻辑在 src/utils/permissions/getNextPermissionMode.ts 中:

default → acceptEdits → plan → (bypassPermissions?) → (auto?) → default

bypassPermissionsauto 只有在对应功能标志或权限位启用时才可用(isBypassPermissionsModeAvailableisAutoModeAvailable)。

dontAskbypassPermissions 的区别

这两个模式常被混淆:

  • bypassPermissions:完全跳过 checkPermissions(),工具无条件执行。
  • dontAsk:仍然运行 checkPermissions(),但将任何 ask 结果转为 deny。它比 default 更严格,而不是更宽松。

3. 权限规则系统

规则是精细控制特定工具调用允许或拒绝的机制。每条规则由三部分组成:

typescript
// src/types/permissions.ts:75-79
type PermissionRule = {
  source: PermissionRuleSource     // 规则来源
  ruleBehavior: PermissionBehavior // 'allow' | 'deny' | 'ask'
  ruleValue: PermissionRuleValue   // { toolName, ruleContent? }
}

规则来源(优先级顺序)

规则从 8 个来源收集,越靠前的来源优先级越高:

优先级来源说明
1policySettings企业管理策略(只读)
2flagSettings功能标志覆盖
3userSettings~/.claude/settings.json
4projectSettings项目根目录的 .claude/settings.json
5localSettings.claude/settings.local.json
6cliArg--allowedTools / --disallowedTools 命令行参数
7command/permissions add 斜杠命令
8session本次会话中批准的(仅内存)

完整来源列表在 src/utils/permissions/permissions.ts:109-114 组装:

typescript
const PERMISSION_RULE_SOURCES = [
  ...SETTING_SOURCES,   // policySettings, flagSettings, userSettings, projectSettings, localSettings
  'cliArg',
  'command',
  'session',
] as const satisfies readonly PermissionRuleSource[]

规则格式

规则以字符串形式存储,由 src/utils/permissions/permissionRuleParser.ts 解析:

ToolName                → 整个工具的规则
ToolName(content)       → 按内容限定的规则
Bash(npm install)       → 精确匹配 bash 命令
Bash(git *)             → 通配符:任意 git 子命令
Read(~/*.ts)            → 读取 home 目录下任意 .ts 文件
WebFetch(https://api.example.com/*)  → 特定 URL 模式

解析器处理内容中的转义括号——Bash(python -c "print\(1\)") 会正确提取 python -c "print(1)" 作为规则内容(permissionRuleParser.ts:55-79)。

规则行为

ruleBehavior效果
allow绕过提示,立即执行
deny阻止,向 Claude 返回错误
ask强制提示,即使模式允许也会询问

ask 行为会覆盖宽松的模式——如果将 Bash(rm *) 设为 ask 规则,即使在 acceptEdits 模式下,用户在任何 rm 命令前都会被询问。

规则匹配

src/utils/permissions/shellRuleMatching.ts 实现了三种规则形态:

形态示例匹配逻辑
exactgit status完全字符串匹配
prefixnpm:*(旧语法)命令以指定前缀开头
wildcardgit *npm run *类 Glob 模式,* 作通配符

通配符引擎(matchWildcardPattern,第 90 行)将模式转换为正则表达式,处理 \* 作为字面星号、\\ 作为字面反斜杠。末尾的 * 使空格+参数变为可选,因此 git * 也能匹配裸 git


4. 权限检查流程

整体流程

mermaid
flowchart TD
    A[工具调用] --> B[tool.checkPermissions]
    B --> C{result.behavior}
    C -->|allow| D[执行工具]
    C -->|deny| E[向 Claude 返回错误]
    C -->|ask| F[hasPermissionsToUseTool]

    F --> G{权限模式}
    G -->|bypassPermissions| D
    G -->|dontAsk| E
    G -->|default/acceptEdits/plan| H[交互式处理器]

    H --> I[pushToQueue — 显示对话框]
    H --> J[runHooks 异步]
    H --> K[Bash 分类器 异步]
    H --> L[Bridge/Channel 中继]

    I & J & K & L --> M{resolve-once 竞争}
    M -->|第一个胜出| N{决策}
    N -->|allow| D
    N -->|deny| E

第一步:tool.checkPermissions()

每个工具实现自己的 checkPermissions()。示例:

  • BashToolsrc/tools/BashTool/BashTool.tsx:539):委托给 bashToolHasPermission
  • FileEditToolsrc/tools/FileEditTool/FileEditTool.ts:125):调用 checkWritePermissionForTool

结果是 PermissionResult——allowdeny 或带消息和建议的 ask

第二步:hasPermissionsToUseTool()

定义在 src/utils/permissions/permissions.ts:473,此函数在内部检查之上应用模式级别的转换:

  1. bypassPermissions → 无条件返回 allow
  2. dontAsk 且结果为 ask → 转为 deny
  3. auto 模式 → 在显示对话框前先尝试 AI 分类器
  4. 否则 → 委托给交互式或无头处理器

第三步:交互式权限处理器

src/hooks/toolPermission/handlers/interactiveHandler.ts 处理交互式情况。它为单个权限请求启动四个并发竞争者

竞争者来源优先级
用户对话框本地终端 UI第一次交互胜出
PermissionRequest 钩子外部脚本后台异步
Bash 分类器AI 安全分类器后台异步
Bridge/Channel 中继claude.ai 网页、Telegram、iMessage远程异步

resolve-once 守卫确保只有一个竞争者胜出:

typescript
// src/hooks/toolPermission/PermissionContext.ts:75-93
function createResolveOnce<T>(resolve: (value: T) => void): ResolveOnce<T> {
  let claimed = false
  let delivered = false
  return {
    resolve(value: T) {
      if (delivered) return
      delivered = true
      claimed = true
      resolve(value)
    },
    claim() {         // 原子性的检查并标记
      if (claimed) return false
      claimed = true
      return true
    },
  }
}

每个竞争者在任何 await 之前都调用 claim()。若 claim() 返回 false,说明另一个竞争者已经胜出——调用者静默丢弃其结果。这即使在并发异步回调中也能防止双重解析。

第四步:权限上下文对象

createPermissionContextsrc/hooks/toolPermission/PermissionContext.ts:96)创建一个上下文对象,包含:

  • pushToQueue / removeFromQueue / updateQueueItem——React 状态桥接
  • runHooks——执行 settings.json 中的 PermissionRequest 钩子
  • tryClassifier——等待 Bash 分类器结果
  • handleUserAllow / handleHookAllow——持久化并记录审批
  • cancelAndAbort——中止信号 + 拒绝消息

这种设计使权限逻辑不直接依赖 React——React 状态通过 PermissionQueueOps 接口注入(src/hooks/toolPermission/PermissionContext.ts:57-61)。


5. Bash 安全:AST 解析与分类器

Bash 命令需要特殊处理,因为 Shell 语法通过命令替换、展开和管道允许任意代码执行。

Tree-sitter AST 门控

bashToolHasPermissionsrc/tools/BashTool/bashPermissions.ts:1663)从 AST 解析开始:

typescript
// src/tools/BashTool/bashPermissions.ts:1688-1695
let astRoot = injectionCheckDisabled
  ? null
  : feature('TREE_SITTER_BASH_SHADOW') && !shadowEnabled
    ? null
    : await parseCommandRaw(input.command)

let astResult: ParseForSecurityResult = astRoot
  ? parseForSecurityFromAst(input.command, astRoot)
  : { kind: 'parse-unavailable' }

AST 结果有三种形态:

类型含义处理
simple干净解析,得到 SimpleCommand[]检查语义,应用规则
too-complex检测到替换/展开应用拒绝规则,然后 ask
parse-unavailableWASM 未加载回退到旧版正则路径

当 tree-sitter 检测到命令替换($(...) 或反引号)、进程替换、heredoc 或控制流时,too-complex 结果触发。这些结构无法静态分析,因此命令始终需要用户批准,除非有精确的拒绝/允许规则匹配。

危险模式列表

src/utils/permissions/dangerousPatterns.ts 定义了进入 auto 模式时会被剥离的模式:

typescript
// src/utils/permissions/dangerousPatterns.ts:44-50
export const DANGEROUS_BASH_PATTERNS: readonly string[] = [
  ...CROSS_PLATFORM_CODE_EXEC,  // python, node, deno, ruby, perl, bash, ssh...
  'zsh', 'fish', 'eval', 'exec', 'env', 'xargs', 'sudo',
]

Bash(python:*) 这样的允许规则会让 Claude 运行任意 Python 代码,绕过分类器。permissionSetup.ts 在切换到 auto 模式之前会剥离这些规则。

语义安全检查

即使对于 simple AST 结果,也会对命令列表运行语义级检查。这些检查捕获解析干净但按名称危险的构造——例如 eval "rm -rf /" 有干净的 AST,但会被语义检查阻止。


6. 文件权限检查

文件工具(Read、Edit、Write)使用 src/utils/permissions/filesystem.ts 中基于 gitignore 风格的模式引擎。

受保护的文件和目录

typescript
// src/utils/permissions/filesystem.ts:57-79
export const DANGEROUS_FILES = [
  '.gitconfig', '.gitmodules', '.bashrc', '.bash_profile',
  '.zshrc', '.zprofile', '.profile', '.ripgreprc',
  '.mcp.json', '.claude.json',
]

export const DANGEROUS_DIRECTORIES = [
  '.git', '.vscode', '.idea', '.claude',
]

这些文件/目录永远不会被自动编辑——无论何种模式都需要用户明确批准。

路径匹配

matchingRuleForInput 使用 ignore 包(gitignore 语义)将文件路径与权限规则进行匹配,支持:

  • 绝对路径
  • ~/path 模式(展开到 home 目录)
  • ./path(相对于项目根目录)
  • ** 的 Glob 模式

路径遍历在 src/utils/permissions/filesystem.ts 中通过 containsPathTraversal 检测并阻止。

大小写不敏感的安全处理

macOS 和 Windows 文件系统是大小写不敏感的。第 90 行的检查在比较前将路径规范化为小写:

typescript
// src/utils/permissions/filesystem.ts:90-92
export function normalizeCaseForComparison(path: string): string {
  return path.toLowerCase()
}

这防止通过访问 .ClaUdE/Settings.JSON 来绕过针对 .claude/settings.json 的拒绝规则。

FileEditTool 的写权限

src/tools/FileEditTool/FileEditTool.ts:125-131 中的 checkWritePermissionForTool 会:

  1. 检查文件路径是否在 deny 规则中
  2. 检查文件是否是危险文件/目录
  3. acceptEdits 模式下:自动批准工作目录内的文件
  4. 否则:返回 ask 并附带允许该路径的建议

7. 权限 UI 组件

src/components/permissions/ 目录包含每个工具权限对话框的基于 Ink 的 React 组件。

组件注册表

组件工具备注
BashPermissionRequestBash显示命令和子命令分解
FileEditPermissionRequestEdit显示差异预览
FileWritePermissionRequestWrite显示新文件内容
FilesystemPermissionRequestRead显示文件路径
WebFetchPermissionRequestWebFetch显示 URL
SedEditPermissionRequestSedEdit显示差异
FallbackPermissionRequest任意通用回退
SandboxPermissionRequestSandbox沙箱覆盖

PermissionDialog

src/components/permissions/PermissionDialog.tsx 是包裹所有工具特定组件的外壳。它:

  1. 根据 tool.name 选择正确的组件
  2. 渲染 PermissionExplanation 显示需要审批的原因
  3. 提供允许/拒绝/始终允许按钮
  4. onAllow / onReject 回调传回 handleInteractivePermission

分类器指示器

当 Bash 分类器在后台运行时,对话框显示一个旋转器(classifierCheckInProgress: true)。当分类器批准时,绿色复选标记会短暂显示,然后对话框自动关闭(终端聚焦时 3 秒,未聚焦时 1 秒)——见 interactiveHandler.ts:509-518

PermissionRuleExplanation

src/components/permissions/PermissionRuleExplanation.tsx 解释为什么触发了规则:

  • 规则来源:"来自用户设置"、"来自项目设置"
  • 规则内容:匹配的规则字符串
  • 行为:规则的作用

这种透明度帮助用户理解和调试权限配置。


8. 沙箱集成

沙箱是更深层的安全层,在操作系统级别独立于权限规则系统强制执行限制。

架构

权限规则(Claude Code 层)

SandboxManager(适配器层)

@anthropic-ai/sandbox-runtime(操作系统层)

src/utils/sandbox/sandbox-adapter.ts 将 Claude Code 的设置格式桥接到 sandbox-runtime 包的 SandboxRuntimeConfig

路径模式解析

Claude Code 使用 sandbox-runtime 不了解的自定义路径前缀(sandbox-adapter.ts:99-119):

模式解析
//path从根目录开始的绝对路径:/path
/path相对于设置文件目录
~/path直接传递给 sandbox-runtime
./path直接传递给 sandbox-runtime

文件系统限制

沙箱在操作系统级别强制执行读写限制。规则来自:

  1. Read/Edit/Write 工具的 permissions.alwaysAllow / permissions.alwaysDeny 规则
  2. 设置中的 sandbox.filesystem.allowWrite / sandbox.filesystem.denyWrite
json
// settings.json 沙箱配置示例
{
  "sandbox": {
    "filesystem": {
      "allowWrite": ["~/projects/**", "./dist/**"],
      "denyWrite": ["~/.ssh/**", "~/.aws/**"]
    },
    "network": {
      "allowedDomains": ["api.github.com", "*.npmjs.com"]
    }
  }
}

网络限制

网络规则从 WebFetch 权限规则中提取,并与 sandbox.network 设置合并。allowedDomains 列表传递给沙箱运行时,在进程级别强制执行。

shouldAllowManagedSandboxDomainsOnly

企业部署可以设置 policySettings.sandbox.network.allowManagedDomainsOnly: true,将 Claude Code 限制为只能访问管理策略中列出的域名,防止用户添加自己的网络允许规则。


9. 动手实践:构建权限系统

让我们构建一个简化但完整的权限系统,演示 Claude Code 实现中的关键模式。

参见配套文件:examples/07-permission-system/permission-check.ts

示例实现了:

  1. 多来源规则加载——来自用户、项目、会话和 CLI 来源的规则
  2. 三种行为规则——allowdenyask
  3. 通配符模式匹配——git *npm run *
  4. resolve-once 并发守卫——防止双重解析
  5. 模式级别覆盖——bypassPermissionsdontAsk

运行示例

bash
cd examples/07-permission-system
npx ts-node permission-check.ts

关键模式解析

模式 1:按来源的规则优先级

来自 policySettings 的规则覆盖 userSettings,后者覆盖 projectSettings。示例中的 getRulesForBehavior 函数按优先级顺序遍历来源,返回第一个匹配项。

模式 2:resolve-once 守卫

typescript
const guard = createResolveOnce(resolve)
// 钩子回调
hook().then(decision => {
  if (!guard.claim()) return  // 另一个竞争者已胜出
  guard.resolve(decision)
})
// 用户对话框回调
onUserApprove(decision => {
  if (!guard.claim()) return
  guard.resolve(decision)
})

claim() 调用是原子性的——它同时检查并标记。这关闭了检查 isResolved() 和调用 resolve() 之间的竞争窗口。

模式 3:模式转换

typescript
if (mode === 'dontAsk' && result.behavior === 'ask') {
  return { behavior: 'deny', message: 'dontAsk 模式' }
}
if (mode === 'bypassPermissions') {
  return { behavior: 'allow', updatedInput: input }
}

模式转换在工具自己的 checkPermissions 之后应用,而不是之前。这意味着对于某些工具类型,即使在 bypassPermissions 模式下,拒绝规则仍然生效。


10. 核心要点与后续章节

核心要点

1. 三层防御

Claude Code 的权限系统有三个独立的层次:

  • 模式:会话级别的姿态(defaultacceptEditsbypassPermissions
  • 规则:来自 8 个来源的工具+内容特定的 allow/deny/ask
  • 沙箱:操作系统级别的文件系统和网络强制执行

2. 工具特定逻辑

checkPermissions() 按工具实现。Bash 使用 AST 分析,FileEdit 使用 gitignore 风格的路径匹配。这让每个工具能对其领域应用正确的安全语义。

3. resolve-once 并发处理

交互式权限处理器竞争四个异步来源(用户、钩子、分类器、bridge)。createResolveOnce 守卫使用原子性的 claim() 确保恰好一个竞争者胜出,关闭了检查-然后-设置的竞争窗口。

4. 透明设计

每个 ask 结果都携带一个 decisionReason,解释为什么工具需要批准。UI 在 PermissionRuleExplanation 中展示这一信息,使系统可审计。

5. 企业锁定

policySettings 是最高优先级来源且只读。结合 allowManagedPermissionRulesOnlyallowManagedSandboxDomainsOnly,企业管理员可以完全控制 Claude Code 能访问的内容。

后续章节

  • 第 8 章:MCP 集成——Claude Code 如何连接到模型上下文协议服务器,以及 MCP 工具权限如何与原生权限系统交互
  • 第 9 章:代理协调——bubble 权限模式在多代理层次结构中的工作原理,以及 swarm worker 如何处理无头权限请求

动手构建:权限检查集成

本节是 demo 的又一次重要升级。 我们新增 utils/permissions.ts 权限检查模块,并将其集成到 query.ts 的工具执行流程中,让 mini-claude 在执行危险操作前能够拦截或询问用户。

项目结构更新

demo/
├── utils/
│   ├── messages.ts      # 第 4 章
│   └── permissions.ts   # ← 新增:权限检查实现
├── query.ts             # 更新:集成权限检查
├── tools/
│   ├── BashTool/
│   ├── FileReadTool/
│   ├── FileWriteTool/
│   ├── FileEditTool/
│   ├── GrepTool/
│   └── GlobTool/
├── main.ts
├── Tool.ts
├── context.ts
├── services/api/
└── types/

utils/permissions.ts 讲解

权限检查模块实现了 Claude Code 权限系统的核心决策流程:

工具调用 → 遍历规则 → 第一个匹配的规则决定行为
  ↓              ↓              ↓
  allow       deny          ask
  (执行)     (拒绝+反馈AI)   (询问用户)

默认规则层次:

  1. 只读工具(Read、Grep、Glob)→ 始终 allow
  2. 危险命令模式(rm -rfmkfsdd if= 等)→ 始终 deny
  3. 写操作(Bash、Write、Edit)→ ask

这三层规则体现了 Claude Code 的核心安全哲学:只读安全放行,危险坚决拒绝,写操作交给用户决定

三种权限模式

模式行为
default严格按规则执行——只读 allow,危险 deny,写操作 ask
auto只读操作自动放行,写操作仍需确认(简化版 acceptEdits
bypassPermissions跳过所有检查(仅限开发调试,生产环境切勿使用)

在真实 Claude Code 中有 7 种模式(见本章第 2 节),demo 简化为 3 种以聚焦核心逻辑。

与 query.ts 的集成

checkPermission 作为可选回调传入 query(),在 QueryOptions 中新增:

typescript
export interface QueryOptions {
  // ...已有字段
  checkPermission?: (toolName: string, input: Record<string, unknown>) => Promise<PermissionResult>;
}

集成点在工具执行前——Agentic Loop 每次拿到工具调用后,先检查权限:

  • allow → 正常执行工具
  • deny → 跳过工具执行,直接返回错误消息给 AI(如 "Permission denied: rm -rf is blocked by safety rules"),AI 会据此调整策略
  • ask → 记录日志并放行(当前 demo 无交互式 UI;第 8 章的 REPL 会弹出确认对话框,实现真正的用户交互确认)

如果未传入 checkPermission 回调,行为与之前完全一致——所有工具无条件执行。这保证了向后兼容。

运行验证

bash
cd demo && bun run main.ts

尝试以下交互来验证权限系统:

你> 删除当前目录下所有文件
# AI 尝试 rm -rf → 被 deny → AI 收到错误并调整策略

你> 读取 package.json
# Read 是只读工具 → 直接 allow,无需确认

你> 创建一个 test.txt 文件
# Write 是写操作 → ask → 日志输出权限检查信息

与真实 Claude Code 的对应关系

Demo 文件真实文件简化了什么
utils/permissions.tssrc/utils/permissions/permissions.ts无多来源规则优先级、无通配符匹配引擎
utils/permissions.ts 危险模式src/utils/permissions/dangerousPatterns.ts硬编码少量模式,无 tree-sitter AST 分析
utils/permissions.ts 模式切换src/utils/permissions/PermissionMode.ts3 种模式 vs 7 种模式
query.ts(权限回调)src/hooks/toolPermission/无 resolve-once 竞争、无分类器、无 bridge

下一章预告

第 8 章将实现交互式终端 UI(React + Ink),包括用户输入、消息渲染、权限确认对话框。届时 ask 权限将真正弹出对话框让用户选择"允许"或"拒绝",而不是仅仅记录日志。


源码引用已对照 anthhub-claude-code 提交树进行验证。