AI 能执行 Bash 命令,这很强大,但也很危险。你需要一个"安检系统":

第 5 课:Hook 安全管控


为什么 Hook 是 DevOps 助手的灵魂?

AI 能执行 Bash 命令,这很强大,但也很危险。你需要一个"安检系统":

bash
没有 Hook:
  AI: "我来清理一下临时文件"
  AI: Bash → rm -rf /     ← 😱 灾难!

有了 Hook:
  AI: "我来清理一下临时文件"
  AI: Bash → rm -rf /
  Hook: 🚫 "拦截!危险命令!"   ← 😮‍💨 救命!

Hook 就是 AI 工具调用的"拦截器"——在 AI 用工具之前检查、之后审计。

Hook 类型一览

Hook 什么时候触发 你能干嘛
PreToolUse AI 要用工具之前 拦截危险操作、自动批准安全操作
PostToolUse 工具执行完之后 审计日志、给 AI 额外反馈
UserPromptSubmit 用户发消息时 注入额外上下文
SubagentStart 子 Agent 启动时 跟踪子 Agent
Stop AI 停止工作时 清理资源

DevOps 最常用的是前两个:PreToolUse(拦截)和 PostToolUse(审计)。

PreToolUse:命令拦截

最基本的拦截

bash
from claude_agent_sdk.types import HookInput, HookContext, HookJSONOutput

async def safety_hook(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """拦截危险的 Bash 命令"""

    tool_name = input_data["tool_name"]
    tool_input = input_data["tool_input"]

    # 只关心 Bash 命令
    if tool_name != "Bash":
        return {}

    command = tool_input.get("command", "")

    # 危险命令黑名单
    dangerous_patterns = [
        "rm -rf /",           # 删除根目录
        "rm -rf ~",           # 删除用户目录
        "dd if=/dev/zero",    # 覆盖磁盘
        "> /dev/sda",         # 破坏磁盘
        "chmod -R 777",       # 权限全开
        ":(){ :|:& };:",      # Fork 炸弹
    ]

    for pattern in dangerous_patterns:
        if pattern in command:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"危险命令已拦截: {pattern}",
                }
            }

    # 不在黑名单里,放行
    return {}

注册 Hook

bash
from claude_agent_sdk import ClaudeAgentOptions
from claude_agent_sdk.types import HookMatcher

options = ClaudeAgentOptions(
    allowed_tools=["Bash", "Read", "Write"],
    hooks={
        "PreToolUse": [
            HookMatcher(
                matcher="Bash",       # 只匹配 Bash 工具
                hooks=[safety_hook],   # 用这个函数检查
            ),
        ],
    }
)

HookMatcher 有两个字段: - matcher:匹配哪些工具。"Bash" = 只匹配 Bash;None = 匹配所有工具 - hooks:匹配后用哪些函数来检查

自动批准安全操作

除了拦截危险操作,Hook 还能自动批准安全操作,免去人工确认:

bash
async def auto_approve_safe_commands(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """自动批准只读命令"""

    tool_name = input_data["tool_name"]
    tool_input = input_data["tool_input"]

    # 读文件:自动批准
    if tool_name in ["Read", "Grep"]:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "allow",
                "permissionDecisionReason": "只读操作,自动批准",
            }
        }

    # 安全的 Bash 命令:自动批准
    if tool_name == "Bash":
        command = tool_input.get("command", "")
        safe_prefixes = ["ls", "cat", "grep", "find", "ps", "df", "du",
                         "git status", "git log", "git diff", "docker ps"]
        if any(command.strip().startswith(p) for p in safe_prefixes):
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "allow",
                    "permissionDecisionReason": "安全命令,自动批准",
                }
            }

    # 其他情况:不表态(交给默认权限系统处理)
    return {}

PostToolUse:审计日志

code
import datetime
import json

audit_log = []

async def audit_hook(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """记录所有工具调用"""

    tool_response = input_data.get("tool_response", "")

    log_entry = {
        "time": datetime.datetime.now().isoformat(),
        "tool": input_data.get("tool_name", "unknown"),
        "input": str(input_data.get("tool_input", {}))[:200],
        "has_error": "error" in str(tool_response).lower(),
    }
    audit_log.append(log_entry)

    # 如果工具报错了,给 AI 额外提示
    if log_entry["has_error"]:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PostToolUse",
                "additionalContext": "上一个命令出错了,请检查命令是否正确。",
            }
        }

    return {}

紧急停止

PostToolUse 还能让你在发现严重问题时紧急停止 AI

code
async def emergency_stop_hook(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """发现严重错误时停止执行"""

    tool_response = str(input_data.get("tool_response", ""))

    if "CRITICAL" in tool_response or "FATAL" in tool_response:
        return {
            "continue_": False,  # 停止!
            "stopReason": "检测到严重错误,紧急停止",
        }

    return {"continue_": True}

组合多个 Hook

实际使用中,你通常需要组合多个 Hook:

bash
options = ClaudeAgentOptions(
    allowed_tools=["Bash", "Read", "Write"],
    hooks={
        "PreToolUse": [
            # 1. 安全拦截(所有工具)
            HookMatcher(matcher=None, hooks=[safety_hook]),
            # 2. 自动批准安全操作(所有工具)
            HookMatcher(matcher=None, hooks=[auto_approve_safe_commands]),
        ],
        "PostToolUse": [
            # 3. 审计日志(所有工具)
            HookMatcher(matcher=None, hooks=[audit_hook]),
            # 4. 紧急停止(只监控 Bash)
            HookMatcher(matcher="Bash", hooks=[emergency_stop_hook]),
        ],
    }
)

执行顺序:

graph TD A["AI 要用 Bash 执行 ls -la"] --> B["PreToolUse 阶段"] B --> B1["safety_hook → 不在黑名单,放行"] B --> B2["auto_approve_safe_commands → ls 是安全命令,自动批准"] B2 --> C["执行 Bash(ls -la)"] C --> D["PostToolUse 阶段"] D --> D1["audit_hook → 记录到审计日志"] D --> D2["emergency_stop_hook → 没有 CRITICAL,继续"]

Hook 的三种返回决策

code
# 1. 拦截(deny)
return {"hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "原因"
}}

# 2. 批准(allow)
return {"hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "原因"
}}

# 3. 不表态(交给默认机制)
return {}

本课小结

  • Hook 是 AI 工具调用的"拦截器"
  • PreToolUse:在工具执行前拦截危险操作、自动批准安全操作
  • PostToolUse:审计日志、错误检测、紧急停止
  • 多个 Hook 可以组合使用,按顺序执行
  • 三种决策:deny(拦截)、allow(批准)、不表态(交给默认机制)

课后练习

  1. 把 safety_hook 跑起来,试试让 AI 执行 rm -rf /,看看是否被拦截
  2. 给 audit_hook 加上"把日志写到文件"的功能
  3. 写一个 Hook,限制 AI 只能操作特定目录(比如 /tmp/ai-workspace/

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录