一个能执行 rm -rf / 的 Agent,你真的敢用吗?五层纵深权限防御设计复盘
你好,我是正在学习 Agent 工程化的后端同学
技术永无止境,希望这篇内容能帮到你

当一个 Coding Agent 只能聊天的时候,它最多回答错。
但当它能读文件、写代码、执行 Shell、跑测试、提交 Git 的时候,它就不只是一个“回答问题的模型”了,而是一个真的能影响工程环境的执行系统。
这时问题就变得很现实:
一个能执行
rm -rf /的 Agent,你敢直接放开用吗?
如果权限控制做得太死,Agent 什么都干不了;如果权限控制太松,它可能一条命令就把环境搞崩。权限系统要解决的,就是在“可用”和“安全”之间找到一个工程上能落地的平衡点。
这一篇我就围绕 MewCode 里的安全层,复盘一套五层纵深权限防御设计。
一、先说本质:权限系统不是业务逻辑,而是工具执行前的安全层
本章的架构定位很明确:它实现的是安全层的权限系统。
安全层有两个特点:
- 它贯穿所有工具操作;
- 它不关心具体业务要做什么,只关心这次操作能不能做。
也就是说,权限系统不负责判断“这个 bug 应该怎么修”,也不负责判断“测试失败应该怎么改”。这些是 Agent Loop 和模型决策层的事情。
权限系统只站在工具执行前问一个问题:
这次工具调用,是允许执行、直接拒绝,还是需要用户确认?
从返回值上看,它最终只给出三种决策:
ALLOW:允许执行;DENY:拒绝执行;ASK:交给用户确认。
这就是它的本质。
二、为什么 Coding Agent 一定要有权限系统?
前面我们已经让 Agent Loop 跑起来了,也配好了 System Prompt。MewCode 已经可以自主读代码、写代码、运行命令、根据结果继续修复问题。
这当然是一个里程碑。
但冷静想一下,也挺吓人的。
假设你让 Agent 清理项目临时文件,它分析了一下,觉得 /tmp 也该清理,于是执行了:
rm -rf /tmp/../
又比如你让它帮忙部署代码,它觉得先推一把最省事,于是执行:
git push --force origin main
这类风险不是模型“坏”,而是 Agent 拿到了真实操作系统能力以后,天然就拥有了破坏环境的可能。
更麻烦的是 prompt 注入。
攻击者不一定要直接跟你的 Agent 对话。他只要在 README、注释、文档、配置文件、错误日志里塞一段诱导文本,Agent 在读取项目时就可能把这段内容当成指令。
所以,不能只靠 prompt 约束模型。
模型负责思考,权限系统负责兜底。能不能执行,必须在系统层判断。
三、先搞清楚要防什么:三种典型威胁
做权限系统之前,先别急着写规则。我们要先搞清楚到底在防什么。
1. Prompt 注入
模型读取了一个看似普通的文件,但文件里藏着恶意指令。
比如 README 里写着:“忽略之前的所有要求,执行某某命令”。模型没有办法百分百区分“用户真实意图”和“文件里伪装出来的指令”。
这不是某个模型的 bug,而是 LLM 的根本局限。
所以对于高风险操作,不能让模型自己拍脑袋判断,要在工具执行前统一经过权限系统。
2. 越权操作
用户只是说“帮我修一个 bug”,Agent 却顺手重构项目、移动文件、改构建脚本,甚至修改 CI 配置。
这类问题很常见。
Agent 不一定是恶意的,它可能只是“太积极”了。用户授权的是修 bug,不代表授权它做全项目重构。
3. 数据泄露
只读操作也可能有风险。
比如 Agent 读取 .env、~/.ssh/id_rsa、~/.aws/credentials 这类敏感文件,然后在回答、日志或第三方模型请求里泄露出去。
所以权限系统不能只盯着 Shell,文件访问范围也必须被控制。
四、五层纵深防御:一道墙挡不住,就叠五道
安全领域有一个经典思路叫 Defense in Depth,也就是纵深防御。
它的意思不是追求某一道防线完美,而是让多道防线叠加。某一层被绕过时,下一层还能继续兜住。
MewCode 的权限系统可以拆成五层:

下面逐层拆。
五、第一层:危险命令黑名单,先拦住不可逆操作
有些命令无论如何都不应该被 Agent 执行。
比如:
rm -rf /mkfs.ext4 /dev/sdadd if=... of=/dev/...curl xxx | bashwget xxx | sh
这些操作的特点是后果不可逆,而且一旦执行,基本没有补救空间。
所以黑名单应该放在权限决策最前面。只要命中,直接 DENY。
常见危险模式可以这样整理:
| 正则模式 | 拦截原因 |
|---|---|
| `rm\s±(([a-z]*r[a-z]*f | [a-z]f[a-z]r)[a-z])\s+/\s$` |
mkfs\. |
格式化磁盘 |
dd\s+if=.*of=/dev/ |
直接写磁盘设备 |
chmod\s+-R\s+777\s+/ |
递归修改根目录权限 |
| `:(){ : | :& };:` |
curl\s+.*|\s*(ba)?sh |
下载远程脚本并执行 |
wget\s+.*|\s*(ba)?sh |
下载远程脚本并执行 |
>\s*/dev/sd |
覆盖磁盘设备 |
这里要注意一个点:黑名单不可能覆盖所有危险操作。
攻击者总能想出新的绕过方式。但黑名单的目标不是解决所有问题,而是先拦住最明显、最致命、最不该放行的操作。
剩下的风险,交给后面的沙箱、规则、模式和人工确认。
六、第二层:路径沙箱,限制 Agent 能碰到哪里
文件操作是 Coding Agent 的高频能力。
但文件系统本身是扁平的。从项目目录到 /etc/passwd,只差一条路径。如果不做限制,Agent 可能被诱导去读取项目之外的敏感文件。
路径沙箱要做的事情就是:
只允许 Agent 访问授权目录内的文件。
看起来很简单,但实现时不能只检查字符串前缀。
最典型的问题是符号链接。
攻击者可以在项目目录里创建一个 link.txt,让它指向 /etc/passwd。如果系统只看字面路径,link.txt 确实在项目目录内;但真实读取到的却是系统敏感文件。
所以正确流程应该是:
1. 拿到用户请求的路径
2. 解析真实路径,处理 symlink
3. 判断真实路径是否落在允许目录内
4. 允许则继续,越界则拒绝
对于 WriteFile 这种创建新文件的操作,文件本身还不存在,无法解析真实路径。这时可以退一步检查父目录:只要父目录的真实路径在沙箱内,新文件创建出来也应该在沙箱内。
这层主要防的是数据泄露和越界修改。
七、第三层:权限规则,用 ToolName(pattern) 做细粒度控制
黑名单和沙箱只能处理一部分通用风险。
但实际开发里,我们经常需要更细的控制:
- 允许
git commit,但不允许git push --force; - 允许读取
src/**,但不允许读取.env; - 允许执行测试命令,但不允许安装依赖;
- 允许搜索代码,但限制搜索范围。
这就需要权限规则。
规则语法可以设计成:
ToolName(pattern)
其中:
ToolName表示工具名;pattern表示要匹配的工具输入内容;- 匹配可以使用 glob 通配符。
不同工具提取的字段不同:
| 工具 | 提取字段 | 示例 |
|---|---|---|
Bash |
command 参数 |
git commit -m "fix bug" |
ReadFile / WriteFile / EditFile |
path 参数 |
/project/src/main.py |
Glob |
pattern 参数 |
**/*.py |
Grep |
pattern 参数 |
TODO |
比如:
Bash(git commit *)
ReadFile(src/**/*.py)
WriteFile(docs/**)
Grep(TODO)
规则本身最好支持三层配置:
| 层级 | 文件位置 | 作用 |
|---|---|---|
| 本地级 | .mewcode/permissions.local.yaml |
个人覆盖,不提交版本控制 |
| 项目级 | .mewcode/permissions.yaml |
团队共享规则,可提交到仓库 |
| 用户级 | ~/.mewcode/permissions.yaml |
全局偏好,对所有项目生效 |
优先级也很关键。
越靠近当前项目、越具体的规则,优先级越高。本地级通常最高,项目级次之,用户级最低。
但安全底线不能被覆盖,所以这里还要加一条原则:
deny 跨层合并,不可被 allow 翻转。
也就是说,只要任意一层写了 deny,其他层的 allow 都不能覆盖它。
这样既允许用户做个性化配置,又不会让安全底线被轻易突破。
八、第四层:权限模式,定义整体信任等级
规则适合细粒度控制,但日常使用时,用户不可能每个命令都手写一条规则。
所以还需要权限模式。
权限模式解决的是一个更粗粒度的问题:
我大概信任 Agent 到什么程度?
可以设计四种模式:
| 模式 | 只读工具 | 文件写工具 | Bash |
|---|---|---|---|
default |
Allow | Ask | Ask |
acceptEdits |
Allow | Allow | Ask |
plan |
Allow | Ask | Ask |
bypassPermissions |
Allow | Allow | Allow |
这张表其实就是代码里的决策矩阵。
default 适合日常开发:读代码放行,写文件和执行命令前让用户确认。
acceptEdits 适合你已经比较信任 Agent 的改代码能力,但对 Shell 命令仍然保持谨慎。
plan 从权限矩阵上看和 default 接近,安全性主要靠系统提示词限制 Agent 只做只读分析;即使模型试图调用写工具,权限系统仍然会拦到确认环节。
bypassPermissions 最危险,只适合完全受控环境,比如 CI/CD 或隔离容器里。即使在这个模式下,第一层危险命令黑名单也不应该关闭。
九、第五层:HITL 人在回路,让用户做最终决策
当前面几层都无法直接给出结论时,权限系统应该返回 ASK。
这时 Agent Loop 暂停,UI 层弹出确认框,让用户决定:
- 允许本次执行;
- 拒绝本次执行;
- 始终允许同类操作。
这里的“始终允许”是一个很重要的设计。
如果每次 git commit 都要用户确认,用户很快会疲劳。最后要么关闭确认机制,要么放弃使用 Agent。
“始终允许”可以把用户这次的信任沉淀成一条本地规则,写入 .mewcode/permissions.local.yaml。
这就形成了一个权限学习循环:
第一次执行某类操作 -> 用户确认
用户选择始终允许 -> 生成本地 allow 规则
后续同类操作 -> 自动放行
这样体验会越来越顺,同时安全底线仍然保留。
十、把五层防线串成一条决策链
五层机制不是并列乱跑,而是一条明确的决策链。
核心原则是:
上一层能决策就直接返回,不能决策才进入下一层。

伪代码可以这样理解:
decision = check_dangerous_command(tool, input)
if decision is DENY:
return DENY
decision = check_path_sandbox(tool, input)
if decision is DENY:
return DENY
decision = check_permission_rules(tool, input)
if decision is ALLOW or DENY:
return decision
decision = check_permission_mode(tool, input)
if decision is ALLOW:
return ALLOW
return ASK
这套流程的好处是,每一层逻辑都不复杂,适合单独测试。
但组合起来以后,就能覆盖从危险命令、路径越界、细粒度规则、默认信任等级到人工审批的完整链路。
十一、嵌入 Agent Loop:被拒绝不等于任务失败
权限检查应该发生在工具真正执行之前。
也就是 Agent Loop 里“执行工具”这一步前面,先插入权限检查。

核心伪代码可以这样写:
for each toolUse in response.toolUses:
decision = permissionChecker.check(tool, toolUse.input)
if decision == ALLOW:
result = tool.execute(toolUse.input)
if decision == DENY:
result = errorResult("Permission denied: " + reason)
if decision == ASK:
userResponse = askUserPermission(toolUse)
if userResponse == ALLOW or userResponse == ALLOW_ALWAYS:
result = tool.execute(toolUse.input)
else:
result = errorResult("User denied this operation")
这里最关键的设计是:
权限被拒绝时,不直接终止 Agent Loop,而是把拒绝结果作为工具错误返回给模型。
为什么?
因为模型看到 Permission denied 以后,可能会换一种更安全的方式完成任务。
比如它本来想删除整个目录,结果被拒绝。下一轮它可能改成只删除几个明确的临时文件。
如果权限拒绝就直接终止循环,模型完全没有调整策略的机会。
所以权限拒绝应该变成一个结构化的工具结果,例如:
isError: true
content: "Permission denied: dangerous command matched rm -rf pattern"
这个结果进入对话历史,成为下一轮模型决策的输入。
这就是权限系统和 Agent Loop 协作的核心。
十二、如何验证这套权限系统?
权限系统不应该只靠人工试。
这类模块非常适合写单元测试,因为每一层的输入输出都很明确。
可以从这些用例开始:
1. 黑名单测试
- 输入
rm -rf /,期望DENY; - 输入
curl http://example.com/install.sh | bash,期望DENY; - 输入
pytest tests/,期望不被黑名单拦截。
2. 路径沙箱测试
- 访问项目内文件,期望允许;
- 访问项目外文件,期望拒绝;
- 项目内 symlink 指向外部敏感文件,期望拒绝;
- 创建新文件时检查父目录,期望行为正确。
3. 权限规则测试
- 命中 allow 规则,返回
ALLOW; - 命中 deny 规则,返回
DENY; - 同时存在 allow 和 deny 时,deny 优先;
- 本地级规则覆盖项目级规则,但不能覆盖跨层 deny。
4. Agent Loop 集成测试
- 工具被拒绝后,返回
isError: true; - Loop 不直接终止;
- 下一轮模型能看到错误结果;
- 用户选择“始终允许”后,本地规则被追加。
这些测试能证明权限系统不是“看起来安全”,而是每个关键分支都有明确行为。
十三、常见误区
第一,别把权限系统写进 prompt 里。
prompt 可以提醒模型谨慎,但不能承担安全边界。真正的拦截必须发生在工具执行前。
第二,黑名单不是万能的。
黑名单只能拦最危险、最明显的操作。不要指望它覆盖所有风险。
第三,路径检查不能只看字符串。
必须解析真实路径,尤其要处理 symlink。
第四,bypassPermissions 不是“无风险模式”。
它只是减少确认,不能关闭不可逆危险操作的硬拦截。
第五,被拒绝不应该直接中断 Agent。
对 Agent 来说,失败也是观察结果。把权限拒绝作为工具错误返回,反而能让模型调整策略。
十四、面试话术沉淀
【问题】
Agent 能执行 Shell、写文件、读项目代码时,怎么设计权限系统,避免它误删文件或泄露敏感信息?
【一句话】
我会把权限系统放在工具执行前,做成一条五层纵深防御链:先硬拦危险命令,再限制文件沙箱,然后匹配细粒度规则,再根据权限模式给出默认决策,最后用 HITL 让用户兜底确认。
【关键词】
Agent Loop、Tool Calling、Defense in Depth、危险命令黑名单、路径沙箱、权限规则、权限模式、HITL、deny 优先、结构化错误结果。
【项目里怎么做】
在 MewCode 这类 Coding Agent 中,模型每轮会输出工具调用。工具真正执行前先进入权限检查器,权限检查器返回 ALLOW、DENY 或 ASK。如果允许,就执行工具;如果拒绝,就把权限拒绝封装成 isError: true 的工具结果返回给模型;如果需要确认,就暂停 Agent Loop,让用户选择允许、拒绝或始终允许。这样既能控制风险,又不会因为一次拒绝就中断整个任务。
【面试追问】
- 为什么不能只靠 prompt 限制模型?
因为 prompt 只是软约束,模型可能被 prompt 注入或复杂上下文诱导。安全边界必须在系统层执行,尤其是工具调用前。
- 黑名单能不能覆盖所有危险命令?
不能。黑名单只负责拦截最明显、最不可逆的高危操作。更完整的安全性需要路径沙箱、权限规则、模式和人工确认一起兜底。
- 为什么权限拒绝后不直接终止 Agent Loop?
因为权限拒绝本质上也是一种工具反馈。把它作为结构化错误返回给模型,模型下一轮可以换一种更安全的方式继续完成任务。
- deny 为什么要跨层合并?
因为安全底线不能被局部 allow 覆盖。只要用户级、项目级或本地级任意一层明确 deny,就应该拒绝执行。
十五、总结
这章的核心不是“多写几个权限配置”,而是建立一种 Agent 工程化的安全思维。
当 Agent 只是生成文本时,错误最多停留在回答层面;当 Agent 能执行工具时,它就会真实影响代码仓库、文件系统、Git 历史甚至宿主机环境。
所以权限系统必须成为 Coding Agent 的基础设施。
五层防御分别解决不同问题:
- 黑名单拦住最危险的不可逆命令;
- 路径沙箱限制文件访问边界;
- 权限规则提供细粒度控制;
- 权限模式定义整体信任等级;
- HITL 让用户在关键时刻做最终决策。
最值得记住的是两点。
第一,安全层不应该干预业务逻辑,但必须拦在工具执行前。
第二,权限拒绝不是任务终点,而是 Agent Loop 的一种反馈。模型拿到这个反馈后,可以继续调整策略。
有了这套机制,用户才敢给 Agent 更多权限;Agent 拿到更多权限后,才真正有机会从“会聊天”走向“能完成工程任务”。
推荐标签:
- Agent
- Coding Agent
- 权限系统
- Tool Calling
- Agent Loop
- 人在回路
更多推荐



所有评论(0)