注:文章由 codex 整理。

背景#

本文分析的是 chengzhycn/claude-code 在提交 4b9d30f7953273e567a18eb819f4eddd45fcc877 上的源码实现。

源码入口:

  • 仓库:https://github.com/chengzhycn/claude-code/tree/4b9d30f7953273e567a18eb819f4eddd45fcc877
  • 权限模式定义:src/utils/permissions/PermissionMode.ts
  • auto-mode 状态切换:src/utils/permissions/permissionSetup.ts
  • 工具权限主流程:src/utils/permissions/permissions.ts
  • auto-mode classifier:src/utils/permissions/yoloClassifier.ts
  • Bash 权限判定:src/tools/BashTool/bashPermissions.ts
  • Bash 只读命令判定:src/tools/BashTool/readOnlyValidation.ts
  • Bash 注入/语法安全检查:src/tools/BashTool/bashSecurity.ts

核心问题是:Claude Code 怎么在“不每一步都问用户”和“不直接 bypass permissions”之间做折中。源码里的 auto-mode 不是一个简单的全局 allow,而是一套组合机制:

  1. 进入 auto-mode 时先检查 feature gate 和 opt-in。
  2. 临时剥离会绕过 classifier 的危险 allow rule。
  3. 工具调用先走原有本地权限系统。
  4. 对本地系统仍然需要 ask 的动作,再交给 auto-mode classifier 判断 allow/block。
  5. classifier 不可用、输出不可解析或 transcript 太长时,按场景 fail closed 或回退到人工 permission prompt。

模型#

auto-mode 可以理解成一个“permission prompt 的自动代理”,而不是新的沙箱。它的安全性来自三层:

工具自己的权限检查
  -> 本地静态安全规则:路径、只读命令、deny/ask/allow rule、shell 注入模式
  -> auto-mode 快速路径:acceptEdits 可允许的操作、安全工具 allowlist
  -> auto-mode classifier:读取 transcript + 当前 action,输出 allow/block

这意味着:

  • 已经被明确 deny 的操作不会因为 auto-mode 自动放行。
  • 原本 acceptEdits 可以安全通过的写工作区操作,不需要昂贵 classifier。
  • 读文件、grep、glob、LSP、任务状态等工具被认为是安全工具,可以跳过 classifier。
  • Bash / PowerShell / Agent 这类可扩大影响面的动作,必须被更谨慎地处理。

PermissionMode:auto 是内部模式#

PermissionMode.ts 中,auto 只在 TRANSCRIPT_CLASSIFIER feature 打开时加入模式表。注释还说明 auto 是 ant-only,不属于 external permission mode。也就是说它不是公开 SDK/API 的普通 permission mode,而是受构建 feature 和用户类型约束的内部模式。

auto 的 UI 名称是 Auto mode,外部映射仍然是 default。这点很重要:外部世界看到的可能仍是默认权限模式,但内部权限系统会在 toolPermissionContext.mode === 'auto' 时走 classifier 流程。

进入 auto-mode:先做 gate,再剥离危险规则#

permissionSetup.ts 的状态切换由 transitionPermissionMode(fromMode, toMode, context) 统一处理。进入 auto-mode 的关键逻辑是:

  1. 如果目标模式是 auto,先确认 isAutoModeGateEnabled()
  2. 设置 autoModeActive = true
  3. 调用 stripDangerousPermissionsForAutoMode(context)
  4. 离开 auto-mode 时设置 autoModeActive = false,并调用 restoreDangerousPermissions(context)

这里最关键的是“剥离危险权限”。源码把危险权限定义为:会让动作在 classifier 之前被 auto-allow,从而绕过 auto-mode 安全评估的 allow rule。

Bash 危险权限#

isDangerousBashPermission() 会把这些 Bash 规则视为危险:

  • Bash / Bash(*) / 空 rule content:等价于允许所有 Bash 命令。
  • *:匹配所有命令。
  • 解释器和代码执行类命令的 prefix/wildcard:例如 python:*python*python *python -*

危险模式来自 DANGEROUS_BASH_PATTERNS,语义上覆盖可以执行任意代码的命令族。设计意图很直接:如果用户之前配置过 Bash(python:*),普通模式下可以生效,但 auto-mode 下不能让它绕过 classifier。

PowerShell 危险权限#

isDangerousPowerShellPermission() 的逻辑类似,但 PowerShell 是大小写不敏感,还额外检查:

  • pwshpowershellcmdwsl
  • iex / Invoke-Expression
  • Invoke-Command
  • Start-Process / job / thread job
  • session/event 相关执行入口
  • .NET escape hatch,例如 Add-TypeNew-Object

这说明实现并不只看“命令名是否常见危险”,而是按 shell 的执行模型列出能间接执行代码、创建进程或绕过当前解释器限制的入口。

Agent 权限也被视为危险#

isDangerousTaskPermission() 对 Agent 很保守:任何 Agent allow rule 都危险。原因是 sub-agent 的 prompt 本身可能带来委托攻击,如果 Agent 创建在 classifier 之前就被 allow,会绕过 classifier 对 delegation 行为的判断。

剥离是临时的#

stripDangerousPermissionsForAutoMode() 只改内存中的 ToolPermissionContext,并把被移除的 rule 存进 strippedDangerousRules。离开 auto-mode 时 restoreDangerousPermissions() 会把这些 rule 加回去。

这个设计避免了两类问题:

  • 不破坏用户原有配置。
  • auto-mode 期间不能被宽泛 allow rule 绕过。

工具权限主流程:auto-mode 只接管 ask 分支#

permissions.ts 里,auto-mode 逻辑位于工具自身 checkPermissions() 之后。也就是说每个工具仍然先给出自己的 allow / deny / ask 结果。

当当前模式满足下面条件时,才进入 auto-mode 分支:

  • toolPermissionContext.mode === 'auto'
  • 或者 mode === 'plan'autoModeActive === true

auto-mode 分支处理顺序如下:

  1. 不可 classifier approve 的 safetyCheck 直接保留原结果
    例如某些路径安全检查不是 classifier 可以覆盖的,不能被 acceptEdits fast path、safe-tool allowlist 或 classifier 放行。

  2. 需要用户交互的工具不自动处理
    tool.requiresUserInteraction?.() 且结果是 ask 时直接返回。

  3. PowerShell 默认不进 classifier
    除非 POWERSHELL_AUTO_MODE feature 打开,否则 PowerShell 在 auto-mode 下仍要求显式用户审批。

  4. acceptEdits fast path
    对 Agent / REPL 之外的工具,用临时的 mode: acceptEdits 再跑一次 tool.checkPermissions()。如果 acceptEdits 会 allow,则 auto-mode 直接 allow。

  5. safe tool allowlist
    如果工具名在 SAFE_YOLO_ALLOWLISTED_TOOLS 中,直接 allow。

  6. 调用 auto-mode classifier
    只有前面都没有放行,才把当前 action 和 transcript 交给 classifyYoloAction()

这条链路说明:auto-mode 不会对所有工具调用都发 LLM classifier 请求,而是尽量保留本地确定性判断;classifier 只处理更需要语义判断的剩余动作。

哪些工具被认为足够安全#

classifierDecision.tsSAFE_YOLO_ALLOWLISTED_TOOLS 包括:

  • 文件读取:Read
  • 搜索/只读查询:GrepGlobLSPToolSearchListMcpResourcesReadMcpResourceTool
  • 任务和计划状态:TodoWriteTaskCreateTaskGetTaskUpdateTaskListTaskStopTaskOutput
  • 交互/计划工具:AskUserQuestionEnterPlanModeExitPlanMode
  • team/swarm 内部协调类工具
  • Sleep
  • 内部 classifier 工具自身

源码注释明确说 write/edit tools 不在这个 allowlist,工作区内编辑由 acceptEdits fast path 处理,工作区外或敏感路径则继续走 classifier 或人工审批。

classifier 怎么判断动作是否安全#

yoloClassifier.ts 是 auto-mode 的核心。它不是只看当前命令字符串,而是构造一份压缩 transcript:

  • 用户消息:只保留 text block。
  • assistant 消息:只保留 tool_use block,不保留 assistant text。源码注释说明,assistant text 是模型生成的,可能被构造来影响 classifier。
  • 每个 tool_use 会调用对应工具的 toAutoClassifierInput() 投影成 classifier 需要看的最小信息。
  • 当前 action 会追加到 transcript 末尾。
  • 如果启用 JSONL transcript,则序列化成类似 {"Bash":"ls"}{"user":"..."} 的形式,避免 hostile content 通过换行伪造 transcript 行。

classifier system prompt 由 buildYoloSystemPrompt() 生成。它会把基础 prompt 与 permissions template 拼起来,并合并 settings.autoMode 里的:

  • allow
  • soft_deny
  • environment

这里有一个关键安全边界:getAutoModeConfig() 只从 userSettingslocalSettingsflagSettingspolicySettings 读取,明确排除了 projectSettings。源码注释给出的理由是:恶意项目不应该能注入 classifier allow/deny rule,否则会有 RCE 风险。

classifier 输出#

旧路径使用 tool schema classify_result,要求模型返回:

{
  "thinking": "...",
  "shouldBlock": true,
  "reason": "..."
}

如果没有 tool use block、schema 无法解析,默认 shouldBlock: true,即 blocking for safety。

两阶段 XML classifier#

如果 twoStageClassifier 打开,classifyYoloActionXml() 会使用两阶段:

  1. Stage 1 fast:max_tokens=64,快速输出 <block>yes/no</block>。如果 Stage 1 允许,直接返回 allow。
  2. Stage 2 thinking:只有 Stage 1 block 或配置要求 thinking 时才进入,用更大 token budget 重新判断,降低误杀。

两阶段共享 system prompt 和 transcript 前缀,并使用 prompt caching。源码里的注释说这个模式有三种运行方式:bothfastthinking

XML prompt 是怎么拼出来的#

两阶段 XML classifier 不是另起一套完整 system prompt。它先调用 buildYoloSystemPrompt(context) 生成普通 auto-mode classifier 的 system prompt,然后用 replaceOutputFormatWithXml() 把末尾的 tool-use 输出要求替换成 XML 输出要求。

替换后的输出协议可以概括为:

## Output Format

If the action should be blocked:
<block>yes</block><reason>one short sentence</reason>

If the action should be allowed:
<block>no</block>

Do NOT include a <reason> tag when the action is allowed.
Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No "Looking at..." or similar preamble.

也就是:block 时输出 <block>yes</block> 和一句 reason;allow 时只输出 <block>no</block>;并且整个回复必须直接从 <block> 开始。

这一步的目的,是把原来的 classify_result tool schema 输出,变成更容易低延迟截断的文本 XML 输出。parseXmlBlock() 只关心第一个 <block>yes/no</block>,并且会先剥掉 <thinking>...</thinking>,避免 thinking 内容里的标签干扰最终判定。

Stage 1 fast prompt#

Stage 1 的 user content 结构是:

<transcript>
  compacted user text, assistant tool_use history, and current action
</transcript>

Err on the side of blocking. <block> immediately.

这里的关键是最后一行 XML_S1_SUFFIX。它同时表达两个约束:

  • 保守策略:不确定就 block。
  • 输出策略:立刻输出 <block>,不要展开 reasoning。

配套请求参数也服务于这个目标:

  • max_tokens: both 模式下是 64fast only 模式下是 256,因为 fast-only 需要可能带上 <reason>
  • temperature: 0
  • thinking: 默认禁用;如果模型是 always-on thinking,则只增加 token headroom。
  • stop_sequences: both 模式下设置为 </block>,让模型输出完 <block> 就停;fast only 不设置,因为它可能需要输出 <reason>

因此 Stage 1 的 prompt 不是“让模型完整解释为什么安全”,而是把问题压缩成一个二分类闸门:先用很短输出判断能不能直接放行。

Stage 2 thinking prompt#

Stage 2 复用同样的 XML system prompt 和同样的 <transcript>...</transcript> 包裹方式,但追加的是 XML_S2_SUFFIX

Review the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>.

它的语义是:

  • 重新审查 classification process。
  • 仔细执行安全分类规则。
  • 对应该 block 的动作保持 block。
  • 只有用户显式确认时,才能覆盖 block;暗示性或间接表达不算。
  • 在输出 <block> 前先使用 <thinking>

Stage 2 的请求参数明显更偏“稳”:

  • max_tokens: 4096 加可能的 thinking padding。
  • temperature: 0
  • 不设置 stop_sequences,允许模型先输出 <thinking>,再输出 <block><reason>

两阶段模式里的实际路径是:

Stage 1 fast
  -> <block>no</block>  => 立即 allow,reason = "Allowed by fast classifier"
  -> <block>yes</block> => 进入 Stage 2 thinking 复核
  -> 无法解析          => fast-only 下 block;both 模式下继续 Stage 2

Stage 2 thinking
  -> 解析 <block>yes/no</block>
  -> 解析 <reason>...</reason>
  -> 如果仍无法解析,block for safety

这个设计的取舍是:允许路径要快,拒绝路径要稳。大部分显然安全的动作在 Stage 1 就通过;一旦 Stage 1 倾向 block,就用更长上下文预算和显式 thinking 再看一遍,减少误拒。

classifier 失败时怎么处理#

auto-mode 对失败场景的策略并不是单一的:

  • classifier 输出不可解析:block for safety。
  • classifier 请求被用户 abort:视为 unavailable。
  • transcript 超过上下文窗口:
    • headless / no prompt 场景:直接 abort。
    • 交互场景:回退到正常 permission prompt,让用户手动审批。
  • classifier API 不可用:
    • 如果 tengu_iron_gate_closed 为 true:fail closed,直接 deny 并提示 classifier unavailable。
    • 否则 fail open 到正常 permission handling,而不是自动 allow。
  • classifier 明确 block:返回 deny,reason 类型是 classifier: auto-mode

还有 denial tracking:连续或总拒绝次数过多时,会触发 handleDenialLimitExceeded(),回退到提示用户审查,而不是让 agent 无限重试。

Bash 命令安全判定#

Bash 是 auto-mode 最重要的风险面。源码里的 Bash 权限检查可以分为四层。

1. AST / parser 安全层#

bashToolHasPermission() 首先尝试 tree-sitter Bash 解析。成功时得到 SimpleCommand[]、重定向、subcommand span 等结构;如果发现过于复杂的结构,例如命令替换、扩展、控制流或 parser differential,就返回 ask

如果 tree-sitter 不可用,则走 legacy shell-quote 路径。解析失败时也返回 ask

这个策略的本质是:能证明结构简单才继续细分判断;不能证明就不要自动 allow。

2. shell 注入和语法绕过检查#

bashSecurity.ts 会检查大量容易绕过静态规则的模式,例如:

  • process substitution:<()>()、zsh =()
  • command substitution:$()、反引号
  • ${}$[]
  • zsh glob qualifier、always block
  • PowerShell comment syntax 作为防御性检查
  • zsh 危险命令:zmodloademulatesysopenztcp
  • unsafe redirection patterns
  • IFS injection、控制字符、Unicode whitespace、brace expansion 等

这些检查的目的不是判断命令“业务上是否危险”,而是判断“静态权限匹配是否可信”。一旦发现可能导致解析结果和运行时行为不一致,就要求审批或进入 classifier。

3. 只读命令 allowlist#

readOnlyValidation.tsCOMMAND_ALLOWLISTisCommandSafeViaFlagParsing() 判断某个简单命令是否是只读安全命令。

判断流程包括:

  1. 用 shell parser 拆 token。
  2. 如果包含 pipe、redirect 等 operator,在这个函数内直接返回 false,交给上游分段处理。
  3. 匹配 allowlist,优先支持多词命令,例如 git diff
  4. 针对 git ls-remote 特判,拒绝 URL、SSH remote、变量引用,避免数据外发。
  5. 拒绝任何参数 token 中含 $,因为运行时变量展开会改变真实参数。
  6. 拒绝带 {} 且有 ,.. 的 token,防 brace expansion 绕过。
  7. 校验 flags,只允许声明过的安全 flag。
  8. 没有专用 regex 时,拒绝反引号、grep/rg pattern 中的换行等。

也就是说,lscatgit diff 这类命令不是靠名字直接 allow,而是“命令 + 参数 + flag + shell token 行为”一起满足只读约束才算安全。

4. 路径和复合命令约束#

bashPermissions.ts 还会处理复合命令:

  • checkCommandOperatorPermissions() 对 pipe 分段递归检查。
  • 如果 pipe segment 被 allow,还会回到原始命令检查重定向目标,避免 >> /tmp/file 被分段时丢失。
  • 命令拆分数量超过 MAX_SUBCOMMANDS_FOR_SECURITY_CHECK=50 时返回 ask,避免 ReDoS 或过度复杂的安全检查。
  • 多个 cd 要求审批。
  • cdgit 出现在同一个 compound command 中要求审批,防止 bare repo 的 core.fsmonitor 等攻击。
  • checkPathConstraints() 会验证 cdlsfindrmmvcatgreprggit 等命令里的路径参数。
  • rm / rmdir 针对 /、home、Windows drive root 等危险删除路径会强制 ask,并且不给保存规则建议。

最终,如果所有 subcommand 都被 allow,且没有潜在 command injection,Bash 才返回 allow。否则会返回 ask,并在 BASH_CLASSIFIER feature 打开时附带 pendingClassifierCheck

auto-mode 下 Bash 的整体决策链#

把 Bash 串起来看,执行一个命令时大致是:

BashTool.checkPermissions
  -> bashToolHasPermission
     -> tree-sitter / legacy parser 检查
     -> sandbox auto-allow 检查
     -> exact deny/allow
     -> prompt deny/ask rules(auto-mode 下跳过,由 auto classifier 接管)
     -> pipe / compound command 处理
     -> shell 注入检查
     -> subcommand 权限检查
     -> path constraints
     -> read-only command allowlist
     -> allow / ask / deny
  -> permissions.ts auto-mode 分支
     -> acceptEdits fast path
     -> safe-tool allowlist
     -> classifyYoloAction(transcript + current Bash action)
     -> allow / deny / fallback to prompt

这解释了“如何判定命令是否安全”:本地层判断的是可静态证明的安全性,例如只读命令、路径边界、无 shell 注入、无危险重定向;auto-mode classifier 判断的是语义层安全性,例如用户意图、上下文、当前 action 是否符合任务和安全策略。

设计边界#

从源码看,auto-mode 的边界也很清楚:

  • 它不是 sandbox,本身不能阻止已经被 shell 执行的系统调用;真正限制执行环境还要依赖 SandboxManager 或外部隔离。
  • 它主要接管 permission decision,不替代工具内部的路径/语法检查。
  • 它依赖 transcript projection。工具如果 toAutoClassifierInput() 投影不足,classifier 看到的信息就可能不完整。
  • 它有明确 fast path:acceptEdits 和 safe-tool allowlist 会跳过 classifier。因此安全性依赖这些 fast path 的边界定义是否准确。
  • 它有配置注入防护:autoMode config 不读 projectSettings,但 user/local/flag/policy 仍然能影响 classifier。
  • 对 PowerShell 的支持默认更保守,只有 feature 打开后才像 Bash 一样进入 auto classifier。

总结#

Claude Code 的 auto-mode 是一套“本地确定性权限系统 + LLM 安全 classifier”的混合权限网关。它不是让 agent 自动拥有更大权限,而是在原有 permission prompt 位置增加一个自动判断器。

最值得借鉴的设计有三点:

  1. 进入 auto-mode 前剥离危险 allow rule:否则用户已有的 Bash(*)Bash(python:*) 会直接绕过 classifier。
  2. 静态安全和语义安全分层:Bash 的路径、flag、shell expansion、pipe/redirection 先由本地代码判定;剩余语义问题再交给 classifier。
  3. 失败不等于自动放行:不可解析、classifier error、transcript overflow 都有 fail closed 或回退人工审批的路径。

这个实现的核心判断是:agentic coding 的权限系统不能只靠 allowlist,也不能只靠模型判断。可形式化的部分应该尽量本地确定性处理;需要理解上下文和用户意图的部分,才交给 classifier。