Claude Code源码剖析 - 权限系统

md 复制代码
# Phase 5: 权限系统

## 权限模式和行为类型
### PermissionMode 和 PermissionBehavior
### PermissionRule:allow / deny / ask 规则

## 通用权限入口:所有工具执行前的大门
### hasPermissionsToUseToolInner:通用权限判断主流程
### hasPermissionsToUseTool:处理 ask 在不同模式下的命运

## Bash 权限系统:从工具名到命令语义
### bashToolCheckPermission:Bash 单条命令的权限判断
### bashToolHasPermission:完整 Bash 命令的权限入口
### checkCommandAndSuggestRules:权限判断后的规则建议

## Bash 安全检查:为什么 shell 特别难
### bashSecurity.ts 的整体角色
### bashCommandIsSafeAsync:命令注入与误解析检查
### validateDangerousPatterns:危险语法模式
### read-only command:为什么"只读命令"也要小心

## 路径与文件权限约束
### checkPathConstraints:命令访问路径的权限边界
### read / write / sensitive path 的区别
### cd、重定向和路径解析为什么会影响权限

## 权限结果如何回到 Agent Loop
### checkPermissionsAndCallTool:权限不是 allow 时如何生成 tool_result
### 权限拒绝为什么不会让 agent 崩溃
### deny / ask 如何影响下一轮模型推理

## Phase 5 总结:模型提动作,runtime 做裁决
### Phase5 完整调用链
### Python mini-agent 的权限系统抽象

Phase 5: 权限系统

权限模式和行为类型

PermissionMode 和 PermissionBehavior

ts 复制代码
// ============================================================================
// Permission Modes
// ============================================================================

export const EXTERNAL_PERMISSION_MODES = [
  'acceptEdits',
  'bypassPermissions',
  'default',
  'dontAsk',
  'plan',
] as const

export type ExternalPermissionMode = (typeof EXTERNAL_PERMISSION_MODES)[number]

// Exhaustive mode union for typechecking. The user-addressable runtime set
// is INTERNAL_PERMISSION_MODES below.
export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble'
export type PermissionMode = InternalPermissionMode

// Runtime validation set: modes that are user-addressable (settings.json
// defaultMode, --permission-mode CLI flag, conversation recovery).
export const INTERNAL_PERMISSION_MODES = [
  ...EXTERNAL_PERMISSION_MODES,
  ...(feature('TRANSCRIPT_CLASSIFIER') ? (['auto'] as const) : ([] as const)),
] as const satisfies readonly PermissionMode[]

export const PERMISSION_MODES = INTERNAL_PERMISSION_MODES

// ============================================================================
// Permission Behaviors
// ============================================================================

export type PermissionBehavior = 'allow' | 'deny' | 'ask'

权限系统里先分清两个概念:

text 复制代码
PermissionMode:当前会话采用什么权限策略
PermissionBehavior:某一次工具调用最终是 allow / deny / ask

PermissionMode 像全局策略,例如:

text 复制代码
default
plan
acceptEdits
bypassPermissions
dontAsk
auto

PermissionBehavior 是单次工具调用的结果:

text 复制代码
allow:允许执行
deny:拒绝执行
ask:需要用户确认

PermissionMode.ts 本身不负责真正的安全判断。

它主要负责 mode 的解析、外部转换和 UI 展示信息。

可以这样记:

text 复制代码
PermissionMode.ts = 权限模式字典
permissions.ts = 通用权限判断入口
具体 Tool.checkPermissions() = 工具自己的权限判断

通用权限入口:所有工具执行前的大门

hasPermissionsToUseToolInner:通用权限判断主流程

ts 复制代码
async function hasPermissionsToUseToolInner(
  tool: Tool,
  input: { [key: string]: unknown },
  context: ToolUseContext,
): Promise<PermissionDecision> {
  if (context.abortController.signal.aborted) {
    throw new AbortError()
  }

  let appState = context.getAppState()

  // 1. Check if the tool is denied
  // 1a. Entire tool is denied
  const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
  if (denyRule) {
    return {
      behavior: 'deny',
      decisionReason: {
        type: 'rule',
        rule: denyRule,
      },
      message: `Permission to use ${tool.name} has been denied.`,
    }
  }

  // 1b. Check if the entire tool should always ask for permission
  const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
  if (askRule) {
    // When autoAllowBashIfSandboxed is on, sandboxed commands skip the ask rule and
    // auto-allow via Bash's checkPermissions. Commands that won't be sandboxed (excluded
    // commands, dangerouslyDisableSandbox) still need to respect the ask rule.
    const canSandboxAutoAllow =
      tool.name === BASH_TOOL_NAME &&
      SandboxManager.isSandboxingEnabled() &&
      SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&
      shouldUseSandbox(input)

    if (!canSandboxAutoAllow) {
      return {
        behavior: 'ask',
        decisionReason: {
          type: 'rule',
          rule: askRule,
        },
        message: createPermissionRequestMessage(tool.name),
      }
    }
    // Fall through to let Bash's checkPermissions handle command-specific rules
  }

  // 1c. Ask the tool implementation for a permission result
  // Overridden unless tool input schema is not valid
  let toolPermissionResult: PermissionResult = {
    behavior: 'passthrough',
    message: createPermissionRequestMessage(tool.name),
  }
  try {
    const parsedInput = tool.inputSchema.parse(input)
    toolPermissionResult = await tool.checkPermissions(parsedInput, context)
  } catch (e) {
    // Rethrow abort errors so they propagate properly
    if (e instanceof AbortError || e instanceof APIUserAbortError) {
      throw e
    }
    logError(e)
  }

  // 1d. Tool implementation denied permission
  if (toolPermissionResult?.behavior === 'deny') {
    return toolPermissionResult
  }

  // 1e. Tool requires user interaction even in bypass mode
  if (
    tool.requiresUserInteraction?.() &&
    toolPermissionResult?.behavior === 'ask'
  ) {
    return toolPermissionResult
  }

  // 1f. Content-specific ask rules from tool.checkPermissions take precedence
  // over bypassPermissions mode. When a user explicitly configures a
  // content-specific ask rule (e.g. Bash(npm publish:*)), the tool's
  // checkPermissions returns {behavior:'ask', decisionReason:{type:'rule',
  // rule:{ruleBehavior:'ask'}}}. This must be respected even in bypass mode,
  // just as deny rules are respected at step 1d.
  if (
    toolPermissionResult?.behavior === 'ask' &&
    toolPermissionResult.decisionReason?.type === 'rule' &&
    toolPermissionResult.decisionReason.rule.ruleBehavior === 'ask'
  ) {
    return toolPermissionResult
  }

  // 1g. Safety checks (e.g. .git/, .claude/, .vscode/, shell configs) are
  // bypass-immune --- they must prompt even in bypassPermissions mode.
  // checkPathSafetyForAutoEdit returns {type:'safetyCheck'} for these paths.
  if (
    toolPermissionResult?.behavior === 'ask' &&
    toolPermissionResult.decisionReason?.type === 'safetyCheck'
  ) {
    return toolPermissionResult
  }

  // 2a. Check if mode allows the tool to run
  // IMPORTANT: Call getAppState() to get the latest value
  appState = context.getAppState()
  // Check if permissions should be bypassed:
  // - Direct bypassPermissions mode
  // - Plan mode when the user originally started with bypass mode (isBypassPermissionsModeAvailable)
  const shouldBypassPermissions =
    appState.toolPermissionContext.mode === 'bypassPermissions' ||
    (appState.toolPermissionContext.mode === 'plan' &&
      appState.toolPermissionContext.isBypassPermissionsModeAvailable)
  if (shouldBypassPermissions) {
    return {
      behavior: 'allow',
      updatedInput: getUpdatedInputOrFallback(toolPermissionResult, input),
      decisionReason: {
        type: 'mode',
        mode: appState.toolPermissionContext.mode,
      },
    }
  }

  // 2b. Entire tool is allowed
  const alwaysAllowedRule = toolAlwaysAllowedRule(
    appState.toolPermissionContext,
    tool,
  )
  if (alwaysAllowedRule) {
    return {
      behavior: 'allow',
      updatedInput: getUpdatedInputOrFallback(toolPermissionResult, input),
      decisionReason: {
        type: 'rule',
        rule: alwaysAllowedRule,
      },
    }
  }

  // 3. Convert "passthrough" to "ask"
  const result: PermissionDecision =
    toolPermissionResult.behavior === 'passthrough'
      ? {
          ...toolPermissionResult,
          behavior: 'ask' as const,
          message: createPermissionRequestMessage(
            tool.name,
            toolPermissionResult.decisionReason,
          ),
        }
      : toolPermissionResult

  if (result.behavior === 'ask' && result.suggestions) {
    logForDebugging(
      `Permission suggestions for ${tool.name}: ${jsonStringify(result.suggestions, null, 2)}`,
    )
  }

  return result
}

hasPermissionsToUseToolInner() 是工具执行前的通用权限判断入口。

它解决的问题是:

text 复制代码
模型发出了 tool_use,
runtime 在真正执行 tool.call() 之前,
要判断这次工具调用是 allow、deny,还是 ask。

核心流程可以这样记:

text 复制代码
1. 如果整个工具被 deny rule 禁止,直接 deny。
2. 如果整个工具有 ask rule,通常直接 ask。
3. 调用 tool.checkPermissions(parsedInput, context),让具体工具自己判断。
4. 如果工具自己返回 deny,直接 deny。
5. 如果工具要求用户交互,即使 bypass mode 也要 ask。
6. 如果是内容级 ask rule 或 safetyCheck,也优先保留 ask。
7. 如果当前是 bypassPermissions,则 allow。
8. 如果整个工具被 allow rule 允许,则 allow。
9. 如果前面都没决定,把 passthrough 转成 ask。

这里有两个层次的权限判断:

text 复制代码
通用层:
  判断这个工具整体是否被 allow / deny / ask。

工具层:
  调用 tool.checkPermissions(),
  判断这次具体输入是否危险。

例如 Bash:

text 复制代码
Bash 工具本身可能没有被禁用,
但 Bash("rm -rf ...") 这个具体输入仍然需要被拦截。

所以权限系统不是简单的全局开关,而是:

text 复制代码
工具整体规则
+ 工具输入语义判断
+ 当前 permission mode
+ 安全兜底策略

最重要的一点:

text 复制代码
不确定是否安全时,默认 ask。

这就是 agent runtime 的边界:

text 复制代码
模型负责提出动作。
本地 runtime 负责裁决动作能不能执行。

核心思想是:

全局规则先拦一层,工具自己再拦一层,最后不确定就问用户

hasPermissionsToUseTool:处理 ask 在不同模式下的命运

ts 复制代码
export const hasPermissionsToUseTool: CanUseToolFn = async (
  tool,
  input,
  context,
  assistantMessage,
  toolUseID,
): Promise<PermissionDecision> => {
  const result = await hasPermissionsToUseToolInner(tool, input, context)
  ...
}

hasPermissionsToUseTool() 是权限系统的外层入口。

它先调用:

ts 复制代码
hasPermissionsToUseToolInner(tool, input, context)

拿到基础权限结果:

text 复制代码
allow / deny / ask

如果结果是 allow,基本直接返回:

ts 复制代码
 // Reset consecutive denials on any allowed tool use in auto mode.
  // This ensures that a successful tool use (even one auto-allowed by rules)
  // breaks the consecutive denial streak.
  if (result.behavior === 'allow') {
    const appState = context.getAppState()
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      const currentDenialState =
        context.localDenialTracking ?? appState.denialTracking
      if (
        appState.toolPermissionContext.mode === 'auto' &&
        currentDenialState &&
        currentDenialState.consecutiveDenials > 0
      ) {
        const newDenialState = recordSuccess(currentDenialState)
        persistDenialState(context, newDenialState)
      }
    }
    return result
  }

如果结果是 ask,它会根据当前 permission mode 和运行环境继续处理。

ts 复制代码
if (result.behavior === 'ask') {
    const appState = context.getAppState()

    if (appState.toolPermissionContext.mode === 'dontAsk') {
      return {
        behavior: 'deny',
        decisionReason: {
          type: 'mode',
          mode: 'dontAsk',
        },
        message: DONT_ASK_REJECT_MESSAGE(tool.name),
      }
    }
    // Apply auto mode: use AI classifier instead of prompting user
    // Check this BEFORE shouldAvoidPermissionPrompts so classifiers work in headless mode
    if (
      feature('TRANSCRIPT_CLASSIFIER') &&
      (appState.toolPermissionContext.mode === 'auto' ||
        (appState.toolPermissionContext.mode === 'plan' &&
          (autoModeStateModule?.isAutoModeActive() ?? false)))
    ) {
      // Non-classifier-approvable safetyCheck decisions stay immune to ALL
      // auto-approve paths: the acceptEdits fast-path, the safe-tool allowlist,
      // and the classifier. Step 1g only guards bypassPermissions; this guards
      // auto. classifierApprovable safetyChecks (sensitive-file paths) fall
      // through to the classifier --- the fast-paths below naturally don't fire
      // because the tool's own checkPermissions still returns 'ask'.
      if (
        result.decisionReason?.type === 'safetyCheck' &&
        !result.decisionReason.classifierApprovable
      ) {
        if (appState.toolPermissionContext.shouldAvoidPermissionPrompts) {
          return {
            behavior: 'deny',
            message: result.message,
            decisionReason: {
              type: 'asyncAgent',
              reason:
                'Safety check requires interactive approval and permission prompts are not available in this context',
            },
          }
        }
        return result
      }
    }
}

可以这样记:

text 复制代码
hasPermissionsToUseToolInner:
  判断这次工具调用按规则应该 allow / deny / ask。

hasPermissionsToUseTool:
  判断 ask 在当前模式下应该怎么落地。

常见改写有三种:

text 复制代码
dontAsk:
  ask -> deny

auto:
  ask -> 交给安全分类器判断
  classifier allow -> allow
  classifier block -> deny

headless / async agent:
  不能弹权限框
  ask -> hook 决策或 deny

其中 auto 不是无条件放行,而是:

text 复制代码
-> 当前是 auto mode
-> 某些安全检查不能走 auto,保留 ask 或 deny
-> 某些工具要求真人交互,保留 ask
-> acceptEdits 快速路径
-> safe allowlist 快速路径
-> classifyYoloAction(...)
-> classifier block: deny
-> classifier allow: allow

headless / async agent意思是:

如果当前上下文不能弹出权限提示,例如后台 agent 或异步 subagent,那么 ask 没地方问,就不能卡住等待用户,只能走 hook 或自动拒绝

所以权限系统可以分两层理解:

text 复制代码
第一层:规则和工具自身判断
  hasPermissionsToUseToolInner()

第二层:运行模式处理 ask
  hasPermissionsToUseTool()

这也是 agent runtime 很重要的安全边界:

text 复制代码
模型只提出动作。
runtime 先判断动作是否允许。
如果需要用户确认,还要看当前环境能不能确认。

Bash 权限系统:从工具名到命令语义

bashToolCheckPermission:Bash 单条命令的权限判断

ts 复制代码
export const bashToolCheckPermission = (
  input,
  toolPermissionContext,
  compoundCommandHasCd?,
  astCommand?,
): PermissionResult => {
  const command = input.command.trim()
  ...
}

bashToolCheckPermission() 是 Bash 工具自己的权限判断核心。

通用权限系统只能知道:

text 复制代码
模型想调用 Bash 工具

但 Bash 真正的风险在于:

text 复制代码
这条 shell 命令具体做了什么

所以 Bash 必须自己判断 input.command

核心判断顺序:

text 复制代码
exact match rule
-> deny / ask prefix rule
-> path constraints
-> exact allow rule
-> prefix allow rule
-> sed constraints
-> permission mode
-> read-only command
-> passthrough

第一步: 精确匹配规则

ts 复制代码
// 1. Check exact match first
  const exactMatchResult = bashToolCheckExactMatchPermission(
    input,
    toolPermissionContext,
  )

  // 1a. Deny/ask if exact command has a rule
  if (
    exactMatchResult.behavior === 'deny' ||
    exactMatchResult.behavior === 'ask'
  ) {
    return exactMatchResult
  }

如果是规则是Deny或ask就直接返回

第二步: 查prefix规则

ts 复制代码
  // 2. Find all matching rules (prefix or exact)
  // SECURITY FIX: Check Bash deny/ask rules BEFORE path constraints to prevent bypass
  // via absolute paths outside the project directory (HackerOne report)
  // When AST-parsed, the subcommand is already atomic --- skip the legacy
  // splitCommand re-check that misparses mid-word # as compound.
  const { matchingDenyRules, matchingAskRules, matchingAllowRules } =
    matchingRulesForInput(input, toolPermissionContext, 'prefix', {
      skipCompoundCheck: astCommand !== undefined,
    })

  // 2a. Deny if command has a deny rule
  if (matchingDenyRules[0] !== undefined) {
    return {
      behavior: 'deny',
      message: `Permission to use ${BashTool.name} with command ${command} has been denied.`,
      decisionReason: {
        type: 'rule',
        rule: matchingDenyRules[0],
      },
    }
  }

  // 2b. Ask if command has an ask rule
  if (matchingAskRules[0] !== undefined) {
    return {
      behavior: 'ask',
      message: createPermissionRequestMessage(BashTool.name),
      decisionReason: {
        type: 'rule',
        rule: matchingAskRules[0],
      },
    }
  }

prefix规则可以理解为下面这一类按照命令前缀匹配的一类命令

text 复制代码
Bash(git status:*)
Bash(npm test:*)
Bash(rm:*)

第三步: 检查路径约束

ts 复制代码
  // 3. Check path constraints
  // This check comes after deny/ask rules so explicit rules take precedence.
  // SECURITY: When AST-derived argv is available for this subcommand, pass
  // it through so checkPathConstraints uses it directly instead of re-parsing
  // with shell-quote (which has a single-quote backslash bug that causes
  // parseCommandArguments to return [] and silently skip path validation).
  const pathResult = checkPathConstraints(
    input,
    getCwd(),
    toolPermissionContext,
    compoundCommandHasCd,
    astCommand?.redirects,
    astCommand ? [astCommand] : undefined,
  )
  if (pathResult.behavior !== 'passthrough') {
    return pathResult
  }

这一步的意义是,即使命令本身看起来普通,路径也可能越界或敏感,如下

bash 复制代码
cat /etc/passwd
echo hi > ~/.ssh/config

且deny / ask rules 要在 path constraints 之前检查,因为如果顺序错了,可能出现安全规则被路径逻辑绕开的情况

第四、五步: 处理allow

ts 复制代码
  // 4. Allow if command had an exact match allow
  if (exactMatchResult.behavior === 'allow') {
    return exactMatchResult
  }

  // 5. Allow if command has an allow rule
  if (matchingAllowRules[0] !== undefined) {
    return {
      behavior: 'allow',
      updatedInput: input,
      decisionReason: {
        type: 'rule',
        rule: matchingAllowRules[0],
      },
    }
  }

  // 5b. Check sed constraints (blocks dangerous sed operations before mode auto-allow)
  const sedConstraintResult = checkSedConstraints(input, toolPermissionContext)
  if (sedConstraintResult.behavior !== 'passthrough') {
    return sedConstraintResult
  }

allow 放在 deny / ask / path constraints 后面,意思是用户允许某类命令,不代表可以跳过更高优先级的安全限制

第六步: 检查 sed 约束

ts 复制代码
const sedConstraintResult = checkSedConstraints(input, toolPermissionContext)
if (sedConstraintResult.behavior !== 'passthrough') {
  return sedConstraintResult
}

sed 很特殊,因为它既可以读,也可以原地改文件,比如

bash 复制代码
sed -i ...

第七步: 检查是否是只读

ts 复制代码
 // 7. Check read-only rules
  if (BashTool.isReadOnly(input)) {
    return {
      behavior: 'allow',
      updatedInput: input,
      decisionReason: {
        type: 'other',
        reason: 'Read-only command is allowed',
      },
    }
  }

第八步: 交回通用权限层

ts 复制代码
// 8. Passthrough since no rules match, will trigger permission prompt
  const decisionReason = {
    type: 'other' as const,
    reason: 'This command requires approval',
  }
  return {
    behavior: 'passthrough',
    message: createPermissionRequestMessage(BashTool.name, decisionReason),
    decisionReason,
    // Suggest exact match rule to user
    // this may be overridden by prefix suggestions in `checkCommandAndSuggestRules()`
    suggestions: suggestionForExactCommand(command),
  }

最后,如果前面都没有决定,也就是交回通用权限层,最终通常会变成ask

bashToolHasPermission:完整 Bash 命令的权限入口

bashToolHasPermission() 是 Bash 工具权限判断的完整入口。

上一节的 bashToolCheckPermission() 判断的是单条子命令。

但真实 Bash 输入可能是一个完整 shell 程序:

bash 复制代码
cmd1 && cmd2
cmd1 | cmd2
echo hi > file.txt
cd some/path && git status

所以这一节解决的问题是:

text 复制代码
模型请求 Bash(command),
runtime 如何判断整个 command 是否可以安全执行?

核心可以这样记:

text 复制代码
bashToolCheckPermission:
  判断一条子命令。

bashToolHasPermission:
  判断整个 Bash command。

第一步:先尝试用 AST 理解整个命令。

ts 复制代码
export async function bashToolHasPermission(
  input: z.infer<typeof BashTool.inputSchema>,
  context: ToolUseContext,
  getCommandSubcommandPrefixFn = getCommandSubcommandPrefix,
): Promise<PermissionResult> {
  let appState = context.getAppState()

  const injectionCheckDisabled = isEnvTruthy(
    process.env.CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK,
  )

  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' }

这里不是执行命令,而是先判断:

text 复制代码
这个 shell 字符串能不能被可靠理解?

astResult 主要有三种情况:

text 复制代码
simple:能解析成清楚的 SimpleCommand[]
too-complex:结构太复杂,不能安全静态分析
parse-unavailable:AST 不可用,走旧解析路径

Bash 权限判断最怕的不是"命令长",而是 runtime 错误理解命令结构。


第二步:如果 AST 判断太复杂,保守 ask。

ts 复制代码
if (astResult.kind === 'too-complex') {
  const earlyExit = checkEarlyExitDeny(input, appState.toolPermissionContext)
  if (earlyExit !== null) return earlyExit

  const decisionReason: PermissionDecisionReason = {
    type: 'other' as const,
    reason: astResult.reason,
  }

  return {
    behavior: 'ask',
    decisionReason,
    message: createPermissionRequestMessage(BashTool.name, decisionReason),
    suggestions: [],
    ...(feature('BASH_CLASSIFIER')
      ? {
          pendingClassifierCheck: buildPendingClassifierCheck(
            input.command,
            appState.toolPermissionContext,
          ),
        }
      : {}),
  }
}

这里有两个重点:

text 复制代码
1. too-complex 不自动执行,而是 ask。
2. 但如果命中显式 deny,仍然直接 deny。

所以不是:

text 复制代码
复杂命令 -> 一律询问

而是:

text 复制代码
复杂命令 -> 先尊重 deny -> 没有 deny 再 ask

第三步:如果 AST 不可用,走 legacy parse 预检。

ts 复制代码
if (astResult.kind === 'parse-unavailable') {
  const parseResult = tryParseShellCommand(input.command)
  if (!parseResult.success) {
    const decisionReason = {
      type: 'other' as const,
      reason: `Command contains malformed syntax that cannot be parsed: ${parseResult.error}`,
    }
    return {
      behavior: 'ask',
      decisionReason,
      message: createPermissionRequestMessage(BashTool.name, decisionReason),
    }
  }
}

这一步仍然是同一个原则:

text 复制代码
解析不了 -> ask

因为只要 runtime 不能可靠理解 Bash 结构,就不能自动放行。


第四步:sandbox 自动允许检查。

ts 复制代码
if (
  SandboxManager.isSandboxingEnabled() &&
  SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&
  shouldUseSandbox(input)
) {
  const sandboxAutoAllowResult = checkSandboxAutoAllow(
    input,
    appState.toolPermissionContext,
  )
  if (sandboxAutoAllowResult.behavior !== 'passthrough') {
    return sandboxAutoAllowResult
  }
}

sandbox 是一个更安全的执行环境。

但它不是绕过权限的后门。

这里的 checkSandboxAutoAllow() 仍然会尊重显式 deny / ask 规则。

可以这样记:

text 复制代码
sandbox 可以降低风险,
但不能覆盖明确的权限规则。

第五步:检查 Bash prompt rule 分类器。

ts 复制代码
const exactMatchResult = bashToolCheckExactMatchPermission(
  input,
  appState.toolPermissionContext,
)

if (exactMatchResult.behavior === 'deny') {
  return exactMatchResult
}

if (isClassifierPermissionsEnabled()) {
  const denyDescriptions = getBashPromptDenyDescriptions(
    appState.toolPermissionContext,
  )
  const askDescriptions = getBashPromptAskDescriptions(
    appState.toolPermissionContext,
  )

  const [denyResult, askResult] = await Promise.all([
    hasDeny ? classifyBashCommand(input.command, getCwd(), denyDescriptions, 'deny', ...) : null,
    hasAsk ? classifyBashCommand(input.command, getCwd(), askDescriptions, 'ask', ...) : null,
  ])

这里处理的是"描述型权限规则"。

普通规则可能是:

text 复制代码
Bash(git status)
Bash(npm test:*)

而 prompt rule 更像:

text 复制代码
拒绝所有发布包的命令
询问所有修改 git history 的命令

这种规则不能只靠字符串匹配,所以要用 classifyBashCommand() 判断。

后面的优先级仍然是:

text 复制代码
deny > ask
ts 复制代码
if (denyResult?.matches && denyResult.confidence === 'high') {
  return {
    behavior: 'deny',
    message: `Denied by Bash prompt rule: "${denyResult.matchedDescription}"`,
    decisionReason: {
      type: 'other',
      reason: `Denied by Bash prompt rule: "${denyResult.matchedDescription}"`,
    },
  }
}

if (askResult?.matches && askResult.confidence === 'high') {
  return {
    behavior: 'ask',
    message: createPermissionRequestMessage(BashTool.name),
    decisionReason: {
      type: 'other',
      reason: `Required by Bash prompt rule: "${askResult.matchedDescription}"`,
    },
    suggestions,
  }
}

第六步:处理管道、重定向等 shell operator。

ts 复制代码
const commandOperatorResult = await checkCommandOperatorPermissions(
  input,
  (i: z.infer<typeof BashTool.inputSchema>) =>
    bashToolHasPermission(i, context, getCommandSubcommandPrefixFn),
  { isNormalizedCdCommand, isNormalizedGitCommand },
  astRoot,
)

这一层处理:

bash 复制代码
cmd1 | cmd2
cmd1 && cmd2
echo hi > file.txt

注意这里把 bashToolHasPermission() 自己传进去。

说明 operator 检查可能会递归检查每一段命令。

如果 operator 检查返回 allow,源码还会回头检查原始命令:

ts 复制代码
if (commandOperatorResult.behavior === 'allow') {
  const safetyResult =
    astSubcommands === null
      ? await bashCommandIsSafeAsync(input.command)
      : null

  const pathResult = checkPathConstraints(
    input,
    getCwd(),
    appState.toolPermissionContext,
    commandHasAnyCd(input.command),
    astRedirects,
    astCommands,
  )
  if (pathResult.behavior !== 'passthrough') {
    return pathResult
  }
}

这个细节非常关键:

text 复制代码
子命令看起来安全,不代表原始命令安全。

例如:

bash 复制代码
echo hello > sensitive-file

如果拆分后只看:

bash 复制代码
echo hello

它像是安全命令。

但原始命令里有 >,这是写文件动作。

所以源码必须重新检查原始 input.command 的路径和重定向。


第七步:拆成 subcommands,并处理 cd 特殊情况。

ts 复制代码
const rawSubcommands =
  astSubcommands ?? shadowLegacySubs ?? splitCommand(input.command)

const { subcommands, astCommandsByIdx } = filterCdCwdSubcommands(
  rawSubcommands,
  astCommands,
  cwd,
  cwdMingw,
)

这里开始进入子命令层:

text 复制代码
完整 Bash command
-> 拆成多个 subcommands

如果子命令太多,保守 ask:

ts 复制代码
if (
  astSubcommands === null &&
  subcommands.length > MAX_SUBCOMMANDS_FOR_SECURITY_CHECK
) {
  return {
    behavior: 'ask',
    message: createPermissionRequestMessage(BashTool.name, decisionReason),
    decisionReason,
  }
}

然后检查 cd

ts 复制代码
const cdCommands = subcommands.filter(subCommand =>
  isNormalizedCdCommand(subCommand),
)

if (cdCommands.length > 1) {
  return {
    behavior: 'ask',
    decisionReason,
    message: createPermissionRequestMessage(BashTool.name, decisionReason),
  }
}

多个 cd 会让路径语义变复杂,所以需要用户确认。

还有一个特殊风险是 cd + git

ts 复制代码
const compoundCommandHasCd = cdCommands.length > 0

if (compoundCommandHasCd) {
  const hasGitCommand = subcommands.some(cmd =>
    isNormalizedGitCommand(cmd.trim()),
  )
  if (hasGitCommand) {
    return {
      behavior: 'ask',
      decisionReason,
      message: createPermissionRequestMessage(BashTool.name, decisionReason),
    }
  }
}

因为 git 的行为依赖当前目录。

cd 到某个目录后再执行 git,风险和单独执行 git status 不一样。


第八步:每个子命令调用 bashToolCheckPermission。

ts 复制代码
const subcommandPermissionDecisions = subcommands.map((command, i) =>
  bashToolCheckPermission(
    { command },
    appState.toolPermissionContext,
    compoundCommandHasCd,
    astCommandsByIdx[i],
  ),
)

这就是和上一节的连接:

text 复制代码
bashToolHasPermission()
  -> 拆成 subcommands
  -> bashToolCheckPermission(subcommand)

这里还把 compoundCommandHasCd 传进去。

原因是单个子命令可能不知道前面发生过 cd

例如:

bash 复制代码
cd .claude && echo x > settings.json

如果只看第二段:

bash 复制代码
echo x > settings.json

它不知道当前目录已经变了。


第九步:合并子命令结果。

ts 复制代码
const deniedSubresult = subcommandPermissionDecisions.find(
  _ => _.behavior === 'deny',
)
if (deniedSubresult !== undefined) {
  return {
    behavior: 'deny',
    message: `Permission to use ${BashTool.name} with command ${input.command} has been denied.`,
    decisionReason: {
      type: 'subcommandResults',
      reasons: new Map(
        subcommandPermissionDecisions.map((result, i) => [
          subcommands[i]!,
          result,
        ]),
      ),
    },
  }
}

合并时先看 deny:

text 复制代码
任意子命令 deny -> 整体 deny

然后再次检查原始命令路径:

ts 复制代码
const pathResult = checkPathConstraints(
  input,
  getCwd(),
  appState.toolPermissionContext,
  compoundCommandHasCd,
  astRedirects,
  astCommands,
)
if (pathResult.behavior === 'deny') {
  return pathResult
}

这里再次强调:

text 复制代码
子命令检查会丢掉某些原始命令信息,
尤其是重定向。

所以原始命令必须再查一次。


第十步:allow / ask / passthrough 的最终合并。

ts 复制代码
const askSubresult = subcommandPermissionDecisions.find(
  _ => _.behavior === 'ask',
)
const nonAllowCount = count(
  subcommandPermissionDecisions,
  _ => _.behavior !== 'allow',
)

if (pathResult.behavior === 'ask' && askSubresult === undefined) {
  return pathResult
}

if (askSubresult !== undefined && nonAllowCount === 1) {
  return {
    ...askSubresult,
    ...(feature('BASH_CLASSIFIER')
      ? {
          pendingClassifierCheck: buildPendingClassifierCheck(
            input.command,
            appState.toolPermissionContext,
          ),
        }
      : {}),
  }
}

这里的意思是:

text 复制代码
如果只有一个地方需要 ask,就直接返回那个 ask。

如果所有子命令都允许,并且没有命令注入风险:

ts 复制代码
if (
  subcommandPermissionDecisions.every(_ => _.behavior === 'allow') &&
  !hasPossibleCommandInjection
) {
  return {
    behavior: 'allow',
    updatedInput: input,
    decisionReason: {
      type: 'subcommandResults',
      reasons: new Map(
        subcommandPermissionDecisions.map((result, i) => [
          subcommands[i]!,
          result,
        ]),
      ),
    },
  }
}

也就是说:

text 复制代码
所有子命令 allow
+ 没有注入风险
= 整体 allow

如果还有不确定的部分,就进入建议规则收集:

ts 复制代码
const collectedRules: Map<string, PermissionRuleValue> = new Map()

for (const [subcommand, permissionResult] of subcommandResults) {
  if (
    permissionResult.behavior === 'ask' ||
    permissionResult.behavior === 'passthrough'
  ) {
    const updates =
      'suggestions' in permissionResult
        ? permissionResult.suggestions
        : undefined

    const rules = extractRules(updates)
    for (const rule of rules) {
      const ruleKey = permissionRuleValueToString(rule)
      collectedRules.set(ruleKey, rule)
    }
  }
}

最后返回:

ts 复制代码
return {
  behavior: askSubresult !== undefined ? 'ask' : 'passthrough',
  message: createPermissionRequestMessage(BashTool.name, decisionReason),
  decisionReason,
  suggestions: suggestedUpdates,
  ...(feature('BASH_CLASSIFIER')
    ? {
        pendingClassifierCheck: buildPendingClassifierCheck(
          input.command,
          appState.toolPermissionContext,
        ),
      }
    : {}),
}

这一节可以这样记:

text 复制代码
bashToolHasPermission =
  Bash 完整命令的安全裁判。

它的主线是:

text 复制代码
先解析整个命令
-> 解析不可靠就 ask
-> 检查 sandbox / prompt rule / operator
-> 拆成 subcommands
-> 每个 subcommand 调 bashToolCheckPermission
-> 任意 deny 则整体 deny
-> 原始命令再做路径检查
-> 全部 allow 且无注入风险才 allow
-> 其他情况 ask 或 passthrough

最重要的理解是:

text 复制代码
Bash 权限系统不能只看命令名。
它必须理解整个 shell 字符串的结构。

尤其要记住:

text 复制代码
子命令安全,不代表原始命令安全。

因为原始命令可能包含:

text 复制代码
重定向写文件
管道组合
cd 后路径变化
命令替换
解析器差异

checkCommandAndSuggestRules:权限判断后的规则建议

checkCommandAndSuggestRules() 解决的问题是:

text 复制代码
当 Bash 命令没有被直接 allow / deny / ask 时,
runtime 要不要给用户一个"以后允许类似命令"的建议规则?

它不是最底层的权限判断函数。

它是在 bashToolHasPermission() 拆出子命令后,用来做二次判断和 suggestions 生成的。


第一步:先检查 exact match。

ts 复制代码
export async function checkCommandAndSuggestRules(
  input: z.infer<typeof BashTool.inputSchema>,
  toolPermissionContext: ToolPermissionContext,
  commandPrefixResult: CommandPrefixResult | null | undefined,
  compoundCommandHasCd?: boolean,
  astParseSucceeded?: boolean,
): Promise<PermissionResult> {
  // 1. Check exact match first
  const exactMatchResult = bashToolCheckExactMatchPermission(
    input,
    toolPermissionContext,
  )
  if (exactMatchResult.behavior !== 'passthrough') {
    return exactMatchResult
  }

这里先看完整命令是否已经有精确规则。

例如:

text 复制代码
Bash(npm test)
Bash(git status)

如果 exact match 已经返回 allow / deny / ask,就直接返回。

只有它是 passthrough,才继续往下判断。

可以这样记:

text 复制代码
已有精确规则 -> 直接尊重
没有精确规则 -> 继续检查 prefix / 安全风险 / 建议规则

第二步:调用 bashToolCheckPermission() 做命令权限判断。

ts 复制代码
  // 2. Check the command prefix
  const permissionResult = bashToolCheckPermission(
    input,
    toolPermissionContext,
    compoundCommandHasCd,
  )
  // 2a. Deny/ask if command was explictly denied/asked
  if (
    permissionResult.behavior === 'deny' ||
    permissionResult.behavior === 'ask'
  ) {
    return permissionResult
  }

这里回到了上一节读过的单条命令权限判断。

bashToolCheckPermission() 会检查:

text 复制代码
prefix deny / ask / allow
path constraints
sed constraints
permission mode
read-only command
passthrough

如果结果已经是 denyask,说明这条命令已经有明确风险或明确规则,不需要再生成新的建议,直接返回。


第三步:检查命令注入风险。

ts 复制代码
  // 3. Ask for permission if command injection is detected. Skip when the
  // AST parse already succeeded --- tree-sitter has verified there are no
  // hidden substitutions or structural tricks, so the legacy regex-based
  // validators (backslash-escaped operators, etc.) would only add FPs.
  if (
    !astParseSucceeded &&
    !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK)
  ) {
    const safetyResult = await bashCommandIsSafeAsync(input.command)

    if (safetyResult.behavior !== 'passthrough') {
      const decisionReason: PermissionDecisionReason = {
        type: 'other' as const,
        reason:
          safetyResult.behavior === 'ask' && safetyResult.message
            ? safetyResult.message
            : 'This command contains patterns that could pose security risks and requires approval',
      }

      return {
        behavior: 'ask',
        message: createPermissionRequestMessage(BashTool.name, decisionReason),
        decisionReason,
        suggestions: [], // Don't suggest saving a potentially dangerous command
      }
    }
  }

这段很关键。

如果 AST 解析已经成功,说明 tree-sitter 已经确认没有隐藏替换或结构技巧,就跳过 legacy regex 检查,避免误报。

如果 AST 没成功,就调用:

ts 复制代码
bashCommandIsSafeAsync(input.command)

检查命令注入、解析误判等风险。

如果发现风险,返回 ask,并且:

ts 复制代码
suggestions: []

这表示:

text 复制代码
危险或可疑命令可以让用户临时确认,
但不建议保存成长期 allow rule。

这是一个很重要的安全设计。


第四步:如果命令已经 allow,直接返回。

ts 复制代码
  // 4. Allow if command was allowed
  if (permissionResult.behavior === 'allow') {
    return permissionResult
  }

如果 bashToolCheckPermission() 已经判断命令可以允许,就不需要生成额外建议。


第五步:生成建议规则。

ts 复制代码
  // 5. Suggest prefix if available, otherwise exact command
  const suggestedUpdates = commandPrefixResult?.commandPrefix
    ? suggestionForPrefix(commandPrefixResult.commandPrefix)
    : suggestionForExactCommand(input.command)

  return {
    ...permissionResult,
    suggestions: suggestedUpdates,
  }
}

走到这里,通常说明命令还没有明确 allow / deny / ask,结果多半是 passthrough

这时函数会给用户建议一个可保存的权限规则:

text 复制代码
如果能提取 command prefix -> 建议 prefix rule
否则 -> 建议 exact command rule

例如:

text 复制代码
npm test -- --watch

可能建议保存成:

text 复制代码
Bash(npm test:*)

而不是保存完整命令。

这样下次参数稍微变化时,规则仍然可复用。

CommandPrefixResult 的结构很简单:

ts 复制代码
export type CommandPrefixResult = {
  /** The detected command prefix, or null if no prefix could be determined */
  commandPrefix: string | null
}

建议规则的生成逻辑在 suggestionForExactCommand() 里:

ts 复制代码
function suggestionForExactCommand(command: string): PermissionUpdate[] {
  // Heredoc commands contain multi-line content that changes each invocation,
  // making exact-match rules useless (they'll never match again). Extract a
  // stable prefix before the heredoc operator and suggest a prefix rule instead.
  const heredocPrefix = extractPrefixBeforeHeredoc(command)
  if (heredocPrefix) {
    return sharedSuggestionForPrefix(BashTool.name, heredocPrefix)
  }

  // Multiline commands without heredoc also make poor exact-match rules.
  // Saving the full multiline text can produce patterns containing `:*` in
  // the middle, which fails permission validation and corrupts the settings
  // file. Use the first line as a prefix rule instead.
  if (command.includes('\n')) {
    const firstLine = command.split('\n')[0]!.trim()
    if (firstLine) {
      return sharedSuggestionForPrefix(BashTool.name, firstLine)
    }
  }

  // Single-line commands: extract a 2-word prefix for reusable rules.
  // Without this, exact-match rules are saved that never match future
  // invocations with different arguments.
  const prefix = getSimpleCommandPrefix(command)
  if (prefix) {
    return sharedSuggestionForPrefix(BashTool.name, prefix)
  }

  return sharedSuggestionForExactCommand(BashTool.name, command)
}

这里可以看出它不是总建议 exact command。

优先级是:

text 复制代码
heredoc 命令 -> 提取 heredoc 前面的稳定 prefix
多行命令 -> 用第一行作为 prefix
普通单行命令 -> 尽量提取 2-word prefix
实在提不出 prefix -> 才保存 exact command

这样做是为了避免保存一些"下次永远匹配不上"的规则。


这一节可以这样记:

text 复制代码
checkCommandAndSuggestRules =
  权限判断 + 生成可保存规则建议。

它的主线是:

text 复制代码
exact match
-> bashToolCheckPermission()
-> 命令注入安全检查
-> allow 直接返回
-> passthrough 时生成 suggestions

最重要的点是:

text 复制代码
危险命令可以 ask,
但不建议保存为长期 allow rule。

所以源码里遇到安全风险时会返回:

ts 复制代码
suggestions: []

而普通未匹配命令才会建议:

text 复制代码
prefix rule 或 exact command rule

这说明 Claude Code 的权限系统不只是"是否执行",还会引导用户把临时确认沉淀成更稳定的权限规则

Bash 安全检查:为什么 shell 特别难

bashSecurity.ts 的整体角色

源码位置:

  • src/tools/BashTool/bashSecurity.ts:1-127
  • src/tools/BashTool/bashSecurity.ts:846-903
  • src/tools/BashTool/bashSecurity.ts:2426-2592
  • src/tools/BashTool/bashPermissions.ts:72-88

bashSecurity.ts 解决的问题不是:

text 复制代码
这条 Bash 命令有没有 allow rule?

而是:

text 复制代码
这条 Bash 字符串有没有命令注入、解析误判、重定向、命令替换等安全风险?

bashPermissions.ts 中,实际使用的是:

ts 复制代码
import {
  bashCommandIsSafeAsync_DEPRECATED,
  stripSafeHeredocSubstitutions,
} from './bashSecurity.js'

const bashCommandIsSafeAsync = bashCommandIsSafeAsync_DEPRECATED

名字里有 DEPRECATED,说明它是 legacy regex / shell-quote 路径。

当前主方向是迁移到 tree-sitter AST,但这条路径仍然在 Bash 权限系统里作为安全检查使用。


文件开头先定义危险 shell 模式:

ts 复制代码
// Note: Backtick pattern is handled separately in validateDangerousPatterns
// to distinguish between escaped and unescaped backticks
const COMMAND_SUBSTITUTION_PATTERNS = [
  { pattern: /<\(/, message: 'process substitution <()' },
  { pattern: />\(/, message: 'process substitution >()' },
  { pattern: /=\(/, message: 'Zsh process substitution =()' },
  // Zsh EQUALS expansion: =cmd at word start expands to $(which cmd).
  // `=curl evil.com` → `/usr/bin/curl evil.com`, bypassing Bash(curl:*) deny
  // rules since the parser sees `=curl` as the base command, not `curl`.
  {
    pattern: /(?:^|[\s;&|])=[a-zA-Z_]/,
    message: 'Zsh equals expansion (=cmd)',
  },
  { pattern: /\$\(/, message: '$() command substitution' },
  { pattern: /\$\{/, message: '${} parameter substitution' },
  ...
]

这些模式说明 Bash 权限判断不能只看第一个命令名。

例如:

bash 复制代码
echo $(curl evil.com)
=cat /etc/passwd

表面命令可能看起来普通,但 shell 展开后实际行为会变化。


典型 validator 之一是 validateDangerousPatterns()

ts 复制代码
function validateDangerousPatterns(
  context: ValidationContext,
): PermissionResult {
  const { unquotedContent } = context

  // Special handling for backticks - check for UNESCAPED backticks only
  // Escaped backticks (e.g., \`) are safe and commonly used in SQL commands
  if (hasUnescapedChar(unquotedContent, '`')) {
    return {
      behavior: 'ask',
      message: 'Command contains backticks (`) for command substitution',
    }
  }

  // Other command substitution checks (include double-quoted content)
  for (const { pattern, message } of COMMAND_SUBSTITUTION_PATTERNS) {
    if (pattern.test(unquotedContent)) {
      return { behavior: 'ask', message: `Command contains ${message}` }
    }
  }

  return { behavior: 'passthrough', message: 'No dangerous patterns' }
}

它检查反引号、$()${}、process substitution 等危险展开。

这些不一定绝对恶意,所以返回 ask,不是 deny

另一个典型 validator 是 validateRedirections()

ts 复制代码
function validateRedirections(context: ValidationContext): PermissionResult {
  const { fullyUnquotedContent } = context

  if (/</.test(fullyUnquotedContent)) {
    return {
      behavior: 'ask',
      message:
        'Command contains input redirection (<) which could read sensitive files',
    }
  }

  if (/>/.test(fullyUnquotedContent)) {
    return {
      behavior: 'ask',
      message:
        'Command contains output redirection (>) which could write to arbitrary files',
    }
  }

  return { behavior: 'passthrough', message: 'No redirections' }
}

< 可能读取敏感文件,> 可能写任意文件,所以都需要用户确认。


bashCommandIsSafeAsync_DEPRECATED() 是这组安全检查的入口:

ts 复制代码
export async function bashCommandIsSafeAsync_DEPRECATED(
  command: string,
  onDivergence?: () => void,
): Promise<PermissionResult> {
  // Try to get tree-sitter analysis
  const parsed = await ParsedCommand.parse(command)
  const tsAnalysis = parsed?.getTreeSitterAnalysis() ?? null

  // If no tree-sitter, fall back to sync version
  if (!tsAnalysis) {
    return bashCommandIsSafe_DEPRECATED(command)
  }

  // Run the same security checks but with tree-sitter enriched context.
  ...
}

它先尝试 tree-sitter。

如果拿不到 tree-sitter 分析,就退回 legacy 同步版本。

拿到 tree-sitter 后,会构造 ValidationContext

ts 复制代码
const context: ValidationContext = {
  originalCommand: command,
  baseCommand,
  unquotedContent: withDoubleQuotes,
  fullyUnquotedContent: stripSafeRedirections(fullyUnquoted),
  fullyUnquotedPreStrip: fullyUnquoted,
  unquotedKeepQuoteChars,
  treeSitter: tsAnalysis,
}

这里把同一条命令拆成多个视图:

text 复制代码
originalCommand:原始命令
unquotedContent:去掉单引号,但保留双引号内容
fullyUnquotedContent:去掉引号内容,并剥掉安全重定向
fullyUnquotedPreStrip:剥重定向前的 fullyUnquoted
treeSitter:AST 提供的更准确信息

需要这么多视图,是因为 Bash 中同一个字符在不同 quote 环境下含义不同。


接着按顺序跑 validators:

ts 复制代码
const validators = [
  validateJqCommand,
  validateObfuscatedFlags,
  validateShellMetacharacters,
  validateDangerousVariables,
  validateCommentQuoteDesync,
  validateQuotedNewline,
  validateCarriageReturn,
  validateNewlines,
  validateIFSInjection,
  validateProcEnvironAccess,
  validateDangerousPatterns,
  validateRedirections,
  validateBackslashEscapedWhitespace,
  validateBackslashEscapedOperators,
  validateUnicodeWhitespace,
  validateMidWordHash,
  validateBraceExpansion,
  validateZshDangerousCommands,
  validateMalformedTokenInjection,
]

每个 validator 负责一种 shell 风险。

最后有一个重要设计:不是所有 ask 都一样。

ts 复制代码
const nonMisparsingValidators = new Set([
  validateNewlines,
  validateRedirections,
])

let deferredNonMisparsingResult: PermissionResult | null = null
for (const validator of validators) {
  const result = validator(context)
  if (result.behavior === 'ask') {
    if (nonMisparsingValidators.has(validator)) {
      if (deferredNonMisparsingResult === null) {
        deferredNonMisparsingResult = result
      }
      continue
    }
    return { ...result, isBashSecurityCheckForMisparsing: true as const }
  }
}
if (deferredNonMisparsingResult !== null) {
  return deferredNonMisparsingResult
}

这里区分两类风险:

text 复制代码
non-misparsing ask:
  普通换行、重定向,属于正常 shell 模式,需要 ask。

misparsing ask:
  可能导致 splitCommand / shell-quote 理解错命令结构,更危险。

普通 ask 会先暂存,继续跑后面的 validator。

如果后面发现 misparsing 风险,就优先返回 misparsing ask。

可以这样记:

text 复制代码
bashSecurity.ts =
  Bash 字符串安全分析器。

它不负责判断用户是否授权,
它负责判断这条 shell 字符串是否存在语法级绕过风险。

返回值含义:

text 复制代码
passthrough:
  没发现安全风险,交给权限规则继续判断。

ask:
  发现需要人工确认的 shell 风险。

isBashSecurityCheckForMisparsing:
  这是更危险的解析误判风险,上层要更早拦住。

bashCommandIsSafeAsync:命令注入与误解析检查

源码位置:

  • src/tools/BashTool/bashSecurity.ts:2257-2412
  • src/tools/BashTool/bashSecurity.ts:2426-2592

bashCommandIsSafeAsync_DEPRECATED() 是 Bash 安全检查的入口。

它解决的问题是:

text 复制代码
这条 Bash command 有没有命令注入、解析误判、危险展开、重定向等语法级风险?

注意它不是权限授权器。

它返回 passthrough 只表示:

text 复制代码
安全检查没发现明显 shell 风险,
后续还要交给权限规则判断。

入口先尝试 tree-sitter:

ts 复制代码
export async function bashCommandIsSafeAsync_DEPRECATED(
  command: string,
  onDivergence?: () => void,
): Promise<PermissionResult> {
  // Try to get tree-sitter analysis
  const parsed = await ParsedCommand.parse(command)
  const tsAnalysis = parsed?.getTreeSitterAnalysis() ?? null

  // If no tree-sitter, fall back to sync version
  if (!tsAnalysis) {
    return bashCommandIsSafe_DEPRECATED(command)
  }

  // Run the same security checks but with tree-sitter enriched context.
  ...
}

这里有两条路径:

text 复制代码
有 tree-sitter:
  用 AST 提供的 quote context 做更准确分析。

没有 tree-sitter:
  退回 legacy regex / shell-quote 路径。

然后先拦截控制字符和 shell-quote 已知 bug:

ts 复制代码
if (CONTROL_CHAR_RE.test(command)) {
  return {
    behavior: 'ask',
    message:
      'Command contains non-printable control characters that could be used to bypass security checks',
    isBashSecurityCheckForMisparsing: true,
  }
}

if (hasShellQuoteSingleQuoteBug(command)) {
  return {
    behavior: 'ask',
    message:
      'Command contains single-quoted backslash pattern that could bypass security checks',
    isBashSecurityCheckForMisparsing: true,
  }
}

这里直接标记:

ts 复制代码
isBashSecurityCheckForMisparsing: true

意思是:

text 复制代码
这类风险可能让权限系统"看错命令结构"。

接着构造 ValidationContext

ts 复制代码
const { processedCommand } = extractHeredocs(command, { quotedOnly: true })

const baseCommand = command.split(' ')[0] || ''

const tsQuote = tsAnalysis.quoteContext
const regexQuote = extractQuotedContent(
  processedCommand,
  baseCommand === 'jq',
)

const context: ValidationContext = {
  originalCommand: command,
  baseCommand,
  unquotedContent: withDoubleQuotes,
  fullyUnquotedContent: stripSafeRedirections(fullyUnquoted),
  fullyUnquotedPreStrip: fullyUnquoted,
  unquotedKeepQuoteChars,
  treeSitter: tsAnalysis,
}

同一条命令会被拆成多个视图:

text 复制代码
originalCommand:原始命令
baseCommand:第一个命令词
unquotedContent:去掉单引号,但保留双引号内容
fullyUnquotedContent:去掉引用内容,并剥掉安全重定向
fullyUnquotedPreStrip:剥重定向前的内容
treeSitter:AST 分析结果

需要多个视图,是因为 Bash 中同一个字符在不同 quote 环境下含义不同。


然后先跑 early validators:

ts 复制代码
const earlyValidators = [
  validateEmpty,
  validateIncompleteCommands,
  validateSafeCommandSubstitution,
  validateGitCommit,
]

for (const validator of earlyValidators) {
  const result = validator(context)
  if (result.behavior === 'allow') {
    return {
      behavior: 'passthrough',
      message: ...,
    }
  }
  if (result.behavior !== 'passthrough') {
    return result.behavior === 'ask'
      ? { ...result, isBashSecurityCheckForMisparsing: true as const }
      : result
  }
}

这里最重要的是:

text 复制代码
validator 返回 allow,
主函数也会转成 passthrough。

因为安全检查里的 allow 只表示:

text 复制代码
没有发现 shell 注入风险。

不表示用户已经授权执行。


普通 validators 是 Bash 安全检查的主体:

ts 复制代码
const validators = [
  validateJqCommand,
  validateObfuscatedFlags,
  validateShellMetacharacters,
  validateDangerousVariables,
  validateCommentQuoteDesync,
  validateQuotedNewline,
  validateCarriageReturn,
  validateNewlines,
  validateIFSInjection,
  validateProcEnvironAccess,
  validateDangerousPatterns,
  validateRedirections,
  validateBackslashEscapedWhitespace,
  validateBackslashEscapedOperators,
  validateUnicodeWhitespace,
  validateMidWordHash,
  validateBraceExpansion,
  validateZshDangerousCommands,
  validateMalformedTokenInjection,
]

每个 validator 负责一种 shell 风险。

最后区分普通 ask 和 misparsing ask:

ts 复制代码
const nonMisparsingValidators = new Set([
  validateNewlines,
  validateRedirections,
])

let deferredNonMisparsingResult: PermissionResult | null = null
for (const validator of validators) {
  const result = validator(context)
  if (result.behavior === 'ask') {
    if (nonMisparsingValidators.has(validator)) {
      if (deferredNonMisparsingResult === null) {
        deferredNonMisparsingResult = result
      }
      continue
    }
    return { ...result, isBashSecurityCheckForMisparsing: true as const }
  }
}
if (deferredNonMisparsingResult !== null) {
  return deferredNonMisparsingResult
}

return {
  behavior: 'passthrough',
  message: 'Command passed all security checks',
}

这里的策略是:

text 复制代码
普通 ask 先暂存。
继续跑后面的 validator。
如果发现 misparsing 风险,优先返回 misparsing ask。
如果没有,再返回普通 ask。

这样可以避免普通重定向 > 提前返回,导致后面更危险的解析绕过没有被发现。

可以这样记:

text 复制代码
bashCommandIsSafeAsync =
  Bash 语法安全检查器。

passthrough:
  没发现语法风险,交给权限规则继续判断。

ask:
  发现需要确认的 shell 风险。

ask + isBashSecurityCheckForMisparsing:
  发现解析误判风险,需要更保守处理。

路径与文件权限约束

checkPathConstraints:命令访问路径的权限边界

源码位置:src/tools/BashTool/pathValidation.ts:820-1109

前面讲 Bash 权限时,重点是"这条命令本身是否被规则允许"。

但 shell 命令还有另一个维度:

text 复制代码
命令要访问哪个路径?
这个路径是在工作区内,还是在外部?
是读,还是写?
有没有重定向?
有没有 cd 改变相对路径语义?

这就是 pathValidation.ts 要解决的问题。

先看单条 path command 的验证入口:

ts 复制代码
function validateSinglePathCommand(
  cmd: string,
  cwd: string,
  toolPermissionContext: ToolPermissionContext,
  compoundCommandHasCd?: boolean,
): PermissionResult {
  const strippedCmd = stripSafeWrappers(cmd)

  const extractedArgs = parseCommandArguments(strippedCmd)
  if (extractedArgs.length === 0) {
    return {
      behavior: 'passthrough',
      message: 'Empty command - no paths to validate',
    }
  }

  const [baseCmd, ...args] = extractedArgs
  if (!baseCmd || !SUPPORTED_PATH_COMMANDS.includes(baseCmd as PathCommand)) {
    return {
      behavior: 'passthrough',
      message: `Command '${baseCmd}' is not a path-restricted command`,
    }
  }

  const operationTypeOverride =
    baseCmd === 'sed' && sedCommandIsAllowedByAllowlist(strippedCmd)
      ? ('read' as FileOperationType)
      : undefined

  const pathChecker = createPathChecker(
    baseCmd as PathCommand,
    operationTypeOverride,
  )
  return pathChecker(args, cwd, toolPermissionContext, compoundCommandHasCd)
}

这段做了几件事:

  1. 先剥掉安全 wrapper,例如 timeout 10 rm -rf / 不能只看到 timeout
  2. 解析命令参数。
  3. 只处理支持路径检查的命令,比如 cdlsfindrmsed 等。
  4. 判断这个命令对应 read / write / create 哪种文件操作。
  5. 把路径参数交给对应的 pathChecker

这里最重要的是第一步:

ts 复制代码
const strippedCmd = stripSafeWrappers(cmd)

如果不剥 wrapper,危险命令可以伪装成:

bash 复制代码
timeout 10 rm -rf ~/.ssh

权限系统如果只看第一个词 timeout,就会漏掉真正的 rm

源码还提供了 AST 版本:

ts 复制代码
function validateSinglePathCommandArgv(
  cmd: SimpleCommand,
  cwd: string,
  toolPermissionContext: ToolPermissionContext,
  compoundCommandHasCd?: boolean,
): PermissionResult {
  const argv = stripWrappersFromArgv(cmd.argv)
  ...
  const pathChecker = createPathChecker(...)
  return pathChecker(args, cwd, toolPermissionContext, compoundCommandHasCd)
}

它不再用 shell-quote 重新解析字符串,而是使用 tree-sitter 已经解析好的 argv。

原因是源码注释写得很明确:

text 复制代码
Avoids the shell-quote single-quote backslash bug.

也就是说,路径校验也要避免"解析器看错命令"。


checkPathConstraints() 还专门检查输出重定向:

ts 复制代码
const { redirections, hasDangerousRedirection } = astRedirects
  ? astRedirectsToOutputRedirections(astRedirects)
  : extractOutputRedirections(input.command)

if (hasDangerousRedirection) {
  return {
    behavior: 'ask',
    message: 'Shell expansion syntax in paths requires manual approval',
    decisionReason: {
      type: 'other',
      reason: 'Shell expansion syntax in paths requires manual approval',
    },
  }
}

const redirectionResult = validateOutputRedirections(
  redirections,
  cwd,
  toolPermissionContext,
  compoundCommandHasCd,
)
if (redirectionResult.behavior !== 'passthrough') {
  return redirectionResult
}

为什么重定向要单独看?

因为这类命令表面上可能像读:

bash 复制代码
echo hello > ~/.ssh/authorized_keys

真正危险的不是 echo,而是 > 后面的写入目标。

所以 Claude Code 不能只判断 base command,还要扫描 shell 语法里的输出路径。

最后,它会遍历所有子命令:

ts 复制代码
if (astCommands) {
  for (const cmd of astCommands) {
    const result = validateSinglePathCommandArgv(
      cmd,
      cwd,
      toolPermissionContext,
      compoundCommandHasCd,
    )
    if (result.behavior === 'ask' || result.behavior === 'deny') {
      return result
    }
  }
} else {
  const commands = splitCommand_DEPRECATED(input.command)
  for (const cmd of commands) {
    const result = validateSinglePathCommand(
      cmd,
      cwd,
      toolPermissionContext,
      compoundCommandHasCd,
    )
    if (result.behavior === 'ask' || result.behavior === 'deny') {
      return result
    }
  }
}

return {
  behavior: 'passthrough',
  message: 'All path commands validated successfully',
}

可以这样记:

text 复制代码
checkPathConstraints =
  看 shell 命令里的路径副作用。

它不是最终 allow,
而是发现路径风险时提前 ask / deny;
没发现就 passthrough 给后面的权限规则继续判断。

read / write / sensitive path 的区别

源码位置:

  • src/utils/permissions/filesystem.ts:1030-1194
  • src/utils/permissions/filesystem.ts:1205-1235
  • src/utils/permissions/filesystem.ts:1479-1645
  • src/utils/permissions/pathValidation.ts:141-230

文件路径权限里有一个关键区分:

text 复制代码
read:读取文件或目录
edit:修改已有文件
create:创建新文件或目录

先看读权限入口:

ts 复制代码
export function checkReadPermissionForTool(
  tool: Tool,
  input: { [key: string]: unknown },
  toolPermissionContext: ToolPermissionContext,
): PermissionDecision {
  if (typeof tool.getPath !== 'function') {
    return {
      behavior: 'ask',
      message: `Claude requested permissions to use ${tool.name}, but you haven't granted it yet.`,
    }
  }
  const path = tool.getPath(input)
  const pathsToCheck = getPathsForPermissionCheck(path)
  ...
}

这里要求工具必须能告诉权限系统"我要访问哪个 path"。

这就是 Phase3 里 Tool.getPath() 的意义:不是 UI 辅助字段,而是权限判断输入。

读权限判断的顺序很保守:

ts 复制代码
// READ-SPECIFIC deny rules first
const denyRule = matchingRuleForInput(..., 'read', 'deny')
if (denyRule) return { behavior: 'deny', ... }

// READ-SPECIFIC ask rules
const askRule = matchingRuleForInput(..., 'read', 'ask')
if (askRule) return { behavior: 'ask', ... }

// Edit access implies read access
const editResult = checkWritePermissionForTool(...)
if (editResult.behavior === 'allow') return editResult

// Allow reads in working directories
if (pathInAllowedWorkingPath(...)) return { behavior: 'allow', ... }

// Internal harness readable paths
const internalReadResult = checkReadableInternalPath(...)
if (internalReadResult.behavior !== 'passthrough') return internalReadResult

// Explicit allow rules
const allowRule = matchingRuleForInput(..., 'read', 'allow')
if (allowRule) return { behavior: 'allow', ... }

// Default ask
return { behavior: 'ask', ... }

这里的顺序很重要:

text 复制代码
read deny / ask
要早于
edit implies read

否则用户显式拒绝读取某个路径,却因为有编辑权限而被绕过。


写权限入口是:

ts 复制代码
export function checkWritePermissionForTool<Input extends AnyObject>(
  tool: Tool<Input>,
  input: z.infer<Input>,
  toolPermissionContext: ToolPermissionContext,
  precomputedPathsToCheck?: readonly string[],
): PermissionDecision {
  if (typeof tool.getPath !== 'function') {
    return {
      behavior: 'ask',
      message: `Claude requested permissions to use ${tool.name}, but you haven't granted it yet.`,
    }
  }
  const path = tool.getPath(input)

  const pathsToCheck =
    precomputedPathsToCheck ?? getPathsForPermissionCheck(path)
  for (const pathToCheck of pathsToCheck) {
    const denyRule = matchingRuleForInput(
      pathToCheck,
      toolPermissionContext,
      'edit',
      'deny',
    )
    if (denyRule) {
      return {
        behavior: 'deny',
        message: `Permission to edit ${path} has been denied.`,
        decisionReason: { type: 'rule', rule: denyRule },
      }
    }
  }
  ...
}

写权限通常比读权限更严格,因为它会改变真实文件系统。

源码还给一些内部路径开了特殊通道:

ts 复制代码
export function checkEditableInternalPath(
  absolutePath: string,
  input: { [key: string]: unknown },
): PermissionResult {
  const normalizedPath = normalize(absolutePath)

  if (isSessionPlanFile(normalizedPath)) {
    return {
      behavior: 'allow',
      updatedInput: input,
      decisionReason: {
        type: 'other',
        reason: 'Plan files for current session are allowed for writing',
      },
    }
  }

  if (isScratchpadPath(normalizedPath)) { ... }
  if (isAgentMemoryPath(normalizedPath)) { ... }
  if (isAutoMemPath(normalizedPath)) { ... }
  if (project .claude/launch.json) { ... }

  return { behavior: 'passthrough', message: '' }
}

这些内部路径不是"随便写",而是 Claude Code 自己运行需要的受控目录,例如:

  • 当前 session 的 plan 文件。
  • scratchpad。
  • agent memory。
  • 自动 memory 文件。
  • 桌面 preview 的 .claude/launch.json

读内部路径也有类似函数:

ts 复制代码
export function checkReadableInternalPath(
  absolutePath: string,
  input: { [key: string]: unknown },
): PermissionResult {
  const normalizedPath = normalize(absolutePath)

  if (isSessionMemoryPath(normalizedPath)) {
    return {
      behavior: 'allow',
      updatedInput: input,
      decisionReason: {
        type: 'other',
        reason: 'Session memory files are allowed for reading',
      },
    }
  }

  if (isProjectDirPath(normalizedPath)) { ... }
  if (isSessionPlanFile(normalizedPath)) { ... }
  ...
}

可以这样记:

text 复制代码
普通工作区路径:
  读通常可自动允许。
  写通常要看 mode / rule。

工作区外路径:
  默认 ask。

敏感路径:
  即使 mode 宽松,也可能 safetyCheck ask。

内部 runtime 路径:
  只对明确用途开小口子。

cd、重定向和路径解析为什么会影响权限

Bash 里的路径不是静态字符串。

这些写法都会改变权限判断难度:

bash 复制代码
cd /tmp && rm file
echo data > output.txt
echo data > "$TARGET"
timeout 5 rm -rf ./dist

所以源码里反复出现几个防御点:

text 复制代码
stripSafeWrappers:
  去掉 timeout / nice / nohup / time 等 wrapper,看到真实命令。

compoundCommandHasCd:
  标记整条复合命令里是否出现 cd。

astRedirects:
  用 tree-sitter 提取重定向目标,避免 shell-quote 误解析。

getPathsForPermissionCheck:
  同时检查原始路径和解析 symlink 后的路径。

checkPathSafetyForAutoEdit:
  对 .git、.claude、shell 配置、credential 等敏感文件做额外安全检查。

这里的核心思想是:

text 复制代码
权限系统不能只看"命令字符串长什么样"。
它必须尽量还原 shell 执行时真正会访问的路径。

权限结果如何回到 Agent Loop

checkPermissionsAndCallTool:权限不是 allow 时如何生成 tool_result

源码位置:src/services/tools/toolExecution.ts:599-733

源码位置:src/services/tools/toolExecution.ts:916-1104

Phase1 里我们讲过:工具结果必须作为 user/tool_result 回到下一轮 messages。

权限拒绝也是一样。

checkPermissionsAndCallTool() 里,工具调用先做 schema 校验:

ts 复制代码
const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
  return [
    {
      message: createUserMessage({
        content: [
          {
            type: 'tool_result',
            content: `<tool_use_error>InputValidationError: ${errorContent}</tool_use_error>`,
            is_error: true,
            tool_use_id: toolUseID,
          },
        ],
        toolUseResult: `InputValidationError: ${parsedInput.error.message}`,
        sourceToolAssistantUUID: assistantMessage.uuid,
      }),
    },
  ]
}

注意,这里不是 throw 给整个 agent loop。

它构造了一个错误 tool_result,让模型下一轮自己修正调用参数。

接着调用工具自己的 validateInput()

ts 复制代码
const isValidCall = await tool.validateInput?.(
  parsedInput.data,
  toolUseContext,
)
if (isValidCall?.result === false) {
  return [
    {
      message: createUserMessage({
        content: [
          {
            type: 'tool_result',
            content: `<tool_use_error>${isValidCall.message}</tool_use_error>`,
            is_error: true,
            tool_use_id: toolUseID,
          },
        ],
        toolUseResult: `Error: ${isValidCall.message}`,
        sourceToolAssistantUUID: assistantMessage.uuid,
      }),
    },
  ]
}

这说明 Claude Code 区分了两类失败:

text 复制代码
schema validation:
  模型生成的参数类型不对。

tool.validateInput:
  参数类型对,但具体值不适合执行。

然后进入 hooks 和权限解析:

ts 复制代码
const resolved = await resolveHookPermissionDecision(
  hookPermissionResult,
  tool,
  processedInput,
  toolUseContext,
  canUseTool,
  assistantMessage,
  toolUseID,
)
const permissionDecision = resolved.decision
processedInput = resolved.input

如果最终不是 allow:

ts 复制代码
if (permissionDecision.behavior !== 'allow') {
  let errorMessage = permissionDecision.message

  const messageContent: ContentBlockParam[] = [
    {
      type: 'tool_result',
      content: errorMessage,
      is_error: true,
      tool_use_id: toolUseID,
    },
  ]

  resultingMessages.push({
    message: createUserMessage({
      content: messageContent,
      toolUseResult: `Error: ${errorMessage}`,
      sourceToolAssistantUUID: assistantMessage.uuid,
    }),
  })

  return resultingMessages
}

这就是权限系统接回 agent loop 的关键:

text 复制代码
deny / ask 没有被用户批准
-> 不是程序崩溃
-> 构造 user message
-> content 里放 tool_result
-> is_error: true
-> 下一轮模型看到错误原因

权限拒绝为什么不会让 agent 崩溃

如果权限拒绝直接抛异常,agent loop 会变成:

text 复制代码
模型请求工具
-> 本地拒绝
-> 程序异常
-> 对话断掉

Claude Code 采用的是另一种设计:

text 复制代码
模型请求工具
-> 本地拒绝
-> 拒绝原因作为 tool_result
-> 模型根据拒绝原因换方案

这很符合 agent 系统的闭环:

text 复制代码
tool_use 不一定成功。
tool_result 可以是成功结果,也可以是结构化失败。
失败本身也是模型下一步推理的上下文。

所以权限拒绝不是"异常路径",而是 agent loop 的正常分支。

deny / ask 如何影响下一轮模型推理

权限拒绝回到模型后,模型会看到类似:

text 复制代码
Error: Permission to use Bash with command ... has been denied.

或者:

text 复制代码
Error: Claude requested permissions to read from /some/path,
but you haven't granted it yet.

这会影响下一轮模型:

  • 如果是路径越界,它可能改读工作区内文件。
  • 如果是 Bash 被拒绝,它可能改用 Read/Grep/Glob。
  • 如果是写入被拒绝,它可能先解释计划,等待用户授权。
  • 如果是命令太复杂,它可能拆成更简单的命令。

这就是权限系统在 agent loop 中的真实作用:

text 复制代码
不是单纯阻止模型,
而是把边界反馈给模型,
让模型在边界内继续完成任务。

Phase 5 总结:模型提动作,runtime 做裁决

Phase5 完整调用链

text 复制代码
assistant/tool_use
  name = Bash / Read / Edit / ...
  input = ...

-> toolExecution.checkPermissionsAndCallTool()
  -> inputSchema.safeParse()
  -> tool.validateInput()
  -> PreToolUse hooks
  -> resolveHookPermissionDecision()

-> hasPermissionsToUseTool()
  -> hasPermissionsToUseToolInner()
     -> tool-level deny rule
     -> tool-level ask rule
     -> tool.checkPermissions()
     -> bypassPermissions / plan bypass
     -> always allow rule
     -> mode-specific handling

-> for Bash:
  -> bashToolHasPermission()
     -> tree-sitter / legacy parse
     -> exact deny / ask / allow
     -> sandbox auto allow
     -> command operators
     -> bashCommandIsSafeAsync()
     -> split subcommands
     -> checkPathConstraints()
     -> read-only allow
     -> classifier / permission prompt

-> if allow:
  -> tool.call()

-> if deny / unresolved ask:
  -> createUserMessage({
       content: [{
         type: "tool_result",
         tool_use_id,
         is_error: true,
         content: errorMessage
       }]
     })
  -> append to messages
  -> next model call

Python mini-agent 的权限系统抽象

Phase5 的实践不需要一开始就实现 Claude Code 这么复杂。

可以先抽象三层:

python 复制代码
from enum import Enum
from dataclasses import dataclass

class PermissionBehavior(Enum):
    ALLOW = "allow"
    DENY = "deny"
    ASK = "ask"

class PermissionMode(Enum):
    DEFAULT = "default"
    ACCEPT_EDITS = "accept_edits"
    BYPASS = "bypass"

@dataclass
class PermissionDecision:
    behavior: PermissionBehavior
    reason: str = ""

class PermissionManager:
    def __init__(self, mode: PermissionMode):
        self.mode = mode
        self.deny_patterns = ["rm -rf /", "sudo ", "git push --force"]
        self.read_only_commands = ["ls", "cat", "grep", "rg", "pwd"]

    def check_shell(self, command: str) -> PermissionDecision:
        for pattern in self.deny_patterns:
            if pattern in command:
                return PermissionDecision(
                    PermissionBehavior.DENY,
                    f"Command matches deny pattern: {pattern}",
                )

        base = command.strip().split()[0] if command.strip() else ""
        if base in self.read_only_commands:
            return PermissionDecision(
                PermissionBehavior.ALLOW,
                "Read-only command",
            )

        if self.mode == PermissionMode.BYPASS:
            return PermissionDecision(
                PermissionBehavior.ALLOW,
                "Bypass mode",
            )

        return PermissionDecision(
            PermissionBehavior.ASK,
            "Command requires approval",
        )

然后在工具执行层里统一处理:

python 复制代码
decision = permission_manager.check_shell(command)

if decision.behavior != PermissionBehavior.ALLOW:
    messages.append({
        "role": "user",
        "content": [{
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "is_error": True,
            "content": f"Permission denied: {decision.reason}",
        }],
    })
    return

result = shell_tool.call(command)
messages.append(result.to_tool_result(tool_use.id))

你可以这样记:

text 复制代码
权限系统最小抽象 =
  PermissionMode:当前会话策略
  PermissionBehavior:本次工具调用结果
  PermissionDecision:行为 + 原因
  PermissionManager:集中裁决
  tool_result:把拒绝反馈给模型

Phase5 的核心不是"把危险命令列全",而是理解这个架构分工:

text 复制代码
模型负责提出动作。
工具负责声明能力。
权限系统负责裁决。
执行系统负责真实动作。
message loop 负责把成功或失败都反馈给模型。
相关推荐
yeflx1 小时前
SAM3 多类别实时检测的完整实践
ai
甲维斯1 小时前
Fable5是真·神!用canvas手搓超级玛丽无bug!
人工智能·游戏开发
lulu12165440781 小时前
大模型API聚合平台技术架构深度对比:六大平台协议转换、路由调度与安全治理全解析 - 微元算力(weytoken)
java·人工智能·安全·架构·ai编程
米小虾1 小时前
我与AI的对话:从大模型的知识本质,到具身智能能否催生真正的知识创造者,再到人的教育与成长
人工智能·aigc
测试者家园1 小时前
用 Skills 自动生成测试用例:一套可落地方案
人工智能·测试用例·持续测试·职业和发展·ai赋能·智能化测试
上海达策TECHSONIC1 小时前
零售ERP选型解析:SAP Business One 适配成长型零售企业的核心逻辑
大数据·运维·人工智能·云计算·运维开发·零售
茉莉玫瑰花茶1 小时前
综合案例 - AI 智能租房助手 [ 4 ]
数据库·python·ai·langgraph
浮午1 小时前
腾讯AI应用开发一面实录:13道硬核面试题全解析
人工智能·面试·职场和发展
qcx231 小时前
固定LLM也能自我进化:上海AI Lab Self-Harness论文深度解读 | Agent性能提升60%的秘密
人工智能