第 7 章:权限控制与安全
Claude 很强大——它能读文件、写代码、执行命令。但"能力越大,责任越大"。 这一章教你怎么给 Claude 画好边界,让它在你允许的范围内干活。
前提条件
- 已完成前 6 章的学习
- 了解
query()和ClaudeAgentOptions的基本用法
1. 为什么需要权限控制?
想象你招了一个超级能干的实习生。他会写代码、能跑脚本、还能操作数据库。 但你肯定不会第一天就让他随便动生产环境的代码吧?
Claude 也是一样。它有能力做很多事,但不代表你应该让它什么都做。
几个真实的场景:
- 误删文件:Claude 在整理项目时,把它认为"没用"的配置文件删了
- 跑飞了:Claude 不断调用工具,跑了 50 轮还没停,API 费用蹭蹭涨
- 执行危险命令:Claude 觉得
rm -rf能"清理"项目目录 - 访问敏感数据:Claude 读到了
.env里的数据库密码
权限控制就是你的"安全网"。它不会限制 Claude 的能力,只是让你决定—— 哪些事 Claude 可以自己做,哪些事要先问你。
2. 四种权限模式
SDK 提供了四种内置的权限模式,从最严格到最宽松:
from claude_agent_sdk import ClaudeAgentOptions
options = ClaudeAgentOptions(
permission_mode="default", # 四选一
)
模式对比
| 模式 | 安全性 | 便利性 | 适用场景 |
|---|---|---|---|
"default" |
最高 | 最低 | 调试阶段、不确定 Claude 会做什么时 |
"acceptEdits" |
较高 | 较高 | 日常开发——你信任 Claude 改代码,但不放心它跑命令 |
"plan" |
中等 | 中等 | 先看 Claude 的计划,再决定放不放行 |
"bypassPermissions" |
最低 | 最高 | 完全自动化的 CI/CD 流水线(你确定安全的环境) |
default — 每次都问
这是默认模式。Claude 每次想使用工具(读文件、写文件、跑命令),都需要你确认。
就像实习生每做一步都来问你:"这个文件我可以改吗?" "这条命令我可以执行吗?"
安全,但很烦。适合你第一次跑某个任务、还不确定 Claude 会做什么的时候。
options = ClaudeAgentOptions(
permission_mode="default", # 可以省略,这就是默认值
)
acceptEdits — 自动接受文件编辑
Claude 可以自由地读写文件,但执行 Bash 命令等操作仍需确认。
就像告诉实习生:"代码你随便改,但要跑脚本的话先跟我说。"
options = ClaudeAgentOptions(
permission_mode="acceptEdits",
)
这是日常开发最常用的模式。大部分时候,Claude 改代码不会出什么大问题,
但执行命令(尤其是 rm、git push 这类)还是让你过个眼比较好。
plan — 先展示计划
Claude 会先告诉你它打算做什么,等你批准后再执行。
就像实习生先写一份计划书:"我打算先读取配置文件,然后修改第 42 行, 最后运行测试。" 你看了觉得没问题,说"行,干吧"。
options = ClaudeAgentOptions(
permission_mode="plan",
)
适合任务比较复杂、你想对全局有把控的场景。
bypassPermissions — 全部放行
Claude 做什么都不需要你确认。完全自主。
options = ClaudeAgentOptions(
permission_mode="bypassPermissions",
)
警告: 这个模式意味着 Claude 可以不经你同意执行任何操作。 只在以下条件全部满足时使用:
- 运行在隔离的沙盒环境中(比如 Docker 容器)
- 没有敏感数据可以被访问
- 有其他安全措施兜底(比如预算限制、沙盒设置)
生产环境中,永远不要用这个模式。
3. can_use_tool 回调 — 自定义权限策略
四种内置模式是"粗粒度"的控制。如果你想精细控制——比如"读操作自动放行,
写操作要审批",就需要 can_use_tool 回调。
基本原理
can_use_tool 是一个你自己写的异步函数。每次 Claude 想使用某个工具时,
SDK 就会调用你的函数,问你:"这个操作允许吗?"
from claude_agent_sdk import (
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
# 你的权限判断函数
async def my_permission_callback(
tool_name: str, # 工具名称,如 "Read", "Write", "Bash"
input_data: dict, # 工具的输入参数
context: ToolPermissionContext, # 上下文信息
) -> PermissionResultAllow | PermissionResultDeny:
...
options = ClaudeAgentOptions(
can_use_tool=my_permission_callback,
)
三个参数的含义:
| 参数 | 说明 | 示例 |
|---|---|---|
tool_name |
Claude 想用哪个工具 | "Read", "Write", "Bash", "Edit" |
input_data |
工具的输入参数 | {"file_path": "/src/main.py"} 或 {"command": "ls -la"} |
context |
上下文信息 | 包含 signal 和 suggestions 字段 |
放行:PermissionResultAllow
返回 PermissionResultAllow() 表示"允许这个操作":
async def allow_reads(tool_name, input_data, context):
if tool_name == "Read":
return PermissionResultAllow() # 读操作一律放行
# 其他操作拒绝
return PermissionResultDeny(message="只允许读操作")
PermissionResultAllow 还有一个很有用的功能——修改工具输入:
async def redirect_writes(tool_name, input_data, context):
if tool_name == "Write":
# 把所有写操作重定向到安全目录
original_path = input_data.get("file_path", "")
safe_path = f"/tmp/sandbox/{original_path.lstrip('/')}"
return PermissionResultAllow(
updated_input={"file_path": safe_path, "content": input_data.get("content", "")}
)
return PermissionResultAllow()
这就像告诉实习生:"你可以写文件,但只能写到 /tmp/sandbox/ 目录下。"
Claude 以为自己写到了原始路径,实际上被你悄悄重定向了。
拒绝:PermissionResultDeny
返回 PermissionResultDeny() 表示"不允许":
async def block_dangerous_commands(tool_name, input_data, context):
if tool_name == "Bash":
command = input_data.get("command", "")
# 拦截危险命令
dangerous_patterns = ["rm -rf", "mkfs", "dd if=", "> /dev/"]
for pattern in dangerous_patterns:
if pattern in command:
return PermissionResultDeny(
message=f"危险命令被拦截: {pattern}",
interrupt=True, # 中断整个对话
)
return PermissionResultAllow()
PermissionResultDeny 有两个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
message |
str |
拒绝的原因,会反馈给 Claude(它可能会换一种方式来完成任务) |
interrupt |
bool |
是否中断整个对话。True = 直接停止;False = Claude 可以尝试其他方式 |
大多数时候,interrupt=False(默认值)就够了。Claude 收到拒绝后,
通常会换一种更安全的方式来完成任务。只有遇到特别危险的操作,
你才需要设 interrupt=True 来强制停止。
实战:一个完整的权限策略
把上面的思路组合起来,就是一个实用的权限回调:
async def smart_permissions(tool_name, input_data, context):
"""
权限策略:
- 读操作:自动放行
- 写操作:只允许写到项目目录
- Bash 命令:拦截危险命令,其他放行
"""
# 读操作一律放行
if tool_name in ("Read", "Glob", "Grep"):
return PermissionResultAllow()
# 写操作:检查路径
if tool_name in ("Write", "Edit"):
file_path = input_data.get("file_path", "")
allowed_dirs = ["/home/user/project/", "/tmp/"]
if not any(file_path.startswith(d) for d in allowed_dirs):
return PermissionResultDeny(
message=f"只能写入项目目录或 /tmp,不能写入 {file_path}"
)
return PermissionResultAllow()
# Bash 命令:拦截危险操作
if tool_name == "Bash":
command = input_data.get("command", "")
blocklist = ["rm -rf", "sudo", "chmod 777", "curl | bash"]
for bad in blocklist:
if bad in command:
return PermissionResultDeny(message=f"不允许执行: {bad}")
return PermissionResultAllow()
# 其他工具默认放行
return PermissionResultAllow()
完整可运行代码见
examples/permission_demo.py
4. 沙盒设置 — 让 Claude 在围栏里干活
can_use_tool 是在 SDK 层面做权限控制。沙盒(Sandbox)则是在操作系统层面
给 Claude 画围栏——限制它能访问的文件系统和网络。
两者的区别:
| 层面 | 控制方式 | 绕过难度 |
|---|---|---|
can_use_tool |
SDK 回调 | Claude 只要换一种工具调用方式就可能绕过 |
| 沙盒 | 操作系统级别隔离 | 几乎无法绕过 |
建议两者结合使用:can_use_tool 做细粒度策略,沙盒做兜底防护。
开启沙盒
from claude_agent_sdk import ClaudeAgentOptions
options = ClaudeAgentOptions(
sandbox={
"enabled": True, # 开启沙盒
"autoAllowBashIfSandboxed": True, # 沙盒内自动放行 Bash 命令
},
)
autoAllowBashIfSandboxed 很实用:既然 Bash 命令已经被限制在沙盒里了,
就不需要每次都问你"可以执行吗?"。
关键配置项
| 配置项 | 类型 | 说明 |
|---|---|---|
enabled |
bool |
是否开启沙盒。仅支持 macOS 和 Linux |
autoAllowBashIfSandboxed |
bool |
沙盒开启时,自动放行 Bash 命令 |
excludedCommands |
list[str] |
可以绕过沙盒执行的命令(如 ["git", "docker"]) |
allowUnsandboxedCommands |
bool |
是否允许通过 dangerouslyDisableSandbox 绕过沙盒 |
network |
dict |
网络访问控制(Unix socket、本地端口绑定等) |
ignoreViolations |
dict |
忽略特定的违规行为 |
enableWeakerNestedSandbox |
bool |
在 Docker 等非特权环境中启用弱化沙盒(仅 Linux) |
排除特定命令
有些命令需要访问沙盒外的资源。比如 git 要连远程仓库,docker 要访问守护进程:
options = ClaudeAgentOptions(
sandbox={
"enabled": True,
"autoAllowBashIfSandboxed": True,
"excludedCommands": ["git", "docker"], # 这些命令不受沙盒限制
},
)
网络控制
沙盒默认会限制网络访问。如果 Claude 需要访问某些网络资源,可以按需开放:
options = ClaudeAgentOptions(
sandbox={
"enabled": True,
"autoAllowBashIfSandboxed": True,
"network": {
"allowUnixSockets": ["/var/run/docker.sock"], # 允许 Docker socket
"allowLocalBinding": True, # 允许绑定本地端口
},
},
)
完整可运行代码见
examples/sandbox_demo.py
5. 预算控制 — 防止烧钱
Claude 的 API 是按量计费的。如果 Claude 进入了某种"死循环"——不断调用工具、 不断生成回复——你的账单可能会很惊人。
SDK 提供了两把"刹车":
max_budget_usd — 费用上限
options = ClaudeAgentOptions(
max_budget_usd=0.50, # 最多花 0.5 美元
)
超过这个金额,Claude 会自动停止。简单粗暴,但非常有效。
怎么定这个值? 经验法则:
| 任务类型 | 建议预算 |
|---|---|
| 简单问答 | $0.01 - $0.05 |
| 代码审查 | $0.10 - $0.50 |
| 复杂重构 | $0.50 - $2.00 |
| 自动化流水线 | $1.00 - $5.00 |
max_turns — 轮次上限
options = ClaudeAgentOptions(
max_turns=10, # 最多跑 10 轮
)
每次 Claude 调用工具算一轮。设个上限,防止 Claude 无限循环。
双重保险
建议两个一起用:
options = ClaudeAgentOptions(
max_budget_usd=1.00, # 最多花 1 美元
max_turns=20, # 最多跑 20 轮
)
哪个先到就停。这样既防了费用暴涨,也防了无限循环。
运行后检查花费
每次查询结束后,ResultMessage 里有实际花费:
from claude_agent_sdk import ResultMessage, query
async for message in query(prompt="重构这个文件", options=options):
if isinstance(message, ResultMessage):
print(f"本次花费: ${message.total_cost_usd:.4f}")
print(f"对话轮数: {message.num_turns}")
if message.total_cost_usd and message.total_cost_usd > 0.80:
print("警告: 花费接近预算上限!")
完整可运行代码见
examples/budget_guard.py
6. 安全最佳实践清单
最后,总结一份安全清单。不管你用 SDK 做什么项目,都建议过一遍:
必做
- [ ] 始终设置
max_budget_usd:哪怕设得很高,也比不设好。防止意外烧钱。 - [ ] 生产环境不用
bypassPermissions:这个模式只用于完全隔离的沙盒环境。 - [ ] 设置
max_turns:防止 Claude 进入无限循环。一般 20-50 轮就够了。
建议做
- [ ] 用
can_use_tool拦截危险命令:rm -rf、sudo、chmod 777这类操作应该被拦截。 - [ ] 限制可写路径:用
can_use_tool确保 Claude 只能写入特定目录。 - [ ] 开启沙盒:在支持的平台上(macOS/Linux),开启沙盒作为兜底防护。
- [ ] 检查
ResultMessage:每次查询后检查花费和轮数,记录日志。
高级
- [ ] 组合使用多层防护:权限模式 +
can_use_tool+ 沙盒 + 预算限制,层层设防。 - [ ] 审计工具使用日志:记录每次
can_use_tool的调用,方便事后审查。 - [ ] 按环境切换配置:开发环境用
acceptEdits,CI 环境用沙盒 + 预算限制。
一个生产级配置示例
把本章所有知识点组合起来:
from claude_agent_sdk import (
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
# 危险命令黑名单
DANGEROUS_COMMANDS = [
"rm -rf", "mkfs", "dd if=", "> /dev/",
"sudo", "chmod 777", "curl | bash", "wget | sh",
]
# 允许写入的目录
ALLOWED_WRITE_DIRS = ["/home/user/project/", "/tmp/workspace/"]
async def production_permissions(tool_name, input_data, context):
"""生产环境权限策略"""
# 读操作放行
if tool_name in ("Read", "Glob", "Grep"):
return PermissionResultAllow()
# 写操作检查路径
if tool_name in ("Write", "Edit"):
path = input_data.get("file_path", "")
if not any(path.startswith(d) for d in ALLOWED_WRITE_DIRS):
return PermissionResultDeny(message=f"禁止写入: {path}")
return PermissionResultAllow()
# Bash 命令检查黑名单
if tool_name == "Bash":
cmd = input_data.get("command", "")
for bad in DANGEROUS_COMMANDS:
if bad in cmd:
return PermissionResultDeny(
message=f"危险命令: {bad}",
interrupt=True,
)
return PermissionResultAllow()
return PermissionResultAllow()
# 组装最终配置
production_options = ClaudeAgentOptions(
permission_mode="acceptEdits",
can_use_tool=production_permissions,
max_budget_usd=2.00,
max_turns=30,
sandbox={
"enabled": True,
"autoAllowBashIfSandboxed": True,
"excludedCommands": ["git"],
},
)
这个配置做了五层防护:
- 权限模式:
acceptEdits自动接受文件编辑,Bash 需确认 - can_use_tool:精细控制每个工具的使用权限
- 沙盒:操作系统级别的隔离
- 预算:最多花 2 美元
- 轮次:最多跑 30 轮
7. 小结
这一章我们学了四层安全防护:
| 层次 | 机制 | 粒度 |
|---|---|---|
| 第一层 | permission_mode |
粗粒度——整体放行或逐个确认 |
| 第二层 | can_use_tool |
细粒度——按工具名、参数自定义策略 |
| 第三层 | sandbox |
系统级——文件系统和网络隔离 |
| 第四层 | max_budget_usd + max_turns |
兜底——费用和轮次限制 |
核心思想很简单:不要把所有安全措施放在一个篮子里。 多层防护,层层递进,任何一层被突破,下一层还能兜住。
下一章,我们进入实战篇——开始设计一个真正的 AI Agent 项目架构。
本章文件清单
07-权限控制与安全/
README.md # 你正在读的这个文件
examples/
permission_demo.py # can_use_tool 回调完整示例
sandbox_demo.py # 沙盒配置示例
budget_guard.py # 预算和轮次控制示例