你的私人助理能操作文件、能执行命令。如果它误删了你的代码怎么办?

第 6 课:Hook 与安全管控


为什么要管 AI?

你的私人助理能操作文件、能执行命令。如果它误删了你的代码怎么办?

code
你: 帮我清理一下临时文件
AI: [执行 rm -rf /tmp/*]     ← 万一删错了呢?
AI: [执行 rm -rf ~/Documents]  ← 万一理解错了呢?

Hook 就是"安全围栏"——在 AI 动手之前先检查,动手之后再审计。

Hook 的工作原理

code
AI 想执行操作
  │
  ├── ① PreToolUse Hook → 执行前检查
  │     ├── allow → 放行
  │     ├── deny → 拦截
  │     └── (空) → 走默认流程
  │
  ├── ② 工具实际执行
  │
  └── ③ PostToolUse Hook → 执行后处理
        ├── 记录日志
        ├── 检查结果
        └── 给 AI 追加提示

场景一:文件保护

bash
from claude_agent_sdk.types import HookInput, HookContext, HookJSONOutput

# 受保护的目录
PROTECTED_PATHS = [
    "~/.ssh",
    "~/.config",
    "~/.env",
    ".git",
    "node_modules",
    "__pycache__",
]

async def file_protection_hook(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """保护重要文件和目录"""

    tool_name = input_data["tool_name"]
    tool_input = input_data.get("tool_input", {})

    # 检查写操作
    if tool_name in ["Write", "Edit", "Bash"]:
        # 获取目标路径
        target = ""
        if tool_name in ["Write", "Edit"]:
            target = tool_input.get("file_path", "")
        elif tool_name == "Bash":
            command = tool_input.get("command", "")
            # 检查危险命令
            dangerous = ["rm -rf", "rm -r", "rmdir", "mv /", "chmod -R"]
            for d in dangerous:
                if d in command:
                    return {
                        "hookSpecificOutput": {
                            "hookEventName": "PreToolUse",
                            "permissionDecision": "deny",
                            "permissionDecisionReason": f"🛡️ 拦截危险命令: {d}",
                        }
                    }

        # 检查受保护路径
        for protected in PROTECTED_PATHS:
            if protected in target:
                return {
                    "hookSpecificOutput": {
                        "hookEventName": "PreToolUse",
                        "permissionDecision": "deny",
                        "permissionDecisionReason": f"🛡️ 受保护目录: {protected}",
                    }
                }

    return {}

场景二:操作审计日志

code
import datetime

audit_log: list[dict] = []

async def audit_hook(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """记录所有操作"""

    entry = {
        "time": datetime.datetime.now().strftime("%H:%M:%S"),
        "tool": input_data.get("tool_name", "?"),
        "input": str(input_data.get("tool_input", ""))[:150],
    }
    audit_log.append(entry)

    return {}


def show_audit_log():
    """显示审计日志"""
    if not audit_log:
        print("📋 暂无操作记录")
        return
    print(f"📋 操作日志({len(audit_log)} 条):")
    for e in audit_log[-10:]:
        print(f"  [{e['time']}] {e['tool']}: {e['input'][:80]}")

场景三:安全命令白名单

python
SAFE_COMMANDS = [
    "ls", "pwd", "echo", "cat", "head", "tail",
    "wc", "grep", "find", "which",
    "python --version", "git status", "git log",
    "date", "cal",
]

async def command_whitelist_hook(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """Bash 命令白名单:安全命令自动放行"""

    if input_data["tool_name"] != "Bash":
        return {}

    command = input_data.get("tool_input", {}).get("command", "").strip()

    for safe in SAFE_COMMANDS:
        if command.startswith(safe):
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "allow",
                    "permissionDecisionReason": f"✅ 安全命令: {safe}",
                }
            }

    # 不在白名单里 → 走默认权限流程(会提示用户确认)
    return {}

场景四:自动批准只读操作

code
async def auto_approve_readonly(
    input_data: HookInput,
    tool_use_id: str | None,
    context: HookContext
) -> HookJSONOutput:
    """自动批准只读工具"""

    readonly_tools = ["Read", "Grep", "Glob",
                      "mcp__productivity__list_todos",
                      "mcp__productivity__search_notes",
                      "mcp__productivity__list_recent_notes",
                      "mcp__productivity__list_files"]

    if input_data["tool_name"] in readonly_tools:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "allow",
                "permissionDecisionReason": "只读操作,自动批准",
            }
        }

    return {}

场景五:权限回调

Hook 是全局拦截,权限回调(Permission Callback)是逐工具审批:

code
from claude_agent_sdk import (
    PermissionResultAllow, PermissionResultDeny,
    ToolPermissionContext,
)

async def productivity_permissions(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    """生产力工具权限控制"""

    # 查看类:自动允许
    if "list" in tool_name or "search" in tool_name or "get" in tool_name:
        return PermissionResultAllow()

    # 添加类:允许
    if "add" in tool_name or "create" in tool_name:
        return PermissionResultAllow()

    # 删除类:需要确认(这里示例打印确认)
    if "delete" in tool_name:
        target = str(input_data)[:100]
        print(f"\n⚠️ 删除操作: {tool_name}")
        print(f"   目标: {target}")
        confirm = input("   确认?(y/N): ").strip().lower()
        if confirm == 'y':
            return PermissionResultAllow()
        return PermissionResultDeny(message="用户取消了删除操作")

    # 其他:允许
    return PermissionResultAllow()

组装安全层

bash
from claude_agent_sdk.types import HookMatcher

options = ClaudeAgentOptions(
    system_prompt="你是 MiniClaw 私人助理...",
    mcp_servers={"productivity": productivity_tools},
    allowed_tools=[...],

    # Hook 配置
    hooks={
        "PreToolUse": [
            # 文件保护(最高优先级)
            HookMatcher(matcher="Bash", hooks=[file_protection_hook]),
            HookMatcher(matcher="Write", hooks=[file_protection_hook]),
            HookMatcher(matcher="Edit", hooks=[file_protection_hook]),
            # 自动批准只读
            HookMatcher(matcher=None, hooks=[auto_approve_readonly]),
            # Bash 白名单
            HookMatcher(matcher="Bash", hooks=[command_whitelist_hook]),
        ],
        "PostToolUse": [
            # 审计日志(记录所有操作)
            HookMatcher(matcher=None, hooks=[audit_hook]),
        ],
    },

    # 权限回调
    can_use_tool=productivity_permissions,
)

Hook 和 Permission Callback 的分工

code
PreToolUse Hook:                    Permission Callback:
├── 文件保护(拦截危险路径)          ├── 查看 → 自动允许
├── 命令白名单(放行安全命令)        ├── 添加 → 自动允许
└── 只读自动批准                     └── 删除 → 需要确认

PostToolUse Hook:
└── 审计日志(记录所有操作)

简单说:Hook 管"不能做什么",Callback 管"怎么做"

本课小结

  • Hook 在工具执行前后拦截,做安全检查
  • PreToolUse:文件保护、命令白名单、只读自动批准
  • PostToolUse:审计日志、结果检查
  • Permission Callback:逐工具精细控制
  • 多层防御:Hook + Callback + 工具内部检查

课后练习

  1. 实现一个"操作频率限制"Hook:同一工具 1 分钟内最多调 10 次
  2. 把审计日志保存到文件,每天一个文件
  3. 实现"撤销"功能:记录最近的写操作,支持回退

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

返回课程目录