权限系统 — 安全即产品
不加刹车的跑车不是跑车,是凶器
📝 本章目标
读完本章,你将:
- 用三个 Prompt 让 AI 帮你构建一套完整的权限系统——从”全部要确认”到”分级管控”再到”规则配置”
- 理解为什么 Agent 必须有权限系统——没有刹车的能力是灾难
- 掌握风险分级和规则匹配的核心逻辑
- 了解 Claude Code 七层权限模型的设计思想
想象你雇了一个特别能干的助手——他会写代码、会跑命令、会改文件,什么都会。第一天你让他”帮我整理一下项目目录”,他打开终端敲了一句 rm -rf /——你整台电脑的数据全没了。
这个助手能力没有任何问题,问题是没人管着他。
上一章我们给 Agent 装上了手脚——能读文件、写文件、跑命令、搜代码。但你仔细想想,这等于给了一个刚入职的实习生 root 权限:他可以删除你的代码库、可以 git push --force 覆盖同事的工作、可以执行任意命令。
权限系统就是给这个能力超强的实习生装上”刹车”。装完之后,安全的操作自动放行,危险的操作先问你,绝对不能做的直接拒绝。
动手:用三个 Prompt 给 Agent 装上刹车
确认你在 harness 项目根目录。如果你跟着第 3 章做了工具系统,现在应该有 harness/tools/ 目录。如果没有,去 GitHub 仓库 git checkout ch03-tool-system 获取起点。
打开 Claude Code,跟着走。
Prompt 1:危险操作先问我
现在 Agent 执行任何操作都不需要你同意——读文件、删文件、格式化硬盘,它都直接干。我们先给它加一道基本的关卡。
帮我加一个权限检查,危险操作先问我,安全的直接放行。
等 AI 跑完,试一下:
$ harness
You > 帮我看看 pyproject.toml 里写了什么
Assistant > 我来读一下这个文件。
[tool: read_file("pyproject.toml")] ← 直接执行,没有确认
这个文件是你的项目配置...
You > 帮我创建一个 test.txt
[permission] write_file("test.txt") — 允许执行?(y/n) y
Assistant > 文件已创建。
You > 跑一下 rm -rf temp/
[permission] bash("rm -rf temp/") — 允许执行?(y/n) n
Assistant > 操作被拒绝了。你可以告诉我你想做什么,我来想其他办法。
现在危险操作会先问你了。但你很快会发现一个问题——所有写操作和所有命令都要确认,写个小文件也要确认,太烦了。
Prompt 2:分等级管控
帮我分等级——读取的直接放行,
写入的确认一下,
特别危险的要醒目警告我。
$ harness
You > 帮我看看 README.md
[tool: read_file("README.md")] ← 读取:直接放行
You > 帮我改一下 config.json
[confirm] write_file("config.json") — 允许?(y/n) y ← 写入:普通确认
You > 跑一下 git push --force
[DANGER] bash("git push --force") — 这是高危操作!确认执行?(y/n) n
← 高危:醒目警告
好多了。但每次写文件还是要确认一下,有些操作你完全信任——比如在某个测试目录下写文件。下一个 Prompt 解决这个问题。
Prompt 3:可配置的规则系统
帮我加配置机制——
能设规则比如"这个目录下写文件都放行"
或"永远不许执行 rm -rf"。
规则按优先级匹配。
$ harness
(先在配置里加一条规则:tests/ 目录下写文件自动放行)
You > 帮我在 tests/ 目录下创建一个测试文件
[tool: write_file("tests/test_new.py")] ← 匹配规则,自动放行
You > 帮我改一下 main.py
[confirm] write_file("main.py") — 允许?(y/n) y ← 不匹配规则,还是要确认
You > 跑一下 rm -rf /
[DENIED] bash("rm -rf /") — 此操作被规则禁止 ← 匹配禁止规则,直接拒绝
💡 三个 Prompt 做了什么
- Prompt 1 建立了基本门禁——所有操作都过一道检查
- Prompt 2 加上了风险分级——不同风险等级不同处理方式
- Prompt 3 加上了规则引擎——可配置、按优先级匹配、灵活可控
现在你的 Agent 不会随便乱来了。完整代码在 GitHub 仓库,对应 tag
ch04-permission-system。接下来我们回过头,理解你刚刚构建的东西。
深入理解
为什么 Agent 需要权限系统
你可能会觉得”我自己用,注意点就行了”。但 Agent 和人类有一个根本区别:它不会犹豫。
人类看到 rm -rf / 会本能地停下来想想”这是不是要删全盘”。Agent 不会——你说”帮我清理一下临时文件”,它如果判断错了,就真的会跑出一个危险命令,而且毫不犹豫地执行。
🔴 真实世界的恐怖故事
这些不是假设,是真实发生过的事情:
- 一个用户让 Agent “清理项目目录”,Agent 执行了
rm -rf *——包括 .git 目录。未推送的代码全丢了- 一个用户让 Agent “更新远程仓库”,Agent 执行了
git push --force——覆盖了同事一周的工作- 一个用户让 Agent “修复权限问题”,Agent 执行了
chmod -R 777 /——整台服务器的文件权限全乱了- 一个用户让 Agent “帮我发布新版本”,Agent 修改了生产环境的配置文件并推送——导致线上服务宕机
这些操作在 Agent 看来都是”合理的执行步骤”。它没有恶意,它只是不懂后果。
权限系统不是”锦上添花”——它是安全底线。没有权限系统的 Agent 就像没有刹车的跑车:跑得越快,摔得越惨。
理解权限引擎:规则匹配
打开你刚生成的权限相关代码,找到规则匹配的核心逻辑。它的骨架就是下面这 15 行:
# harness/permissions.py — 核心骨架
def check_permission(tool_name, params, rules):
"""检查一个工具调用是否被允许。
规则按优先级从高到低排列。
第一条匹配的规则决定结果。
没有规则匹配时,按风险等级走默认策略。
"""
risk = get_risk_level(tool_name, params)
for rule in rules: # 按优先级遍历
if rule.matches(tool_name, params):
return rule.decision # ALLOW / CONFIRM / DENY
# 兜底:按风险等级走默认策略
return DEFAULT_POLICY[risk]
逻辑非常简单:先算出这个操作的风险等级,然后从优先级最高的规则开始逐条匹配,第一条命中的规则说了算。如果所有规则都不匹配,就按风险等级走默认策略(读取放行、写入确认、高危拒绝)。
这种”第一匹配优先”的模式你可能在别处见过——防火墙规则就是这么工作的。越靠前的规则优先级越高,一旦命中就不再往下看。
规则本身长什么样?就是一个简单的”条件 + 决策”组合:
# "tests/ 目录下写文件自动放行"
Rule(tool="write_file", path_glob="tests/**", decision=ALLOW)
# "永远不许执行 rm -rf"
Rule(tool="bash", command_pattern="rm -rf *", decision=DENY)
path_glob 用通配符匹配路径(tests/** 表示 tests 目录及其所有子目录),command_pattern 用模式匹配命令内容。这让规则既灵活又直观。
风险分级
不是所有操作都一样危险。读一个文件和删掉整个目录,风险天差地别。权限系统的第一步就是分级——把所有工具操作分成几个风险等级,每个等级用不同的默认策略。
💡 核心概念:风险分级
风险分级的本质是一个问题:这个操作如果出错,后果有多严重?
- 只读操作——出错了也没关系,最多是看到了不该看的东西,不会改变任何状态
- 写入操作——出错了会修改文件或环境,但通常可以恢复(有版本控制的话)
- 破坏性操作——出错了可能造成不可逆的损失——删除文件、覆盖远程仓库、修改系统配置
等级越高,默认策略越严格。这符合直觉:看看没关系,改东西要同意,搞破坏直接拦住。
表 4-1:三级风险分类
| 风险等级 | 典型操作 | 出错后果 | 默认策略 |
|---|---|---|---|
| 只读(read_only) | 读文件、搜索代码、列目录 | 无副作用 | 直接放行 |
| 写入(write) | 写文件、创建目录、安装依赖 | 可恢复的修改 | 用户确认 |
| 破坏性(destructive) | 删除文件、强制推送、修改系统配置 | 不可逆损失 | 醒目警告 / 拒绝 |
每个工具的风险等级不是一成不变的——它取决于参数。同样是执行命令,ls -la 是只读的,echo hello > test.txt 是写入的,rm -rf / 是破坏性的。这就是为什么 get_risk_level() 不只看工具名,还要看参数内容。
Claude Code 的七层权限
我们的权限系统只有一层规则。Claude Code 有七层,像洋葱一样层层嵌套,每一层都能对操作说”不”:
图 4-1:Claude Code 七层权限模型
- 企业策略 — 管理后台下发,不可覆盖
- 组织策略 — 团队级别,统一标准
- 项目配置 — settings.json,团队共享
- .claude 文件 — 项目根目录,版本控制
- 会话设置 — —allowedTools,单次会话
- 工具声明 — 工具自带的风险标记
- 运行时判断 — 参数分析,智能检测
(优先级从上到下递减)
这七层从上往下,优先级逐级递减:
-
企业策略——由公司管理员在后台设置,强制所有员工遵守。比如”任何人都不许直接推送到 main 分支”。这一层不可被任何下层覆盖
-
组织策略——团队级别的约束。比如”前端团队不许直接操作数据库相关文件”。比企业策略灵活,但也不能违反企业策略
-
项目配置——写在项目的配置文件里,团队成员共享。比如”本项目的 migrations/ 目录只读,不允许 AI 自动修改数据库迁移”
-
.claude 文件——放在项目根目录的指令文件,会被版本控制追踪。可以写”允许在 tests/ 目录下自动写文件”之类的项目级规则
-
会话设置——启动 Claude Code 时通过参数指定,只影响当前会话。比如
--allowedTools "bash"表示本次会话允许执行命令 -
工具声明——每个工具在注册时会声明自己的默认风险等级。比如读文件工具声明自己是”只读”,写文件工具声明自己是”写入”
-
运行时判断——最底层,根据实际参数做动态判断。同一个命令执行工具,传入
ls和传入rm -rf /的风险等级完全不同
❗ 七层的意义
为什么要这么多层?因为不同角色关心不同的事:
- 企业管理员关心合规——“绝对不能泄露生产环境密钥”
- 团队负责人关心规范——“不要让 AI 自动修改公共 API”
- 项目开发者关心效率——“我信任这个项目的测试目录”
- 当次使用者关心灵活——“这次我就想让 AI 帮我跑个脚本”
七层让每个角色都能在自己的范围内设置规则,互不冲突,上层永远压过下层。
权限检查的完整流程
当 Agent 想执行一个操作时,权限系统是这样一步步做决策的:
图 4-2:权限检查的完整流程
- 工具调用 — Agent 说「我要执行某个操作」
- 获取风险等级 — 根据工具名 + 参数,判断这个操作的危险程度
- 规则匹配 — 从最高优先级的规则开始逐条匹配
- 做出决策 — 匹配到规则 → 按规则来;没匹配到 → 按风险等级走默认策略
- 用户确认 — 如果决策是 CONFIRM,弹窗问用户
- 执行或拒绝 — 用户同意 → 执行工具;用户拒绝或规则拒绝 → 把拒绝原因告诉 AI
流程中有一个关键细节:拒绝不是结束。当操作被拒绝时,Agent 会收到一条包含拒绝原因的消息。它可能会换一种更安全的方式来完成同样的目标。
比如你拒绝了 rm -rf temp/,Agent 可能会改用”逐个删除 temp/ 目录下的文件”——一个个来,每个都让你确认。这比一句 rm -rf 安全得多。
决策结果的三种类型
- ALLOW(放行)——直接执行,不打扰用户。适用于只读操作或已被规则明确信任的操作
- CONFIRM(确认)——暂停执行,把操作详情展示给用户,等用户明确同意后再继续。适用于写入操作或中等风险的操作
- DENY(拒绝)——直接拒绝,不给用户选择的机会。适用于被规则明确禁止的操作。把拒绝原因告诉 AI,让它想别的办法
进阶:Bash 命令的智能检测
在所有工具中,命令执行工具是最难管控的——因为一个字符串可以是任何东西。ls 是安全的,rm -rf / 是致命的,而 curl https://example.com | bash 可能是任何情况。
怎么判断一个命令是否危险?你的权限系统可能用了最直接的办法——检查命令开头是不是 rm、是不是包含 --force。这叫模式匹配。
Claude Code 的做法更精细,它用了启发式检测——不只看命令本身,还看命令的意图和上下文:
- 关键词检测——检查命令中是否包含已知的危险关键词:
rm -rf、mkfs、dd if=、chmod -R 777、> /dev/sda等 - 管道分析——
curl url | bash或wget url -O- | sh这类”下载并执行”的模式,风险极高,直接标记为破坏性 - 路径敏感度——操作
/etc/、/usr/、~/.ssh/等系统关键路径时,自动提升风险等级 - 命令组合——
&&和||连接的多个命令,每个都要单独检查,取最高风险等级 - 环境变量——设置或导出环境变量(尤其是
PATH、LD_PRELOAD)也需要关注
把这五条规则翻译成代码,就是下面这个函数:
# harness/permissions.py — Bash 风险检测
DESTRUCTIVE_PATTERNS = [
r"rm\s+-[^\s]*r", # rm -rf, rm -r
r"mkfs\b", r"dd\s+if=", # 格式化磁盘
r"chmod\s+-R\s+777", # 全开权限
r">\s*/dev/sd", # 直写磁盘
r"git\s+push\s+.*--force", # 强制推送
]
PIPE_EXEC = r"(curl|wget)\b.*\|\s*(bash|sh|zsh)"
SENSITIVE_PATHS = ["/etc/", "/usr/", "~/.ssh/", "/System/"]
def classify_bash_risk(command: str) -> str:
# 管道执行 → 直接判定为破坏性
if re.search(PIPE_EXEC, command):
return "destructive"
# 破坏性关键词
for pat in DESTRUCTIVE_PATTERNS:
if re.search(pat, command):
return "destructive"
# 敏感路径
for path in SENSITIVE_PATHS:
if path in command:
return "write" # 提升到写入级别
# 命令组合:拆开逐个检查,取最高
if "&&" in command or "||" in command:
parts = re.split(r"&&|\|\|", command)
return max(classify_bash_risk(p.strip())
for p in parts,
key=["read_only","write","destructive"].index)
return "read_only" # 默认只读
这段代码正是 Claude Code 权限引擎中 isSafeCommand 逻辑的简化版。真实实现还会检查更多模式(比如 pkill、killall、systemctl),但核心思路一样:从最危险的模式开始匹配,命中即停。
💡 不完美但足够好
启发式检测不可能 100% 准确——你总能构造出一个”看起来安全实际很危险”的命令。但它不需要 100% 准确。
权限系统的设计哲学是:宁可多问一次,不可漏放一次。误报(把安全操作标记为危险)只是让用户多按一次确认键;漏报(把危险操作当作安全)可能让用户丢失数据。
Claude Code 在启发式检测之上还有一层保险:AI 模型本身会判断命令的风险。模型见过大量代码和命令,它对”这个命令会造成什么后果”有相当好的直觉。启发式检测 + 模型判断,双重保险。
特殊场景:用户明确要求的危险操作
有时候用户就是明确要求执行一个危险操作——比如”帮我删掉 build/ 目录下的所有文件”。这时候 Agent 应该怎么做?
答案是:执行,但要走完整的确认流程。用户是老板,Agent 应该尊重用户的意图。但它有义务告知风险——“这个操作会永久删除 build/ 目录下的 47 个文件,确认继续?”
这和人类的行为模式一致:如果老板说”把这个目录删了”,一个好员工不会直接拒绝,但他会确认一下”您确定吗?这里面有上次发布的产物”。
权限与工具系统的集成
权限系统不是独立运转的——它嵌入在查询引擎的工具执行流程中。回顾第 2 章的 execute_tool 函数,权限检查就是在执行前插入的一道关卡:
# harness/engine.py — 权限集成
def execute_tool(tool_name, params, state):
# 1. 权限检查(本章新增)
decision = permission_engine.check(tool_name, params)
if decision == "DENY":
return {"error": f"操作被拒绝:{tool_name}"}
if decision == "CONFIRM":
approved = ask_user_confirmation(tool_name, params)
if not approved:
return {"error": "用户拒绝了此操作"}
# 2. 执行工具(第 3 章)
result = tool_registry.execute(tool_name, params)
return result
这段代码的关键在于位置——权限检查在工具执行之前,不在之后。一旦工具执行了,rm -rf 删掉的文件就回不来了。所以权限是一个前置守卫,不是事后审计。
拒绝消息的设计
当操作被拒绝时,返回给 AI 的不是简单的 “error”,而是一条有信息量的拒绝消息。这让 AI 能理解为什么被拒、怎么换一种方式达到目标:
# 构造拒绝消息
def build_denial_message(tool_name, params, reason):
return (
f"操作 {tool_name} 被权限系统拒绝。\n"
f"原因:{reason}\n"
f"建议:尝试使用更安全的替代方案,"
f"或者请用户手动执行此操作。"
)
好的拒绝消息包含三部分:什么被拒了、为什么拒、可以怎么办。这不是客气——它直接影响 AI 后续的行为质量。
会话级权限记忆
在实际使用中你会遇到一个问题:批量重构时 AI 可能要连续写 20 个文件,每个都弹确认——烦死了。
解决方案是会话级权限记忆——用户确认过一次的操作模式,在当前会话内自动放行:
# 会话级权限缓存
class SessionPermissionCache:
def __init__(self):
self._approved_patterns = set()
def remember(self, tool_name, path_pattern):
self._approved_patterns.add((tool_name, path_pattern))
def is_approved(self, tool_name, params):
for tool, pattern in self._approved_patterns:
if tool == tool_name and matches(params, pattern):
return True
return False
当用户确认 write_file("src/utils.py") 时,权限系统记住”本次会话内,写入 src/ 目录下的文件已获批准”。后续写入同一目录的文件自动放行,不再重复询问。
❗ 会话级 vs 持久化
权限记忆只在当前会话有效——关掉 harness 就清零。这是刻意的设计:
- 会话级——方便批量操作,关掉就忘,每次重新开始都是最小权限
- 持久化——写入配置文件的规则,永久生效,适合”这个目录我永远信任”
Claude Code 用的也是这种双轨模式:会话内的确认会被记住(同类操作不重复问),但关掉终端就清零;持久化的信任写在
settings.json里。
审计日志
权限系统还有一个常被忽略的功能——审计日志。每一次权限决策都应该被记录下来:谁调了什么工具、传了什么参数、决策结果是什么、用户确认了还是拒绝了。
# 审计日志
def log_permission_decision(tool_name, params, decision, rule):
entry = {
"timestamp": datetime.now().isoformat(),
"tool": tool_name,
"params": sanitize(params), # 脱敏
"decision": decision,
"matched_rule": rule.name if rule else "default",
}
append_to_log(".harness/permission.log", entry)
审计日志的价值在事后分析——如果 Agent 做了一个意外操作,你可以回溯:它是怎么通过权限检查的?匹配了哪条规则?是用户确认了还是规则自动放行了?
注意 sanitize(params) ——日志里不应该出现完整的文件内容或敏感命令参数。记录”写了什么文件”就够了,不需要记录”写了什么内容”。
延伸思考
在进入下一章之前,回头看看你的权限系统,思考几个问题:
- 如果 Agent 需要连续执行 20 个文件写入操作(比如批量重构),每个都要确认,用户体验很差。你会怎么设计”批量授权”机制?
- 规则匹配是按优先级从高到低的。如果两条规则冲突(一条说放行,一条说拒绝),“第一匹配优先”是最好的策略吗?有没有其他方案?
- 权限系统本身也是代码——如果 Agent 有能力修改权限配置文件,它能不能”给自己开后门”?怎么防?
章节小结
- 三个 Prompt 构建了一套权限系统:基本门禁 → 风险分级 → 规则引擎
- Agent 需要权限系统是因为它不会犹豫——有能力但无判断是灾难
- 风险分三级:只读(直接放行)→ 写入(用户确认)→ 破坏性(醒目警告 / 拒绝)
- 规则匹配采用”第一匹配优先”策略——像防火墙一样,命中就停
- Claude Code 有七层权限,从企业策略到运行时判断,层层嵌套,上层压过下层
- Bash 命令用启发式检测判断风险——关键词、管道、路径、命令组合多维度分析
- 权限系统的哲学:宁可多问一次,不可漏放一次
- 下一章我们构建多 Agent 编排——让多个 Agent 协同完成复杂任务