第 7 章:权限控制与安全

Claude 很强大——它能读文件、写代码、执行命令。但"能力越大,责任越大"。 这一章教你怎么给 Claude 画好边界,让它在你允许的范围内干活。

前提条件

  1. 已完成前 6 章的学习
  2. 了解 query()ClaudeAgentOptions 的基本用法

1. 为什么需要权限控制?

想象你招了一个超级能干的实习生。他会写代码、能跑脚本、还能操作数据库。 但你肯定不会第一天就让他随便动生产环境的代码吧?

Claude 也是一样。它有能力做很多事,但不代表你应该让它什么都做。

几个真实的场景:

权限控制就是你的"安全网"。它不会限制 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 改代码不会出什么大问题, 但执行命令(尤其是 rmgit push 这类)还是让你过个眼比较好。

plan — 先展示计划

Claude 会先告诉你它打算做什么,等你批准后再执行。

就像实习生先写一份计划书:"我打算先读取配置文件,然后修改第 42 行, 最后运行测试。" 你看了觉得没问题,说"行,干吧"。

options = ClaudeAgentOptions(
    permission_mode="plan",
)

适合任务比较复杂、你想对全局有把控的场景。

bypassPermissions — 全部放行

Claude 做什么都不需要你确认。完全自主。

options = ClaudeAgentOptions(
    permission_mode="bypassPermissions",
)

警告: 这个模式意味着 Claude 可以不经你同意执行任何操作。 只在以下条件全部满足时使用:

  1. 运行在隔离的沙盒环境中(比如 Docker 容器)
  2. 没有敏感数据可以被访问
  3. 有其他安全措施兜底(比如预算限制、沙盒设置)

生产环境中,永远不要用这个模式。


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 上下文信息 包含 signalsuggestions 字段

放行: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 做什么项目,都建议过一遍:

必做

建议做

高级

一个生产级配置示例

把本章所有知识点组合起来:

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"],
    },
)

这个配置做了五层防护:

  1. 权限模式acceptEdits 自动接受文件编辑,Bash 需确认
  2. can_use_tool:精细控制每个工具的使用权限
  3. 沙盒:操作系统级别的隔离
  4. 预算:最多花 2 美元
  5. 轮次:最多跑 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                  # 预算和轮次控制示例