allowed-tools 是写在文件头的一行权限清单:这个技能能用哪些工具、能跑哪些命令。 它是第一道筛子,不是安全边界——Bash 的前缀匹配拦不住「先跑合法命令、再接一条 rm」这种写法。
前面四节讲的是「读什么」,这一节讲「能干什么」。前者影响成本,后者影响后果。 这一节也是第 3 章前半的最后一块:结构、披露、触发、正文、权限,五件事凑齐,一个技能才算完整。
一、这个字段管什么
它写在 YAML 头里,一行字符串,逗号分隔。 触发时按这份清单收窄权限:清单外的工具,即使全局允许,在这个技能里也用不了。 所以它的作用不是「授权」,是「限权」。默认的权限来自全局配置,这个字段只是往下砍。 一个技能不写这个字段,等于继承全局权限——这也是为什么很多技能「什么都能干」,因为作者根本没写。
反过来,写得太紧会挡路:技能跑到第三步发现没有 Edit 权限,只能停下来问人,一次自动执行就断了。 判据只有一句:这个技能完成它的本职工作,最少需要哪些工具?多给一个都是风险,少给一个就干不成。
二、白名单与显式拒绝
清单可以写成两种语义:允许什么,或者拒绝什么。多数情况下写允许(白名单)就够了。 但有两类工具值得显式拒绝:删除类命令、系统路径的写操作。 写清楚比不写安全——不写是「没提」,写了是「明确不放」。 演示里的清单前六条是白名单(Read、Grep、两条 Bash 子命令、一条精确命令、Edit 一个目录), 后两条是显式拒绝(rm 类、系统绝对路径)。
要注意显式拒绝在有些实现里并不生效,它更像一份声明。真正拦住动作的,还是全局权限和钩子。 所以这一行的定位是「意图声明加第一道筛子」,不是最后一道闸门。
三、Bash 的精细控制语法
Bash 是最难收窄的一个工具,因为它什么都能干。收窄的办法是写子命令:
Bash(uv run alembic:*) # 以 uv run alembic 开头的都放行
Bash(uv run pytest:*) # 测试命令
Bash(git status) # 只放行这一条,不带参数
Bash(git diff:*) # diff 的任意参数
冒号加星号是前缀通配,不带星号是精确匹配。精确匹配最安全,但只适用于「不需要参数」的命令。
前缀通配的粒度要选对:uv run alembic:* 放行所有 alembic 子命令,uv run:* 就等于放行整个 uv。
差一个层级,范围差几十倍。
给 git:* 是最常见的坏写法:它包含 push、reset、checkout,一个技能能强推代码,
那这个技能就不该存在。
四、前缀匹配的漏洞
演示里有一行必须单独指出来:调用 uv run alembic downgrade base; rm -rf /,被判成放行。
原因很简单:它只看开头。参数以 uv run alembic 开头,后面的分号接什么都不过问。
这是前缀匹配的固有漏洞,不是实现 bug。
任何「看前缀」的规则都会被拼接绕过:分号、&&、管道、子 shell,四种写法都能接。
所以在 Bash 上用前缀匹配,就必须再配一条钩子做二次检查——检查完整命令行里有没有危险子串。 钩子的事在第 5 章讲。这一节只需要记住结论:权限清单挡不住刻意绕过,它挡的是「无意中乱来」。
五、三条原则
第一条:默认不给 Bash 通配。Bash(*) 等于把整个 shell 交出去,那这个技能跟没有约束没区别。
第二条:命令按子命令给,不给整个命令。
给 git status 而不是 git:*,给 uv run alembic:* 而不是 uv run:*。
第三条:写操作给目录,不给全盘。Edit(src*) 是能接受的;给所有文件等于没约束。
三条之外还有一条隐含的:别抄别人的清单。 别人的技能边界跟你的项目不一样,抄过来要么挡住自己的路,要么放开通往密钥的路。 抄清单比抄 CLAUDE.md 更危险:CLAUDE.md 写错了顶多让模型走弯路,权限写宽了是直接给它开了一道门。
六、动手 Demo:allowed-tools 怎么写,才既不挡路也不放水
先给一份八条的权限清单(六条白名单、两条显式拒绝),再拿十个调用逐个判。 判完看那一行漏网的,以及三条常见坏写法各自的问题。
RULES = [(u"Bash", u"uv run alembic:*", True),
(u"Bash", u"uv run pytest:*", True),
(u"Bash", u"git status", True),
(u"Edit", u"src*", True),
(u"Bash", u"rm:*", False)]
def ok(tool, arg):
for t, pat, allow in RULES:
if t != tool:
continue
if pat.endswith(u":*"): # 前缀匹配:只看开头
if arg.startswith(pat[:-2]):
return allow
elif arg == pat:
return allow
return False # 清单外一律不放
实跑输出:
--------------------------------------------------------------------
allowed-tools 怎么写,才既不挡路也不放水
--------------------------------------------------------------------
① 一份权限清单
--------------------------------------------------------------------
| 规则 | 含义 | 效果 |
|---|---|---|
| Read | 读文件,任意路径 | 允许 |
| Grep | 搜内容,任意路径 | 允许 |
| Bash(uv run alembic:*) | 以 uv run alembic 开头的命令 | 允许 |
| Bash(uv run pytest:*) | 以 uv run pytest 开头的命令 | 允许 |
| Bash(git status) | 只允许 git status 这一条 | 允许 |
| Edit(src*) | src 下任意文件 | 允许 |
| Bash(rm:*) | 删除类命令一律不放 | 拒绝 |
| Write(/*) | 系统绝对路径一律不放 | 拒绝 |
前六条是白名单,后两条是显式拒绝。
真机上这个字段就是一行字符串,写在 SKILL.md 头部,管的是这个技能能用哪些工具。
② 十个调用,逐个判
--------------------------------------------------------------------
| 工具 | 参数 | 结果 |
|---|---|---|
| Read | docs/install.md | 放行 |
| Bash | uv run alembic heads | 放行 |
| Bash | uv run alembic upgrade head | 放行 |
| Bash | uv run pytest -q | 放行 |
| Bash | git status | 放行 |
| Bash | git push --force | 拦下 |
| Bash | uv run alembic downgrade base; rm -rf / | 放行 |
| Edit | src/app/main.py | 放行 |
| Edit | docs/install.md | 拦下 |
| Write | /etc/passwd | 拦下 |
③ 有一行必须指出来
--------------------------------------------------------------------
第七个调用是 uv run alembic downgrade base 后面接一个 rm -rf。
它被判成放行了——因为参数以 uv run alembic 开头。
! 这是前缀匹配的固有漏洞:它只看开头,不管后面接了什么。
所以在 Bash 上要用前缀匹配,就必须配一条钩子做二次检查。权限清单不是安全边界,它只是第一道筛子。
④ 怎么写才合理
--------------------------------------------------------------------
第一条原则:默认不写 Bash 的通配。Bash(*) 等于把整个 shell 交出去,那这个技能跟没有约束没区别。
第二条原则:命令按子命令给,不给整个命令。
给 uv run alembic:* 而不是 uv run:*,给 git status 而不是 git:*。
第三条原则:写操作给目录,不给全盘。Edit(src*) 是能接受的,Edit(所有文件) 等于没约束。
| 写法 | 问题 | 怎么改 |
|---|---|---|
| Bash(*) | 等于不设限 | 删掉,改成具体子命令 |
| Bash(git:*) | 能 push、能 reset | 拆分:只给 status、diff、log |
| Edit(任意路径) | 能改任何文件 | 限定到 src 或 docs |
| Write(系统目录) | 能写配置和密钥 | 永远别给 |
! 权限设计的判据只有一句:这个技能完成它的本职工作,最少需要哪些工具?
多给一个都是风险,少给一个就干不成。「顺手都给了」是最常见的坏习惯。
①八条清单:四条 Bash 白名单(两条子命令前缀、一条精确命令)、Read 与 Grep、Edit 一个目录,
加两条显式拒绝。
前六条是白名单,后两条是显式拒绝。
真机上这个字段就是一行字符串,写在 SKILL.md 头部,管的是这个技能能用哪些工具。
②十个调用里判错了两个方向的问题:一个该拦的放行了、两个该放的拦下了——严格说,只有第七个是真漏洞。
第七个是 uv run alembic downgrade base; rm -rf /。
它被判成放行,因为参数以 uv run alembic 开头。
③这一行是整节的靶子:前缀匹配的固有漏洞,它只看开头,不管后面接了什么。
所以在 Bash 上用前缀匹配,就必须配一条钩子做二次检查。权限清单不是安全边界,它只是第一道筛子。
④三条原则:不给 Bash 通配、命令按子命令给、写操作给目录。
最后一张表把坏写法列全了:Bash(*) 等于不设限,git:* 能强推,Edit(任意路径) 能改任何文件,
Write(系统目录) 能写配置和密钥。
判据还是那一句:完成本职工作最少需要哪些工具。多给一个都是风险。
30 秒实验
- 把第七个调用改成分号、
&&、管道三种写法,重跑一遍。三种都能绕过吗?哪种能被前缀匹配挡住? - 把
Bash(uv run alembic:*)换成Bash(uv run alembic upgrade:*),十个调用里还有几个放行? 这样收窄的代价是什么? - 给这个技能加一条
Write(reports*),判断它是白名单还是白给。 如果技能真的需要写报告,这条该怎么写才安全?
常见误区
- 把权限清单当成安全边界。它挡得住无意乱来,挡不住刻意拼接;真正的拦截要靠钩子。
- 写
Bash(*)图省事。这等于把整个 shell 交出去,技能跟没约束一样。 - 给
git:*。它包含 push、reset、checkout,一个「查状态」的技能不该有强推的能力。 - 不写 allowed-tools,靠全局权限兜底。技能会继承全部权限,比写宽了更危险。
- 抄别人的权限清单。边界跟项目强相关,抄过来不是挡路就是开路。
自测三问(点开对答案)
1. allowed-tools 的作用是授权还是限权? —— 限权。默认权限来自全局配置,这个字段只是在全局的基础上往下砍,不写就等于继承全部。 2. 为什么 `Bash(uv run alembic:*)` 拦不住 `uv run alembic downgrade base; rm -rf /`? —— 因为它是前缀匹配,只看参数开头。 分号后面的内容不在判断范围里,所以整条被判成以 `uv run alembic` 开头而放行。 3. Bash 权限的三条写法原则是什么? —— 不给通配(不写 `Bash(*)`);命令按子命令给而不是整个命令;写操作限定到目录而不是全盘。第 3 章前半到此为止:结构、披露、触发、正文、权限五件事齐了。下一节开始加功能——参数怎么传。
- ☐ (本篇完,下一篇 3.7 参数传递与动态注入)
相关笔记
- 正文的形状:正文该写成什么形状
- 参数传递:参数与动态注入
- 网络与信息安全 · 索引
- 🐎 Claude Code 实战 · 索引