权限管线:Agent 的护栏
上一节讲了工具系统——Agent 在一次会话中可能调用几十次工具,每次调用都潜藏风险:一个 rm -rf,一次意外的 npm publish,都可能造成不可逆的后果。
权限管线(Permission Pipeline)正是为 Agent 构建的安全护栏。它不是简单的"允许/拒绝"开关,而是一条多阶段检查管线。 每一层独立运作,即使某一层被绕过,下一层仍能拦截。这就是"纵深防御":在自动化效率与安全控制之间寻找精确的平衡。
权限检查管线
每次工具调用都进入权限检查管线,它执行固定的决策顺序,第一个匹配的步骤做出决策后,后续步骤跳过:
第一步:格式验证
这一层检查工具调用参数的格式问题:
- 缺少必填参数——如 Bash 工具缺少
command字段 - 类型不匹配——如
file_path应该是字符串却传了数字 - 意外参数——传了工具定义中不存在的字段
第二步:规则匹配(deny → ask → allow)
系统按严格顺序检查权限规则:
deny:在拒绝名单上 -> 立即拒绝,工具甚至不会出现在 Claude 的上下文中
ask:被标记为"总是询问" -> 强制弹出确认,hook 也无法跳过
allow:在放行名单上 -> 直接通过铁律:优先级为 deny > ask > allow。无论规则来自全局配置、项目配置还是命令行参数,只要有一个地方配置了高优先级的规则,就会覆盖其他地方的配置。
第三步:工具自定义权限检查(checkPermissions)
每个工具可以实现自己的 checkPermissions(input, context) 方法,返回四种判断之一:
| 判断 | 含义 |
|---|---|
allow | 放行 |
deny | 拒绝 |
ask | 要求人类确认,后续 allow 规则无法覆盖 |
passthrough | 无意见,交给后续关卡,可被 allow 规则覆盖 |
passthrough 与 ask 的关键区别:前者是"我不管,后续决定",可被 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 脚本——通过
PreToolUsehook 执行的自动审批逻辑,在权限提示之前运行。比如 CI/CD 环境中,Hook 脚本可根据自定义规则自动批准或拒绝。 - 用户——终端界面手动选择"允许/拒绝/本次允许",可将决策持久化到配置文件。
- AI 分类器——auto 模式下,另一个 AI 模型判断当前操作是否安全("AI 监督 AI")。
对于 headless/异步 agent(无法显示交互提示),系统会运行 PermissionRequest hooks 作为替代决策通道,如果无 hook 做决定则自动拒绝。
多角色可能同时响应,但权限请求只能被一个决策者认领,先到先得。
注意:安全路径检查(第四步)对 Hook 的 allow 决定免疫——即使 Hook 批准了,写入 .git/ 等核心目录仍需人工确认。
六种权限模式
权限模式控制"关卡有多严格",从严格到宽松排列:
default:逐次确认
每次写入或命令执行都需用户确认(被 allow 规则放行的除外),只读操作(Read、Grep、ls、cat 等)直接执行。最安全也最繁琐。
acceptEdits:自动批准编辑
自动接受工作目录内的文件编辑和常见文件系统命令(mkdir、touch、rm、rmdir、mv、cp、sed 等)。超出工作目录的路径、受保护路径的写入、所有其他 Bash 命令仍然会提示。适合迭代开发——先在编辑器或 git diff 中审查更改,而不是逐个批准每个编辑。
plan:只读模式
Claude 读取文件、运行只读 shell 命令进行探索并编写计划,但不编辑源代码。计划准备好后用户可选择批准执行方式。适用于代码审查和架构分析——先理解再行动。
auto:AI 分类器
用另一个 AI 模型代替人工审批——"AI 监督 AI"。每次分类器调用都是一次 API 请求,有延迟、有成本,所以系统按顺序做了几层处理:
前置处理:广泛的 allow 规则被临时删除。 进入 auto 模式时,全局性的允许规则(如 Bash(*)、Bash(python*)、包管理器 run 命令、Agent 允许规则)被移除,离开时恢复。防止宽泛规则绕过分类器。
第一层:安全工具白名单快速通道。 约 20 个被标记为"安全"的工具直接跳过分类器检查,不浪费 API 调用。包括: 只读工具(FileRead、Grep等)、任务管理(TodoWrite等)、规划模式(AskUserQuestion等)、Agent Team 协调工具(TeamCreate等)
第二层:分类器两阶段判断。 没被白名单跳过的操作,交给分类器。分类器收到三样东西:
- 对话历史的压缩摘要(只包含用户的原始输入和 Agent 的工具调用,不包含 Agent 自己生成的文字——防止 Agent 用自己的输出来"说服"分类器)
- 当前要执行的操作
- 用户的 allow/deny 规则和 CLAUDE.md 配置
分类器本身分两步走:
- 快速阶段:先让模型给出即时的"放行/阻止",只分配极少 token(64 个)。如果快速阶段说"放行",直接通过。
- 深度阶段:快速阶段说"阻止"时才触发。给模型更多 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 个,不可配置):
| 包装器 | 支持的变体 | 示例 |
|---|---|---|
timeout | GNU 长/短 flag(--foreground、--kill-after、--signal、-v、-k、-s) | timeout 30 npm test → npm test |
time | 无前缀 | time make build → make build |
nice | nice -n N、nice -N、无前缀 | nice -n 5 git status → git status |
stdbuf | 短 flag 组合(-o0、-iL、-eL) | stdbuf -o0 npm test → npm test |
nohup | 无前缀 | nohup npm run build → npm run build |
安全环境变量(30+ 个,按类别分,只影响显示格式、日志级别等表层行为,不改变实际命令执行):
| 类别 | 变量 |
|---|---|
| Go 编译 | GOOS、GOARCH、CGO_ENABLED、GO111MODULE、GOEXPERIMENT |
| Rust 日志 | RUST_BACKTRACE、RUST_LOG |
| Node/Python | NODE_ENV、PYTHONUNBUFFERED、PYTHONDONTWRITEBYTECODE、PYTEST_* |
| 终端/显示 | TERM、COLORTERM、NO_COLOR、FORCE_COLOR、TZ、LANG、LC_*、CHARSET |
| 颜色配置 | LS_COLORS、LSCOLORS、GREP_COLOR、GREP_COLORS、GCC_COLORS |
| 格式化 | TIME_STYLE、BLOCK_SIZE、BLOCKSIZE |
| 认证 | ANTHROPIC_API_KEY |
注意 NODE_OPTIONS、PYTHONPATH 等能改变程序行为的变量不在安全列表中,不会被剥离。
子命令拆分:复合命令不能"一揽子"通过
这是 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 个子命令),系统也会拒绝自动放行——无法证明安全性的命令,默认视为不安全。