版本要求:本文档针对 CodeBuddy Agent SDK v0.1.0 及以上版本。
本文档介绍如何在 SDK 中使用 Hook 系统,在工具执行前后插入自定义逻辑。
概述
Hook 允许您在 CodeBuddy 的会话生命周期内插入自定义逻辑,实现:
工具调用前的校验和拦截
工具执行后的日志记录
用户提交内容的审查
会话开始/结束时的初始化和清理
worktree 创建与清理时的自定义流程支持的事件
事件 | 触发时机 |
PreToolUse | 工具执行前 |
PostToolUse | 工具执行成功后 |
UserPromptSubmit | 用户提交消息时 |
Stop | 主 Agent 响应结束时 |
SubagentStop | 子 Agent 结束时 |
PreCompact | 上下文压缩前 |
WorktreeCreate | 创建隔离 worktree 时 |
WorktreeRemove | 删除隔离 worktree 时 |
unstable_Checkpoint | 文件修改后自动创建检查点时 |
Hook 配置
通过
hooks 选项配置 Hook。每个事件可以有多个 matcher,每个 matcher 可以有多个 hook 回调。基本结构
import { query } from '@tencent-ai/agent-sdk';const q = query({prompt: '帮我分析代码',options: {model: 'deepseek-v3.1',hooks: {PreToolUse: [{matcher: 'Bash', // 只匹配 Bash 工具hooks: [async (input, toolUseId, ctx) => {console.log('即将执行:', input);return { continue: true };}],timeout:5000 // 超时时间(毫秒)}]}}});
from codebuddy_agent_sdk import query, CodeBuddyAgentOptions, HookMatcherasync def pre_tool_hook(input_data, tool_use_id, context):print(f"即将执行: {input_data}")return {"continue_": True}options = CodeBuddyAgentOptions(model="deepseek-v3.1",hooks={"PreToolUse": [HookMatcher(matcher="Bash", # 只匹配 Bash 工具hooks=[pre_tool_hook],timeout=5.0 # 超时时间(秒))]})async for msg in query(prompt="帮我分析代码", options=options):print(msg)
HookMatcher 结构
字段 | 类型 | 说明 |
matcher | string | 匹配模式,支持正则表达式。 * 或空字符串匹配所有 |
hooks | HookCallback[] | 回调函数数组 |
timeout | number | 超时时间(TypeScript 毫秒,Python 秒) |
Matcher 模式
精确匹配:
"Bash" 只匹配 Bash 工具正则匹配:
"Edit|Write" 匹配 Edit 或 Write通配符:
"*" 或 "" 匹配所有工具前缀匹配:
"mcp__.*" 匹配所有 MCP 工具事件类型
PreToolUse
工具执行前触发,可以阻止执行或修改输入。
hooks: {PreToolUse: [{matcher: 'Bash',hooks: [async (input, toolUseId, ctx) => {const command = input.command as string;// 阻止危险命令if (command.includes('rm -rf')) {return {decision: 'block',reason: '危险命令被阻止'};}return { continue: true };}]}]}
async def pre_bash_hook(input_data, tool_use_id, context):command = input_data.get("command", "")# 阻止危险命令if "rm -rf" in command:return {"decision": "block","reason": "危险命令被阻止"}return {"continue_": True}hooks = {"PreToolUse": [HookMatcher(matcher="Bash", hooks=[pre_bash_hook])]}
PostToolUse
工具执行成功后触发,可以添加额外上下文。
hooks: {PostToolUse: [{matcher: 'Write|Edit',hooks: [async (input, toolUseId) => {console.log(`文件已修改: ${input.file_path}`);// 记录修改日志await logFileChange(input.file_path);return { continue: true };}]}]}
async def post_write_hook(input_data, tool_use_id, context):print(f"文件已修改: {input_data.get('file_path')}")# 记录修改日志await log_file_change(input_data.get("file_path"))return {"continue_": True}hooks = {"PostToolUse": [HookMatcher(matcher="Write|Edit", hooks=[post_write_hook])]}
UserPromptSubmit
用户提交消息时触发,可以添加上下文或阻止处理。
hooks: {UserPromptSubmit: [{hooks: [async (input) => {const prompt = input.prompt as string;// 敏感词检查if (containsSensitiveWords(prompt)) {return {decision: 'block',reason: '消息包含敏感内容'};}return { continue: true };}]}]}
async def prompt_check_hook(input_data, tool_use_id, context):prompt = input_data.get("prompt", "")# 敏感词检查if contains_sensitive_words(prompt):return {"decision": "block","reason": "消息包含敏感内容"}return {"continue_": True}hooks = {"UserPromptSubmit": [HookMatcher(hooks=[prompt_check_hook])]}
Stop / SubagentStop
Agent 响应结束时触发,可以阻止停止并要求继续。
hooks: {Stop: [{hooks: [async (input) => {// 检查任务是否真正完成if (!isTaskComplete()) {return {decision: 'block',reason: '任务未完成,请继续'};}return { continue: true };}]}]}
async def stop_hook(input_data, tool_use_id, context):# 检查任务是否真正完成if not is_task_complete():return {"decision": "block","reason": "任务未完成,请继续"}return {"continue_": True}hooks = {"Stop": [HookMatcher(hooks=[stop_hook])]}
unstable_Checkpoint(实验性)
文件修改后(Write/Edit/MultiEdit 工具执行成功)自动触发,提供文件快照和变更统计信息。
实验性 API
此 Hook 为实验性功能,API 可能在未来版本中发生变化。
import type { CheckpointHookInput } from '@tencent-ai/agent-sdk';hooks: {unstable_Checkpoint: [{hooks: [async (input) => {const checkpointInput = input as CheckpointHookInput;const checkpoint = checkpointInput.checkpoint;console.log('文件变更检查点:', {id: checkpoint.id,label: checkpoint.label,files: checkpoint.fileChangeStats?.files,additions: checkpoint.fileChangeStats?.additions,deletions: checkpoint.fileChangeStats?.deletions});// 访问文件快照for (const [filePath, version] of Object.entries(checkpoint.fileSnapshots)) {console.log(` ${filePath} - 版本 ${version.version}`);}return { continue: true };}]}]}
async def checkpoint_hook(input_data, tool_use_id, context):checkpoint = input_data.get("checkpoint", {})file_change_stats = checkpoint.get("fileChangeStats", {})print(f"文件变更检查点:")print(f" ID: {checkpoint.get('id')}")print(f" Label: {checkpoint.get('label')}")print(f" Files: {file_change_stats.get('files', [])}")print(f" Additions: +{file_change_stats.get('additions', 0)} lines")print(f" Deletions: -{file_change_stats.get('deletions', 0)} lines")# 访问文件快照for file_path, version in checkpoint.get("fileSnapshots", {}).items():print(f" {file_path} - 版本 {version.get('version')}")return {"continue_": True}hooks = {"unstable_Checkpoint": [HookMatcher(hooks=[checkpoint_hook])]}
Checkpoint 数据结构:
id: 检查点唯一标识符label: 人类可读标签(通常为用户提示)createdAt: 创建时间戳fileSnapshots: 文件路径到版本信息的映射filePath: 文件绝对路径version: 版本号backupFileName: 备份文件名backupTime: 备份时间戳fileChangeStats: 文件变更统计files: 变更的文件路径列表additions: 新增行数deletions: 删除行数Hook 输入
Hook 回调接收的输入结构因事件类型而异。
公共字段
{"session_id": "abc123","cwd": "/path/to/project","permission_mode": "default","hook_event_name": "PreToolUse"}
PreToolUse / PostToolUse 输入
{"tool_name": "Bash","tool_input": {"command": "ls -la"}}
UserPromptSubmit 输入
{"prompt": "帮我写一个函数"}
Stop / SubagentStop 输入
{"stop_hook_active": false}
WorktreeCreate 输入示例:
{"hook_event_name": "WorktreeCreate","session_id": "abc123","cwd": "/path/to/project","transcript_path": "/path/to/transcript.jsonl","name": "feature-auth"}
WorktreeRemove 输入示例:
{"hook_event_name": "WorktreeRemove","session_id": "abc123","cwd": "/path/to/project","transcript_path": "/path/to/transcript.jsonl","worktree_path": "/tmp/codebuddy-worktrees/feature-auth"}
unstable_Checkpoint 输入
{"checkpoint": {"id": "ckpt_abc123","label": "帮我写一个函数","createdAt": 1705920000000,"fileSnapshots": {"/path/to/file.ts": {"filePath": "/path/to/file.ts","version": 1,"backupFileName": "file.ts.v1.backup","backupTime": 1705920000000}},"fileChangeStats": {"files": ["/path/to/file.ts"],"additions": 10,"deletions": 2}}}
Hook 输出
Hook 回调返回的输出控制后续行为。
基本输出字段
字段 | 类型 | 说明 |
continue / continue_ | boolean | 是否继续执行(默认 true) |
decision | 'block' | 设为 'block' 阻止操作 |
reason | string | 阻止原因 |
stopReason | string | 当 continue 为 false 时显示的停止消息 |
suppressOutput | boolean | 隐藏输出 |
PreToolUse 特殊输出
可以修改工具输入:
return {continue: true,hookSpecificOutput: {hookEventName: 'PreToolUse',updatedInput: {command: `echo "安全检查通过" && ${input.command}`}}};
return {"continue_": True,"hookSpecificOutput": {"hookEventName": "PreToolUse","updatedInput": {"command": f'echo "安全检查通过" && {input_data["command"]}'}}}
PostToolUse 特殊输出
可以向 Agent 追加上下文(
additionalContext),或用 updatedToolOutput 替换将要发送给 Agent 的工具结果(对所有工具生效,常用于压缩冗长输出以节省 token):return {continue: true,hookSpecificOutput: {hookEventName: 'PostToolUse',// 替换工具结果(结果可变短);也可用 additionalContext 追加(结果只会变长)updatedToolOutput: compress(input.tool_response)}};
return {"continue_": True,"hookSpecificOutput": {"hookEventName": "PostToolUse",# 替换工具结果(结果可变短);也可用 additionalContext 追加(结果只会变长)"updatedToolOutput": compress(input_data.get("tool_response"))}}
示例
完整示例:Bash 命令审计
import { query } from '@tencent-ai/agent-sdk';import * as fs from 'fs';const logFile = '/tmp/bash-audit.log';const q = query({prompt: '帮我清理临时文件',options: {model: 'deepseek-v3.1',hooks: {PreToolUse: [{matcher: 'Bash',hooks: [async (input, toolUseId) => {const command = input.command as string;const timestamp = new Date().toISOString();// 记录命令fs.appendFileSync(logFile, `${timestamp} [PRE] ${command}\\n`);// 危险命令检查const dangerous = ['rm -rf /', 'mkfs', ':(){:|:&};:'];for (const d of dangerous) {if (command.includes(d)) {return {decision: 'block',reason: `危险命令被阻止: ${d}`};}}return { continue: true };}]}],PostToolUse: [{matcher: 'Bash',hooks: [async (input, toolUseId) => {const command = input.command as string;const timestamp = new Date().toISOString();// 记录执行完成fs.appendFileSync(logFile, `${timestamp} [POST] ${command} - 完成\\n`);return { continue: true };}]}]}}});for await (const message of q) {console.log(message);}
import asynciofrom datetime import datetimefrom codebuddy_agent_sdk import query, CodeBuddyAgentOptions, HookMatcherlog_file = "/tmp/bash-audit.log"async def pre_bash_hook(input_data, tool_use_id, context):command = input_data.get("command", "")timestamp = datetime.now().isoformat()# 记录命令with open(log_file, "a") as f:f.write(f"{timestamp} [PRE] {command}\\n")# 危险命令检查dangerous = ["rm -rf /", "mkfs", ":(){:|:&};:"]for d in dangerous:if d in command:return {"decision": "block","reason": f"危险命令被阻止: {d}"}return {"continue_": True}async def post_bash_hook(input_data, tool_use_id, context):command = input_data.get("command", "")timestamp = datetime.now().isoformat()# 记录执行完成with open(log_file, "a") as f:f.write(f"{timestamp} [POST] {command} - 完成\\n")return {"continue_": True}async def main():options = CodeBuddyAgentOptions(model="deepseek-v3.1",hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[pre_bash_hook])],"PostToolUse": [HookMatcher(matcher="Bash", hooks=[post_bash_hook])]})async for message in query(prompt="帮我清理临时文件", options=options):print(message)asyncio.run(main())
示例:限制文件修改范围
hooks: {PreToolUse: [{matcher: 'Write|Edit',hooks: [async (input) => {const filePath = input.file_path as string;// 只允许修改 src 目录if (!filePath.startsWith('/path/to/project/src/')) {return {decision: 'block',reason: `不允许修改 src 目录外的文件: ${filePath}`};}// 禁止修改配置文件if (filePath.endsWith('.env') || filePath.includes('.git/')) {return {decision: 'block',reason: '不允许修改敏感文件'};}return { continue: true };}]}]}
async def file_scope_hook(input_data, tool_use_id, context):file_path = input_data.get("file_path", "")# 只允许修改 src 目录if not file_path.startswith("/path/to/project/src/"):return {"decision": "block","reason": f"不允许修改 src 目录外的文件: {file_path}"}# 禁止修改配置文件if file_path.endswith(".env") or ".git/" in file_path:return {"decision": "block","reason": "不允许修改敏感文件"}return {"continue_": True}hooks = {"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[file_scope_hook])]}
示例:文件修改追踪(Checkpoint Hook)
import { query, type CheckpointHookInput } from '@tencent-ai/agent-sdk';import * as fs from 'fs';const changeLog = '/tmp/file-changes.log';const q = query({prompt: '重构 src/utils.ts 文件',options: {model: 'deepseek-v3.1',hooks: {unstable_Checkpoint: [{hooks: [async (input) => {const checkpointInput = input as CheckpointHookInput;const checkpoint = checkpointInput.checkpoint;const stats = checkpoint.fileChangeStats;if (!stats) return { continue: true };// 记录文件变更const timestamp = new Date().toISOString();const logEntry = `[${timestamp}] Checkpoint ${checkpoint.id}Label: ${checkpoint.label}Files: ${stats.files.join(', ')}Changes: +${stats.additions}/-${stats.deletions}Snapshots: ${Object.keys(checkpoint.fileSnapshots).length} files`;fs.appendFileSync(changeLog, logEntry);// 如果变更过大,提醒用户if (stats.additions + stats.deletions > 100) {console.warn('大量代码变更,建议review');}return { continue: true };}]}]}}});for await (const message of q) {console.log(message);}
import asynciofrom datetime import datetimefrom codebuddy_agent_sdk import query, CodeBuddyAgentOptions, HookMatcherchange_log = "/tmp/file-changes.log"async def checkpoint_tracker(input_data, tool_use_id, context):checkpoint = input_data.get("checkpoint", {})stats = checkpoint.get("fileChangeStats")if not stats:return {"continue_": True}# 记录文件变更timestamp = datetime.now().isoformat()log_entry = f"""[{timestamp}] Checkpoint {checkpoint.get('id')}Label: {checkpoint.get('label')}Files: {', '.join(stats.get('files', []))}Changes: +{stats.get('additions', 0)}/-{stats.get('deletions', 0)}Snapshots: {len(checkpoint.get('fileSnapshots', {}))} files"""with open(change_log, "a") as f:f.write(log_entry)# 如果变更过大,提醒用户total_changes = stats.get("additions", 0) + stats.get("deletions", 0)if total_changes > 100:print("⚠️ 大量代码变更,建议 review")return {"continue_": True}async def main():options = CodeBuddyAgentOptions(model="deepseek-v3.1",hooks={"unstable_Checkpoint": [HookMatcher(hooks=[checkpoint_tracker])]})async for message in query(prompt="重构 src/utils.ts 文件", options=options):print(message)asyncio.run(main())
相关文档
SDK 概览 - 快速入门和使用示例
SDK 权限控制 - canUseTool 回调
Hook 参考指南 - CLI Hook 完整参考
TypeScript SDK 参考 - 完整 API 参考
Python SDK 参考 - 完整 API 参考