Agent SDK智能体开发套件2

7 Agent SDK中的Hooks:程序化的拦截

7.1 PreToolUse:执行前拦截

点击查看代码
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher


async def block_dangerous_bash(input_data, tool_use_id, context):
    """拦截危险的 Bash 命令"""
    # 仅处理 Bash 工具调用
    if input_data["tool_name"] != "Bash":
        return {}

    command = input_data["tool_input"].get("command", "")

    # 定义危险命令特征列表
    dangerous = ["rm -rf", "sudo", "chmod 777", "> dev", "mkfs", "dd if="]
    for pattern in dangerous:
        if pattern in command:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"Blocked: {pattern}",
                }
            }

    return {}


async def protect_config_files(input_data, tool_use_id, context):
    """保护关键配置文件不被修改"""
    tool_name = input_data["tool_name"]

    # 仅处理文件写入类工具
    if tool_name not in ["Write", "Edit"]:
        return {}

    file_path = input_data["tool_input"].get("file_path", "")

    # 定义受保护的路径或文件名特征
    protected = [".env", "secrets", "production.yaml", "database/migrations"]
    for p in protected:
        if p in file_path:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"Protected file: {file_path}",
                }
            }

    return {}


# === 配置 Agent 选项,注册安全拦截 Hook ===
options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[block_dangerous_bash]),
            HookMatcher(matcher="Write", hooks=[protect_config_files]),
            HookMatcher(matcher="Edit", hooks=[protect_config_files]),
        ],
    },
)

7.2 PostToolUse:执行后处理

点击查看代码
import json
import subprocess
from datetime import datetime

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher


async def auto_format_on_write(input_data, tool_use_id, context):
    """写入文件后自动触发代码格式化"""
    # 仅处理文件写入类工具
    if input_data["tool_name"] not in ["Write", "Edit"]:
        return {}

    file_path = input_data["tool_input"].get("file_path", "")

    # 根据文件扩展名选择对应的格式化工具
    if file_path.endswith(".py"):
        # 使用 Black 格式化 Python 代码
        subprocess.run(["black", file_path], capture_output=True)
    elif file_path.endswith((".ts", ".js", ".tsx", ".jsx")):
        # 使用 Prettier 格式化前端代码
        subprocess.run(["prettier", "--write", file_path], capture_output=True)

    return {}


async def audit_all_tools(input_data, tool_use_id, context):
    """记录所有工具调用的审计日志"""
    audit_entry = {
        "timestamp": datetime.now().isoformat(),
        "tool": input_data["tool_name"],
        "input": input_data["tool_input"],
        "tool_use_id": tool_use_id,
    }

    # 将审计日志追加写入 JSONL 文件
    with open("agent-audit.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(audit_entry) + "\n")

    return {}


# === 配置 Agent 选项,注册后置 Hook ===
options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [...],  # 此处可接前面定义的安全拦截 Hook
        "PostToolUse": [
            HookMatcher(matcher="Write", hooks=[auto_format_on_write]),
            HookMatcher(matcher="Edit", hooks=[auto_format_on_write]),
            HookMatcher(matcher="*", hooks=[audit_all_tools]),
        ],
    },
)

7.3 Agent SDK Hooks与Shell Hooks的关系

8 4道安全防线

Agent SDK构建了由4道安全防线组成的纵深防御体系

点击查看代码
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher


async def can_use_tool(tool_name: str, tool_input: dict) -> dict:
    """运行时权限检查回调"""

    # 场景1:文件写入保护 - 防止敏感配置文件被篡改
    if tool_name in ["Write", "Edit"]:
        file_path = tool_input.get("file_path", "")
        if ".env" in file_path or "secrets" in file_path:
            return {
                "allowed": False,
                "reason": "Access to sensitive files denied",
            }

    # 场景2:网络命令封锁 - 即使白名单允许 Bash,也要禁止特定网络操作
    if tool_name == "Bash":
        command = tool_input.get("command", "")
        if any(cmd in command for cmd in ["curl", "wget", "ssh"]):
            return {
                "allowed": False,
                "reason": "Network commands not allowed",
            }

    return {"allowed": True}


# === 配置 Agent 选项 ===
options = ClaudeAgentOptions(
    permission_mode="acceptEdits",
    allowed_tools=["Read", "Write", "Edit", "Grep", "Glob"],
    can_use_tool=can_use_tool,
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="*", hooks=[audit_all_tools]),
        ],
    },
)
  • permission_mode确立全局基调,
  • allowed_tools剔除冗余工具,
  • canUseTool在运行时对每次调用实施动态校验,
  • 而PreToolUse Hooks则负责最细粒度的参数审查、输入修正及日志记录审计。

9 结构化输出:强制JSON Schema

  • Python
点击查看代码
from pydantic import BaseModel

from claude_agent_sdk import ClaudeAgentOptions, query


class SecurityReport(BaseModel):
    """安全审查报告结构化输出模型"""

    summary: str
    issues: list[dict]  # 结构示例:[{severity, file, line, description}]
    risk_score: float  # 范围:0.0 ~ 10.0


# === 配置 Agent 选项 ===
options = ClaudeAgentOptions(
    output_format={
        "type": "json_schema",
        "schema": SecurityReport.model_json_schema(),
    },
    max_turns=10,
    allowed_tools=["Read", "Grep", "Glob"],
)


async def run_security_audit():
    """执行安全审查并解析结构化结果"""
    async for message in query(prompt="对 src/ 进行安全审查", options=options):
        if message.type == "result" and message.structured_output:
            report = SecurityReport.model_validate(message.structured_output)

            # report 是类型安全的 Pydantic 对象
            print(f"风险评分:{report.risk_score}")
            for issue in report.issues:
                severity = issue["severity"]
                file_path = issue["file"]
                line = issue.get("line", "?")
                description = issue["description"]
                print(f"[{severity}] {file_path}:{line} - {description}")
  • TypeScript
点击查看代码
import { z } from 'zod';
import type { ClaudeAgentOptions } from 'claude-agent-sdk';

// === 定义安全审查报告结构化输出模型 ===
const SecurityReport = z.object({
  summary: z.string(),
  issues: z.array(
    z.object({
      severity: z.enum(['critical', 'high', 'medium', 'low']),
      file: z.string(),
      description: z.string(),
    }),
  ),
  riskScore: z.number().min(0).max(10),
});

// === 配置 Agent 选项 ===
const options: ClaudeAgentOptions = {
  outputFormat: {
    type: 'json_schema',
    schema: z.toJSONSchema(SecurityReport),
  },
};

10 实战:构建代码分析Web服务

点击查看代码
#!/usr/bin/env python3
"""代码分析Web服务——完整运行示例"""

import asyncio
import json
import sys
from datetime import datetime
from claude_agent_sdk import query, ClaudeAgentOptions


async def analyze_codebase(directory: str, focus: str = "general"):
    """
    分析指定目录的代码库。

    Args:
        directory: 要分析的本地目录路径
        focus: 分析重点领域 - "security" / "performance" / "quality" / "general"
    """
    # 定义不同关注点的 System Prompt
    focus_prompts = {
        "security": "专注于安全漏洞:SQL 注入、XSS、敏感信息硬编码、权限控制缺失等。",
        "performance": "专注于性能问题:N+1 查询、内存泄漏、冗余计算、缓存策略缺失等。",
        "quality": "专注于代码质量:命名规范、DRY原则、圈复杂度、测试覆盖率建议等。",
        "general": "进行全面代码审查:涵盖安全性、性能、代码质量及架构合理性。",
    }

    options = ClaudeAgentOptions(
        model="claude-sonnet-4-6",
        max_turns=15,
        max_budget_usd=0.50,
        allowed_tools=["Read", "Grep", "Glob"],
        permission_mode="plan",  # 只读模式
        cwd=directory,
        append_system_prompt=focus_prompts.get(focus, focus_prompts["general"]),
    )

    # 初始化收集器
    output_text = []
    tools_used = []
    metadata = {}

    # 执行流式查询
    async for message in query(
        prompt=f"分析当前项目的代码,{focus_prompts.get(focus, '')}。请直接输出Markdown格式的分析报告。",
        options=options,
    ):
        if message.type == "assistant":
            for block in message.content:
                if hasattr(block, "text"):
                    output_text.append(block.text)
                    # 【关键点】流式输出:在实际 Web 服务中,此处应为 yield SSE 事件
                    print(block.text, end="", flush=True)
                elif hasattr(block, "name"):
                    tools_used.append(block.name)
        elif message.type == "result":
            metadata = {
                "session_id": message.session_id,
                "cost_usd": message.total_cost_usd,
                "turns": message.num_turns,
                "duration_ms": message.duration_ms,
                "success": not message.is_error,
            }

    # 返回结构化结果
    return {
        "report": "\n".join(output_text),
        "tools_used": tools_used,
        "metadata": metadata,
    }


async def main():
    directory = sys.argv[1] if len(sys.argv) > 1 else "."
    focus = sys.argv[2] if len(sys.argv) > 2 else "general"

    print(f"分析 {directory}(重点:{focus})...\n")
    result = await analyze_codebase(directory, focus)

    print("\n\n--- 会话统计 ---")
    print(f"状态: {'成功' if result['metadata']['success'] else '失败'}")
    print(f"费用:${result['metadata'].get('cost_usd', 0):.4f}")
    print(f"耗时:{result['metadata'].get('duration_ms', 0) / 1000:.1f}s")
    print(f"交互轮数: {result['metadata'].get('turns', 0)}")
    print(f"使用工具:{', '.join(result['tools_used'])}")


if __name__ == "__main__":
    asyncio.run(main())
  • 以下是一个基于FastAPI的完整示例。
点击查看代码
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from claude_agent_sdk import query, ClaudeAgentOptions


app = FastAPI()


class AnalyzeRequest(BaseModel):
    """代码分析请求体"""
    prompt: str
    directory: str = "."
    focus: str = "general"


def build_options(request: AnalyzeRequest) -> ClaudeAgentOptions:
    """根据请求参数构建 Agent 配置"""
    focus_prompts = {
        "security": "专注于安全漏洞:SQL 注入、XSS、敏感信息硬编码、权限控制缺失等。",
        "performance": "专注于性能问题:N+1 查询、内存泄漏、冗余计算、缓存策略缺失等。",
        "quality": "专注于代码质量:命名规范、DRY原则、圈复杂度、测试覆盖率建议等。",
        "general": "进行全面代码审查:涵盖安全性、性能、代码质量及架构合理性。",
    }

    return ClaudeAgentOptions(
        model="claude-sonnet-4-6",
        max_turns=15,
        max_budget_usd=0.50,
        allowed_tools=["Read", "Grep", "Glob"],
        permission_mode="plan",
        cwd=request.directory,
        append_system_prompt=focus_prompts.get(request.focus, focus_prompts["general"]),
    )


@app.post("/api/analyze")
async def analyze(request: AnalyzeRequest):
    options = build_options(request)

    async def event_stream():
        async for message in query(prompt=request.prompt, options=options):
            if message.type == "assistant":
                for block in message.content:
                    if hasattr(block, "text"):
                        payload = {"type": "text", "content": block.text}
                        yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
            elif message.type == "result":
                payload = {"type": "done", "cost": message.total_cost_usd}
                yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"

    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",  # 防止 Nginx 缓冲 SSE 流
        },
    )

11 Agent SDK与Headless CLI:如何选型

  • 如果你的逻辑可以用一行Shell命令描述,请选用CLI;如果需要写if/else判断或循环,请选用Agent SDK。

本章小结

  • query函数是Agent SDK的入口,其功能对标Headless模式的claude -p命令,但以异步生成器的形式返回消息流,支持实时处理Claude的每一步输出。该消息流包含5种类型:system/init(初始化信号)、assistant(Claude的响应,可能包含文本内容及工具调用请求)、user(工具执行后的反馈结果)和result(任务终止信号,携带成本、轮数、耗时等元数据)。
  • ClaudeAgentOptions则提供了与Headless CLI参数对等的编程接口,涵盖模型选择、工具权限、权限模式、Prompt定制、会话管理及结构化输出等功能。相较于命令行,编程接口的核心优势在于动态配置能力:开发者可根据用户角色动态调整权限模式,依据任务类型灵活切换模型,或基于上一轮的执行结果实时优化下一轮的Prompt。
  • 自定义工具通过@tool装饰器与进程内MCP服务器实现,赋予Claude调用任意自定义函数的能力,无论是查询数据库、调用内部API,还是发送通知。与此同时,4道安全防线(权限模式、工具白名单、canUseTool回调、PreToolUse Hooks)确保Claude在应用中只做该做的事。
  • 从CLAUDE.md配置、Skills定义,到Hooks拦截、MCP集成,再到Headless模式与Agent SDK的全面掌控,每深入一层,意味着我们距离构建成熟的“产品”而非单一的“工具”更近一步。
posted @ 2026-08-04 11:27  不知者buwei  阅读(0)  评论(0)    收藏  举报