Claude Code 源码分析第 06 课 · 第 05

权限系统

Claude Code 如何决定是否运行工具:从拒绝规则到 AI 分类器

一、系统概述

每次 Claude 想要使用工具(运行 bash 命令、编辑文件、调用 MCP 端点)时,权限系统都会运行多步骤决策管道。 该管道产生以下三种结果之一:

allow

工具立即执行

deny

工具被卡住; 克劳德被告知原因

ask

提示用户批准或拒绝

管道的三个输入是: tool 经要求,其 input (例如 shell 命令)和当前 权限上下文 (模式、所有来源的规则、会话状态)。

2. 决策流程

该管道实施于 hasPermissionsToUseToolInner() in permissions.ts。 步骤已编号以匹配源中的内联注释。

flowchart TD START([Tool use requested]) --> ABORT{Session\naborted?} ABORT -->|yes| THROW([Throw AbortError]) ABORT -->|no| S1A subgraph STEP1["Step 1 — Rule & Safety Checks (always run)"] S1A{Deny rule\nfor tool?} -->|yes| DENY1([deny]) S1A -->|no| S1B S1B{Ask rule\nfor tool?} -->|"yes (no sandbox)"| ASK1([ask]) S1B -->|"no / sandbox allowed"| S1C S1C["tool.checkPermissions(input)"] --> S1D S1D{Result =\ndeny?} -->|yes| DENY2([deny]) S1D -->|no| S1E S1E{requiresUser\nInteraction?} -->|"yes + ask"| ASK2([ask]) S1E -->|no| S1F S1F{Content ask\nrule matched?} -->|yes| ASK3([ask]) S1F -->|no| S1G S1G{Safety check\n.git/.claude/\nshell configs?} -->|yes| ASK4([ask]) S1G -->|no| STEP2 end subgraph STEP2["Step 2 — Mode & Allow Rule Checks"] M1{bypassPermissions\nor plan+bypass?} -->|yes| ALLOW1([allow]) M1 -->|no| M2 M2{Tool-wide\nallow rule?} -->|yes| ALLOW2([allow]) M2 -->|no| PASSTHRU PASSTHRU["Convert passthrough → ask"] end STEP1 --> STEP2 PASSTHRU --> STEP3 subgraph STEP3["Step 3 — Mode Transformations (post-ask)"] T1{mode =\ndontAsk?} -->|yes| DENY3([deny]) T1 -->|no| T2 T2{mode = auto\nor auto+plan?} -->|yes| AUTOF T2 -->|no| T3 T3{shouldAvoid\nPrompts?} -->|yes| HOOKS T3 -->|no| ASK5([ask user]) subgraph AUTOF["Auto Mode Fast Paths"] AF1{Non-classifier\nsafety check?} -->|yes| ASK6([ask/deny]) AF1 -->|no| AF2 AF2{acceptEdits\nwould allow?} -->|yes| ALLOW3([allow]) AF2 -->|no| AF3 AF3{Tool on safe\nallowlist?} -->|yes| ALLOW4([allow]) AF3 -->|no| AF4 AF4["classifyYoloAction()"]:::classifier --> AF5 AF5{shouldBlock?} -->|yes| DENY4([deny/ask on limit]) AF5 -->|no| ALLOW5([allow]) end HOOKS["Run PermissionRequest hooks"] --> HOOKD HOOKD{Hook\ndecision?} -->|allow/deny| HD([hook result]) HOOKD -->|none| DENY5([deny — headless]) end classDef classifier fill:#211b14,stroke:#b8965e,color:#b8965e

每个编号的步骤直接对应于 // 1a, 1b, 1c... 中的评论 permissions.ts,使源代码自我记录。

逐步细分
  1. 1a — 整个工具的拒绝规则: getDenyRuleForTool() 检查工具本身是否(例如, "Bash" 没有内容)出现在任何拒绝列表中。 即时 deny 如果找到的话。
  2. 1b — 整个工具的询问规则: 对询问列表进行同样的检查。 当沙盒自动允许打开且命令将被沙盒化时,此步骤将被跳过并进入 checkPermissions.
  3. 1c — 工具自己的 checkPermissions(): 每个工具都实现自己的逻辑(例如,Bash 检查子命令前缀规则,FileWrite 检查工作目录边界)。
  4. 1d — 工具被拒绝许可: If checkPermissions returns deny,立即停止。
  5. 1e — 需要用户交互: 有些工具(ExitPlanMode、AskUserQuestion、ReviewArtifact)始终需要人工操作 — 甚至 bypassPermissions 不能覆盖。
  6. 1f — 特定于内容的询问规则: 像这样的规则 Bash(npm publish:*) 即使在旁路模式下,也会遵守明确要求命令模式的命令。
  7. 1g — 安全检查(旁路免疫): 写信给 .git/, .claude/, .vscode/,无论模式如何,shell 配置文件始终需要用户批准。
  8. 2a — 绕过权限模式: 如果模式是 bypassPermissions (or plan 如果可以使用旁路),则允许通过步骤 1 的所有内容。
  9. 2b — 工具范围允许规则: toolAlwaysAllowedRule() 检查整个工具是否在允许列表中(例如, "Bash" 没有内容)。
  10. 3 — 模式转换: An ask 结果变换为: dontAskdeny; auto → 分类器; 无头 → 钩子 → 自动拒绝。

3. 5种权限模式

该模式控制对任何管道执行哪个分支 ask 结果。 它存储在 toolPermissionContext.mode.

default

标准模式。 针对每个无法识别的工具使用情况向用户显示提示。

plan

只读规划阶段。 写入工具将被阻止,直到用户退出计划模式。

⏵⏵
acceptEdits

自动批准工作目录内的文件编辑。 Shell 命令仍然提示。

⏵⏵
bypassPermissions

跳过所有提示(拒绝规则、内容询问规则、安全检查、requireUserInteraction 除外)。 需要门控访问。

⏵⏵
dontAsk

转换每个 ask 变成一个 deny。 克劳德被告知无法使用该工具。

⏵⏵
自动(仅限 ANT)

Routes ask 通过人工智能分类器而不是人类提示做出决策。 功能标记通过 TRANSCRIPT_CLASSIFIER.

来源:PermissionMode 配置来自 PermissionMode.ts
const PERMISSION_MODE_CONFIG = {
  default:           { title: 'Default',            color: 'text',       external: 'default' },
  plan:              { title: 'Plan Mode',          color: 'planMode',  external: 'plan' },
  acceptEdits:       { title: 'Accept edits',       color: 'autoAccept', external: 'acceptEdits' },
  bypassPermissions: { title: 'Bypass Permissions', color: 'error',      external: 'bypassPermissions' },
  dontAsk:           { title: "Don't Ask",          color: 'error',      external: 'dontAsk' },
  // auto is ANT-only, enabled by feature('TRANSCRIPT_CLASSIFIER')
  auto:              { title: 'Auto mode',          color: 'warning',    external: 'default' },
}

Note: auto 报告为 default 对于外部用户来说——它的存在是内部的。

bypassPermissions 无法绕过什么
  • 步骤 1a — 拒绝规则: 始终受到尊重,无一例外。
  • 步骤 1e — 需要用户交互: ExitPlanMode、AskUserQuestion、ReviewArtifact 始终需要人工。
  • 步骤 1f — 针对特定内容的询问规则: Bash(npm publish:*) 在询问列表中总是提示。
  • 步骤 1g — 安全检查: 写信给 .git/, .claude/, .vscode/,shell 配置始终会提示 — 即使在旁路模式下也是如此。

4. 规则匹配系统

每个规则都是以下形式的字符串 ToolName or ToolName(content),存储在允许/拒绝/询问列表中。 解析由以下处理: permissionRuleParser.ts.

规则字符串格式

// Tool-wide rule — no content
"Bash"                 // match any Bash command
"mcp__myserver"        // match all tools from myserver MCP
"mcp__myserver__*"     // same, wildcard variant

// Content-specific rules (three types)
"Bash(npm install)"    // exact match
"Bash(npm:*)"          // legacy prefix — matches anything starting with "npm"
"Bash(git add *)"      // wildcard — * matches any sequence of chars
"Bash(git ad\* file)"  // escaped * — matches literal asterisk

三种内容规则类型(shellRuleMatching.ts)

exact

修剪后的完整字符串相等。 Bash(npm install)

前缀(旧版)

结束于 :*。 之前的部分 : 必须是命令的前缀。 Bash(npm:*) matches npm install, npm run build, ETC。

wildcard

包含未转义的 *。 转换为完整的正则表达式 dotAll flag. Bash(git * --dry-run)

通配符模式边缘情况:尾随 *

当模式结束时  * (空格+单个通配符),匹配引擎使尾部部分可选:

// Rule: "git *"
// Matches both:
"git add"     // command with argument
"git"         // bare command — optional trailing match
// This aligns wildcard semantics with legacy prefix "git:*"

多通配符模式 (* run *)被排除在此优化之外,以避免错误匹配。

规则解析代码(permissionRuleParser.ts)
// "Bash(python -c \"print\\(1\\)\")"  →
function permissionRuleValueFromString(ruleString) {
  const openIdx  = findFirstUnescapedChar(ruleString, '(')
  const closeIdx = findLastUnescapedChar(ruleString, ')')

  if (openIdx === -1) return { toolName: normalizeLegacyToolName(ruleString) }

  const toolName   = ruleString.substring(0, openIdx)
  const rawContent = ruleString.substring(openIdx + 1, closeIdx)

  // Empty or standalone "*" → treat as tool-wide rule
  if (rawContent === '' || rawContent === '*')
    return { toolName: normalizeLegacyToolName(toolName) }

  return { toolName: normalizeLegacyToolName(toolName), ruleContent: unescapeRuleContent(rawContent) }
}
旧版工具名称别名

用户配置中的旧规则字符串会在解析时自动迁移:

const LEGACY_TOOL_NAME_ALIASES = {
  Task:            AGENT_TOOL_NAME,     // → "Agent"
  KillShell:       TASK_STOP_TOOL_NAME, // → "TaskStop"
  AgentOutputTool: TASK_OUTPUT_TOOL_NAME,
  BashOutputTool:  TASK_OUTPUT_TOOL_NAME,
}

5. 规则来源和优先级

规则从多个源加载并合并。 什么时候 allowManagedPermissionRulesOnly 设置于 policySettings,所有非政策来源都被忽略。

Source 文件/来源 共享? 可编辑吗?
policySettings 企业管理政策 Yes 否(只读)
projectSettings .claude/settings.json (committed) 是的——git Yes
userSettings ~/.claude/settings.json No Yes
localSettings .claude/settings.local.json (gitignored) No Yes
flagSettings --settings CLI 标志 No 否(只读)
cliArg CLI 启动参数 No 否(运行时)
session 内存中(仅此会话) No 否(短暂)
command 斜线命令 frontmatter Yes 否(只读)

设置 JSON 格式

{
  "permissions": {
    "allow": ["Bash(npm:*)", "Bash(git status)"],
    "deny":  ["WebFetch"],
    "ask":   ["Bash(npm publish:*)"]
  }
}

6. 自动模式和人工智能分类器

当模式为 auto,Claude 没有提示用户,而是通过辅助 Claude 模型(“YOLO 分类器”)路由不明确的工具使用,该模型根据描述允许和禁止的操作类别的系统提示来决定允许/拒绝。

快速路径(跳过分类器 API 调用)

  1. 不可分类的安全检查: 写信给 .git/ 或 shell 配置 - 即使分类器也不会自动批准。
  2. 接受编辑快速路径: 如果该工具被允许进入 acceptEdits 模式(CWD 内的文件编辑),立即允许而不调用分类器。
  3. 安全工具白名单: 只读工具(FileRead、Grep、Glob、LSP、TodoWrite、sleep 等)完全跳过分类器。 该列表位于 classifierDecision.ts.
安全工具白名单(删节自 classifierDecision.ts)
const SAFE_YOLO_ALLOWLISTED_TOOLS = new Set([
  // Read-only file operations
  FILE_READ_TOOL_NAME,
  GREP_TOOL_NAME, GLOB_TOOL_NAME, LSP_TOOL_NAME,
  TOOL_SEARCH_TOOL_NAME, LIST_MCP_RESOURCES_TOOL_NAME,
  // Task management (metadata only)
  TODO_WRITE_TOOL_NAME,
  TASK_CREATE_TOOL_NAME, TASK_GET_TOOL_NAME, TASK_UPDATE_TOOL_NAME,
  TASK_LIST_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_TOOL_NAME,
  // Plan mode / UI
  ASK_USER_QUESTION_TOOL_NAME,
  ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME,
  // Misc
  SLEEP_TOOL_NAME,
  YOLO_CLASSIFIER_TOOL_NAME, // the classifier itself
])

拒绝追踪

分类器跟踪连续和完全拒绝以检测失控循环。 当达到限制时,它会回退到提示用户:

const DENIAL_LIMITS = {
  maxConsecutive: 3,   // 3 denials in a row → prompt user
  maxTotal:      20,   // 20 total denials this session → prompt user
}

function shouldFallbackToPrompting(state) {
  return state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive
      || state.totalDenials       >= DENIAL_LIMITS.maxTotal
}

成功使用工具会重置连续计数(但不会重置总数)。

进入自动模式时去除危险图案

进入自动模式时, permissionSetup.ts strips 允许允许 Claude 完全绕过分类器的规则:

const DANGEROUS_BASH_PATTERNS = [
  // Script interpreters (arbitrary code execution)
  'python', 'node', 'deno', 'ruby', 'perl', 'php', 'lua',
  // Package runners
  'npx', 'bunx', 'npm run', 'yarn run', 'bun run',
  // Shells & execution multipliers
  'bash', 'sh', 'eval', 'exec', 'sudo', 'xargs',
  // ... plus ANT-only: gh, curl, wget, git, kubectl, aws, gcloud
]

激活自动模式时,匹配这些模式的现有允许规则将从上下文中删除,确保分类器看到所有 shell 命令。

分类器失败模式

失败关闭(默认)

分类器 API 错误 → 拒绝重试指导。 控制者 tengu_iron_gate_closed 成长书门。

成绩单太长

回退到正常权限提示(交互模式)或抛出 AbortError(无头模式)。

打开失败(门关闭)

如果铁门功能关闭,分类器错误将退回到正常权限处理而不是拒绝。

7. 影子规则检测

权限规则可以是 unreachable - 写得正确但从未评估过,因为更通用的规则首先被触发。 shadowedRuleDetection.ts 检测并报告这些冲突。

Deny-shadowed

工具范围的拒绝规则(例如, Bash 在拒绝列表中)制定任何特定的允许规则(例如, Bash(ls:*)) 无法访问——拒绝首先被触发。

Ask-shadowed

工具范围的询问规则(例如, Bash 在询问列表中)始终提示用户,制定特定的允许规则(例如, Bash(ls:*)) 无法访问。 Exception: 如果询问规则来自个人设置并且沙箱自动允许已打开,则不会发出警告。

// Example: allow rule is deny-shadowed
permissions.deny  = ["Bash"]      // ← fires first, blocks everything
permissions.allow = ["Bash(ls:*)"] // ← never reached → shadowed!

// Fix: remove the tool-wide "Bash" from deny,
// or add more specific deny rules instead.

8. 权限解释器

当出现权限提示时, permissionExplainer.ts 进行侧面 API 调用(使用当前模型)以生成人类可读的解释,说明命令的作用、Claude 为何要运行它以及风险级别。

// Structured output schema returned by the explainer model
{
  explanation: "What this command does (1-2 sentences)",
  reasoning:   "I need to check the file contents",  // starts with "I"
  risk:        "Modifies files outside the working directory",
  riskLevel:   "HIGH"  // LOW | MEDIUM | HIGH
}
  • Uses sideQuery() — 单独的 API 调用,不计入主会话的令牌总数。
  • 该模型被迫使用特定工具(explain_command)以保证结构化输出。
  • 可以通过禁用 permissionExplainerEnabled: false 在全局配置中。
  • 包括最多 1,000 个字符的最近对话上下文,因此“为什么”答案基于 Claude 正在做的事情。

9. 要点

  • 拒绝规则总是首先检查 — 在旁路模式之前、在允许规则之前、在特定于工具的逻辑之前。 它们是唯一的无条件块。
  • bypassPermissions 并不是真正无条件的 - 它无法绕过拒绝规则、特定于内容的询问规则、受保护路径的安全检查或需要用户交互的工具。
  • 安全检查路径在设计上不受旁路影响.git/, .claude/, .vscode/,并且 shell 配置文件始终提示,防止静默配置损坏。
  • 规则的特殊性对影子很重要 — 工具范围内的拒绝或询问规则会默默地使该工具的所有特定允许规则无法访问。 阴影探测器对此发出警告。
  • 自动模式具有分层快速路径 — 安全白名单 → 接受编辑检查 → 分类器。 只有最后一步需要 API 调用。
  • 分类器拒绝限制可防止无限循环 — 在连续 3 次或总共 20 次分类器拒绝后,系统会回退到提示人类。
  • 规则源形成分层覆盖系统 — 企业策略可以完全锁定规则 allowManagedPermissionRulesOnly,防止任何用户覆盖。
  • 旧规则名称会自动迁移 — 使用旧工具名称的规则(Task, KillShell) 在解析时被透明地重写为当前规范名称。

10. 测验

测试你的理解力。 为每个问题选择最佳答案。

Q1. 一个用户有 Bash 在他们的拒绝名单中并且 Bash(ls:*) 在他们的允许列表中。 当克劳德试图逃跑时会发生什么 ls -la?

Q2。 在 bypassPermissions 模式下,哪一个仍会提示用户?

Q3。 自动模式分类器本次连续 3 次拒绝。 第四次暧昧行动会发生什么?

Q4。 规则字符串 "Bash(npm:*)" 使用什么规则类型?

Q5. 什么是 dontAsk 权限模式与 bypassPermissions?