Skip to content

权限管线:Agent 的护栏

上一节讲了工具系统——Agent 在一次会话中可能调用几十次工具,每次调用都潜藏风险:一个 rm -rf,一次意外的 npm publish,都可能造成不可逆的后果。

权限管线(Permission Pipeline)正是为 Agent 构建的安全护栏。它不是简单的"允许/拒绝"开关,而是一条多阶段检查管线。 每一层独立运作,即使某一层被绕过,下一层仍能拦截。这就是"纵深防御":在自动化效率与安全控制之间寻找精确的平衡。

权限管线:Agent 的护栏

权限检查管线

每次工具调用都进入权限检查管线,它执行固定的决策顺序,第一个匹配的步骤做出决策后,后续步骤跳过:

第一步:格式验证

这一层检查工具调用参数的格式问题:

  • 缺少必填参数——如 Bash 工具缺少 command 字段
  • 类型不匹配——如 file_path 应该是字符串却传了数字
  • 意外参数——传了工具定义中不存在的字段

第二步:规则匹配(deny → ask → allow)

系统按严格顺序检查权限规则:

text
deny:在拒绝名单上      -> 立即拒绝,工具甚至不会出现在 Claude 的上下文中
ask:被标记为"总是询问" -> 强制弹出确认,hook 也无法跳过
allow:在放行名单上     -> 直接通过

铁律:优先级为 deny > ask > allow。无论规则来自全局配置、项目配置还是命令行参数,只要有一个地方配置了高优先级的规则,就会覆盖其他地方的配置

第三步:工具自定义权限检查(checkPermissions

每个工具可以实现自己的 checkPermissions(input, context) 方法,返回四种判断之一:

判断含义
allow放行
deny拒绝
ask要求人类确认,后续 allow 规则无法覆盖
passthrough无意见,交给后续关卡,可被 allow 规则覆盖

passthroughask 的关键区别:前者是"我不管,后续决定",可被 allow 覆盖;后者是"我要求人类确认",不可被 allow 覆盖。

Bash 工具的权限检查最为复杂,详见文末「Bash 工具权限检查详解」章节。

第四步:安全路径保护

对核心目录的写入操作具有分类器免疫——无论分类器如何判断,都必须人工确认(bypassPermissions 除外):

  • .git/ 目录
  • .claude/ 目录(但 .claude/commands.claude/agents.claude/skills.claude/worktrees 除外)
  • .vscode/.idea/.husky/ 目录
  • .gitconfig.bashrc.zshrc 等配置文件

第五步:模式特定处理

根据当前权限模式做最终裁决:

  • bypassPermissions:全部放行,不进入任何人工确认窗口
  • auto:进入 AI 分类器审查(见下文)
  • dontAsk:将 ask 转为 deny,完全非交互式
  • default / acceptEdits:进入人工确认窗口
  • plan:只读操作直接执行,写入操作进入人工确认窗口

如果所有关卡均未终局,系统进入第五步人工窗口。

第六步:人工窗口

系统弹出交互提示。但"人工窗口"里的决策者不止一个——实际上有多个角色参与决策:

  • Hook 脚本——通过 PreToolUse hook 执行的自动审批逻辑,在权限提示之前运行。比如 CI/CD 环境中,Hook 脚本可根据自定义规则自动批准或拒绝。
  • 用户——终端界面手动选择"允许/拒绝/本次允许",可将决策持久化到配置文件。
  • AI 分类器——auto 模式下,另一个 AI 模型判断当前操作是否安全("AI 监督 AI")。

对于 headless/异步 agent(无法显示交互提示),系统会运行 PermissionRequest hooks 作为替代决策通道,如果无 hook 做决定则自动拒绝。

多角色可能同时响应,但权限请求只能被一个决策者认领,先到先得

注意:安全路径检查(第四步)对 Hook 的 allow 决定免疫——即使 Hook 批准了,写入 .git/ 等核心目录仍需人工确认。

六种权限模式

权限模式控制"关卡有多严格",从严格到宽松排列:

default:逐次确认

每次写入或命令执行都需用户确认(被 allow 规则放行的除外),只读操作(ReadGreplscat 等)直接执行。最安全也最繁琐。

acceptEdits:自动批准编辑

自动接受工作目录内的文件编辑和常见文件系统命令(mkdirtouchrmrmdirmvcpsed 等)。超出工作目录的路径、受保护路径的写入、所有其他 Bash 命令仍然会提示。适合迭代开发——先在编辑器或 git diff 中审查更改,而不是逐个批准每个编辑。

plan:只读模式

Claude 读取文件、运行只读 shell 命令进行探索并编写计划,但不编辑源代码。计划准备好后用户可选择批准执行方式。适用于代码审查和架构分析——先理解再行动。

auto:AI 分类器

用另一个 AI 模型代替人工审批——"AI 监督 AI"。每次分类器调用都是一次 API 请求,有延迟、有成本,所以系统按顺序做了几层处理:

前置处理:广泛的 allow 规则被临时删除。 进入 auto 模式时,全局性的允许规则(如 Bash(*)Bash(python*)、包管理器 run 命令、Agent 允许规则)被移除,离开时恢复。防止宽泛规则绕过分类器。

第一层:安全工具白名单快速通道。 约 20 个被标记为"安全"的工具直接跳过分类器检查,不浪费 API 调用。包括: 只读工具(FileReadGrep等)、任务管理(TodoWrite等)、规划模式(AskUserQuestion等)、Agent Team 协调工具(TeamCreate等)

第二层:分类器两阶段判断。 没被白名单跳过的操作,交给分类器。分类器收到三样东西:

  • 对话历史的压缩摘要(只包含用户的原始输入和 Agent 的工具调用,不包含 Agent 自己生成的文字——防止 Agent 用自己的输出来"说服"分类器)
  • 当前要执行的操作
  • 用户的 allow/deny 规则和 CLAUDE.md 配置

分类器本身分两步走:

  1. 快速阶段:先让模型给出即时的"放行/阻止",只分配极少 token(64 个)。如果快速阶段说"放行",直接通过。
  2. 深度阶段:快速阶段说"阻止"时才触发。给模型更多 token(4096 个)展开思考链,减少误判——快速阶段倾向于"宁可错杀",深度阶段给模型机会重新审视。

第三层:熔断兜底。 如果分类器连续 3 次拒绝,或本次会话累计拒绝 20 次,系统自动回退到人工确认——就像保险丝,当自动决策连续失败时,切断自动审批,回到人类把关。任何允许的操作会重置连续计数器。

贯穿全程的安全底线:核心安全目录的操作是分类器免疫的,分类器无权批准,必须人工确认。

分类器默认行为。 分类器的核心逻辑是:信任工作目录和仓库配置的 remote,其他一切视为外部。以下是内置规则:

行为权限
下载并执行代码(如 curl | bash拦截
向外部端点发送敏感数据拦截
生产部署和迁移拦截
云存储大规模删除拦截
授予 IAM 或仓库权限拦截
修改共享基础设施拦截
不可逆地销毁会话开始前就存在的文件拦截
强制推送,或直接推送到 main拦截
工作目录内的本地文件操作放行
安装 lockfile/manifest 中声明的依赖放行
读取 .env 并向对应 API 发送凭证放行
只读 HTTP 请求放行
推送到你启动的分支或 Claude 创建的分支放行

如果常规操作被误拦,管理员可通过 autoMode.environment 配置添加受信任的仓库、桶和服务。

dontAsk:仅允许预先批准的工具

自动拒绝每个会提示的工具调用。仅与 permissions.allow 规则和只读 Bash 命令匹配的操作可以执行;显式 ask 规则被拒绝而不是提示。完全非交互式,适合 CI 管道或受限环境。

bypassPermissions:绕过权限

跳过所有权限提示和安全检查,工具调用立即执行。受保护路径的写入也被允许,不再有人工确认窗口。

适用于 CI/CD 和受控环境(容器、VM、无网络访问的 dev container)。

规则如何生效和记住:权限更新的两层机制

用户在权限提示中有两种选择:

  • "仅允许一次"——规则只写入内存,本次会话有效,关闭后消失。
  • "始终允许"——规则写入内存(立即生效)的同时,也写入配置文件(持久化),重启后仍然有效。

两者的共同点是内存即时生效,区别只在是否持久化到文件。持久化失败不影响当前会话——"现在能正常用"比"将来一定能记住"更紧急。

规则来源与优先级

从低到高:

来源作用范围
托管设置(管理员)无法被任何其他级别覆盖
命令行参数本次会话临时覆盖
用户设置(~/.claude/settings.json本机所有项目
共享项目设置(.claude/settings.json提交到版本控制,团队共享
本地项目设置(.claude/settings.local.json不提交版本控制,仅自己

铁律:如果工具在任何级别被拒绝,没有其他级别可以允许它。用户级别的 deny 会阻止项目级别的 allow

配置建议

  • 团队共享:项目设置(.claude/settings.json,提交版本控制)
  • 个人偏好:本地项目设置(.claude/settings.local.json,不提交)
  • 临时需求:"仅允许一次"(会话级规则,不污染任何配置文件)

Bash 工具权限检查详解

工具可以实现自己的 checkPermissions(input, context) 方法,这里讲解最复杂的工具 Bash。

为什么 Bash 这么特殊?因为一行 Bash 命令本质上就是一段程序——它可能包含管道、重定向、变量替换、条件执行、循环体等多种结构。如果只用简单的字符串匹配来判断权限,攻击者只需加一个 | cat /etc/passwd 就能绕过 Bash(git *) 的放行规则。

因此 Bash 工具的权限检查不走"正则匹配"这条路,而是走了完整的语法分析 → 语义检查 → 规则匹配流程。

但需要明确,没有任何权限系统能防止"所有"攻击手段。Claude Code 的 Bash 防护本质上是启发式模式匹配——针对已知的攻击模式设检查点,无法覆盖未知或变种的攻击。

AST 语法树解析:从"字符串"到"结构"

Bash 工具的第一步是用 tree-sitter 将命令字符串解析为抽象语法树。这一步的意义是把命令从文本变成结构化的数据,后续所有检查都基于 AST 而不是正则。

举例来说,当 Agent 调用:

git status && npm test | grep PASS

经过 AST 解析后,系统看到的不是字符串,而是一棵树:

复合命令 (&&)
├── 左子树: git status
│   └── 子命令: git [status]
└── 右子树: npm test | grep PASS
    └── 管道 (|)
        ├── 左: npm [test]
        └── 右: grep [PASS]

有了这棵树,后续的每一步检查都可以精确地定位到每个操作符、每个子命令、每个路径。

沙箱自动放行:OS 级隔离替代权限提示

沙箱(sandbox)是操作系统级别的隔离机制,限制命令能访问的文件和网络。当沙箱启用且自动放行开关打开(默认开启)时,系统会拆分子命令,逐个检查 deny 和 ask 规则 → 命中则拒绝或者弹窗。否则直接放行

具体参考:Claude Code Docs: 配置沙箱化 Bash 工具

进程包装器剥离:让规则匹配实际命令

用户配置的规则通常针对的是实际命令,而不是被各种包装器包裹后的形式。系统在匹配规则前会剥离一组安全的进程包装器和安全环境变量:

进程包装器(共 5 个,不可配置):

包装器支持的变体示例
timeoutGNU 长/短 flag(--foreground--kill-after--signal-v-k-stimeout 30 npm testnpm test
time无前缀time make buildmake build
nicenice -n Nnice -N、无前缀nice -n 5 git statusgit status
stdbuf短 flag 组合(-o0-iL-eLstdbuf -o0 npm testnpm test
nohup无前缀nohup npm run buildnpm run build

安全环境变量(30+ 个,按类别分,只影响显示格式、日志级别等表层行为,不改变实际命令执行):

类别变量
Go 编译GOOSGOARCHCGO_ENABLEDGO111MODULEGOEXPERIMENT
Rust 日志RUST_BACKTRACERUST_LOG
Node/PythonNODE_ENVPYTHONUNBUFFEREDPYTHONDONTWRITEBYTECODEPYTEST_*
终端/显示TERMCOLORTERMNO_COLORFORCE_COLORTZLANGLC_*CHARSET
颜色配置LS_COLORSLSCOLORSGREP_COLORGREP_COLORSGCC_COLORS
格式化TIME_STYLEBLOCK_SIZEBLOCKSIZE
认证ANTHROPIC_API_KEY

注意 NODE_OPTIONSPYTHONPATH 等能改变程序行为的变量不在安全列表中,不会被剥离。

子命令拆分:复合命令不能"一揽子"通过

这是 Bash 权限检查的核心安全原则:复合命令的每个子命令必须独立通过检查

举例来说,如果用户配置了 Bash(git *) 放行所有 git 命令:

命令结果原因
git status通过匹配 Bash(git *)
git status && npm test不通过npm test 没有匹配到任何放行规则
git status && ls通过git status 匹配 Bash(git *)ls 是只读命令

Bash(safe-cmd *) 不会给 Agent 权限运行 safe-cmd && other-cmd。这防止了通过附加操作符来"搭便车"绕过规则的攻击。

路径约束验证:命令操作的文件安全吗?

AST 解析后,系统提取命令中涉及的所有文件路径,检查它们是否在允许范围内:

命令检查结果原因
rm -rf /tmp/test需要确认删除操作,目标路径需用户确认
rm -rf .git/hooks必须人工确认写入 .git/ 目录,分类器免疫
cat src/index.ts直接执行只读操作,在工作目录内
sed -i 's/old/new/g' .bashrc必须人工确认修改 .bashrc 配置文件

语义安全检查:防止"看似安全"的命令做坏事

即使命令通过了规则匹配,Bash 工具还会进行语义层面的安全检查。核心原理是:一个命令是否安全,不仅取决于命令名,还取决于命令的参数、上下文和 shell 语法特征

bashSecurity.ts 内置了 20+ 种检查模式,覆盖了命令注入、参数混淆、路径绕过等攻击向量。以下是几个代表性例子:

cd + git 组合cd /tmp/malicious && git status 会被拦截。攻击者可以在恶意目录放置裸 git 仓库,通过 core.fsmonitor 配置让 git 执行任意命令。

命令注入git commit -m "$(cat /etc/passwd)" 中的命令替换会被拦截。变量用于管道或重定向(echo $HOME > /etc/passwd)也会被拦截。

混淆 Flag 绕过rm -rf /(Unicode 编码绕过)、rm"" -rf /(空引号混淆)、git\ \ status(反斜杠转义空格)等试图绕过规则匹配的混淆写法都会被识别。

如果 AST 解析失败或命令结构过于复杂(超过 50 个子命令),系统也会拒绝自动放行——无法证明安全性的命令,默认视为不安全。