Hooks 是 Blade Agent SDK 的运行时扩展机制,让你可以在 Agent 执行过程中拦截、审计、修改工具调用行为。
import { createSession, HookEvent } from '@blade-ai/agent-sdk';
const session = await createSession({
provider: { type: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY },
model: 'claude-sonnet-4-20250514',
hooks: {
[HookEvent.PreToolUse]: [
async (input) => {
console.log(`[工具调用] ${input.toolName}`, input.toolInput);
return { action: 'continue' };
},
],
[HookEvent.PostToolUse]: [
async (input) => {
console.log(`[调用完成] ${input.toolName}`);
return { action: 'continue' };
},
],
},
});type HookCallback = (input: HookInput) => Promise<HookOutput>;interface HookInput {
event: HookEvent;
toolName?: string;
toolInput?: unknown;
toolOutput?: unknown;
error?: Error;
sessionId: string;
[key: string]: unknown;
}interface HookOutput {
action: 'continue' | 'skip' | 'abort';
modifiedInput?: unknown;
modifiedOutput?: unknown;
reason?: string;
}三种 action 的含义:
| action | 说明 | 适用场景 |
|---|---|---|
continue |
继续正常执行 | 审计日志、数据收集 |
skip |
跳过当前工具调用 | 阻止特定操作 |
abort |
终止整个会话 | 检测到危险行为 |
会话创建时触发。
hooks: {
[HookEvent.SessionStart]: [
async (input) => {
console.log(`会话 ${input.sessionId} 已启动`);
return { action: 'continue' };
},
],
}HookInput 字段: event、sessionId
用户发送消息时触发。
hooks: {
[HookEvent.UserPromptSubmit]: [
async (input) => {
const message = input.message as string;
console.log(`用户输入: ${message}`);
return { action: 'continue' };
},
],
}HookInput 字段: event、sessionId、message
工具执行之前触发。这是最常用的 Hook,可以用来审计、修改输入、或阻止执行。
hooks: {
[HookEvent.PreToolUse]: [
async (input) => {
// 阻止删除操作
if (input.toolName === 'Bash') {
const command = (input.toolInput as { command: string }).command;
if (command.includes('rm -rf')) {
return { action: 'abort', reason: '禁止执行危险的删除命令' };
}
}
return { action: 'continue' };
},
],
}HookInput 字段: event、sessionId、toolName、toolInput
支持的 HookOutput 操作:
continue— 正常执行continue+modifiedInput— 修改输入后执行skip— 跳过此次工具调用abort— 终止会话
工具执行成功后触发。
hooks: {
[HookEvent.PostToolUse]: [
async (input) => {
console.log(`${input.toolName} 执行成功:`, input.toolOutput);
return { action: 'continue' };
},
],
}HookInput 字段: event、sessionId、toolName、toolInput、toolOutput
支持的 HookOutput 操作:
continue— 正常返回结果continue+modifiedOutput— 修改输出后返回
工具执行失败后触发。
hooks: {
[HookEvent.PostToolUseFailure]: [
async (input) => {
console.error(`${input.toolName} 执行失败:`, input.error?.message);
return { action: 'continue' };
},
],
}HookInput 字段: event、sessionId、toolName、error
权限检查时触发。
hooks: {
[HookEvent.PermissionRequest]: [
async (input) => {
console.log(`权限请求: ${input.toolName}`);
return { action: 'continue' };
},
],
}HookInput 字段: event、sessionId、toolName、toolInput
任务/子任务完成时触发。
hooks: {
[HookEvent.TaskCompleted]: [
async (input) => {
console.log('任务完成');
return { action: 'continue' };
},
],
}会话结束时触发。
hooks: {
[HookEvent.SessionEnd]: [
async (input) => {
console.log(`会话 ${input.sessionId} 已结束`);
return { action: 'continue' };
},
],
}通过 modifiedInput 在工具执行前修改参数:
hooks: {
[HookEvent.PreToolUse]: [
async (input) => {
if (input.toolName === 'Write') {
const params = input.toolInput as { filePath: string; content: string };
return {
action: 'continue',
modifiedInput: {
...params,
content: `// Auto-generated\n${params.content}`,
},
};
}
return { action: 'continue' };
},
],
}通过 modifiedOutput 在工具执行后修改返回值:
hooks: {
[HookEvent.PostToolUse]: [
async (input) => {
if (input.toolName === 'Read') {
const output = input.toolOutput as { content: string };
return {
action: 'continue',
modifiedOutput: {
...output,
content: output.content.replace(/SECRET_KEY=\w+/g, 'SECRET_KEY=***'),
},
};
}
return { action: 'continue' };
},
],
}canUseTool 与 Hooks 是两个独立的系统:
| 特性 | Hooks | canUseTool |
|---|---|---|
| 用途 | 拦截、审计、修改 | 权限决策 |
| 返回值 | HookOutput (continue/skip/abort) | PermissionResult (allow/deny/ask) |
| 执行时机 | 工具调用前后 | 权限检查时 |
type CanUseTool = (
toolName: string,
input: Record<string, unknown>,
options: CanUseToolOptions,
) => Promise<PermissionResult>;
interface CanUseToolOptions {
signal: AbortSignal;
toolKind: 'readonly' | 'write' | 'execute';
affectedPaths: string[];
}// 允许执行(可选修改输入)
{ behavior: 'allow', updatedInput?: Record<string, unknown> }
// 拒绝执行
{ behavior: 'deny', message: string, interrupt?: boolean }
// 交给内置权限系统决定
{ behavior: 'ask' }const session = await createSession({
provider: { type: 'openai', apiKey: process.env.OPENAI_API_KEY },
model: 'gpt-4o',
canUseTool: async (toolName, input, options) => {
if (options.toolKind === 'readonly') {
return { behavior: 'allow' };
}
// 对写操作弹出自定义确认框
const approved = await showConfirmDialog(
`允许 ${toolName} 操作 ${options.affectedPaths.join(', ')}?`,
);
return approved
? { behavior: 'allow' }
: { behavior: 'deny', message: '用户拒绝' };
},
});const auditLog: Array<{ time: string; tool: string; input: unknown }> = [];
const session = await createSession({
// ...provider, model
hooks: {
[HookEvent.PreToolUse]: [
async (input) => {
auditLog.push({
time: new Date().toISOString(),
tool: input.toolName ?? 'unknown',
input: input.toolInput,
});
return { action: 'continue' };
},
],
[HookEvent.SessionEnd]: [
async () => {
const { writeFileSync } = await import('node:fs');
writeFileSync('audit.json', JSON.stringify(auditLog, null, 2));
return { action: 'continue' };
},
],
},
});const DANGEROUS_PATTERNS = [/rm\s+-rf/, /mkfs/, /dd\s+if=/, />\s*\/dev\//];
hooks: {
[HookEvent.PreToolUse]: [
async (input) => {
if (input.toolName === 'Bash') {
const cmd = (input.toolInput as { command: string }).command;
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(cmd)) {
return { action: 'abort', reason: `危险命令被阻止: ${cmd}` };
}
}
}
return { action: 'continue' };
},
],
}const callCounts = new Map<string, number>();
const MAX_CALLS_PER_TOOL = 50;
hooks: {
[HookEvent.PreToolUse]: [
async (input) => {
const name = input.toolName ?? '';
const count = (callCounts.get(name) ?? 0) + 1;
callCounts.set(name, count);
if (count > MAX_CALLS_PER_TOOL) {
return { action: 'skip', reason: `${name} 调用次数超过限制 (${MAX_CALLS_PER_TOOL})` };
}
return { action: 'continue' };
},
],
}同一事件可以注册多个 Hook,它们按数组顺序依次执行:
hooks: {
[HookEvent.PreToolUse]: [hookA, hookB, hookC],
}- 按
hookA→hookB→hookC顺序执行 - 如果任意一个返回
skip或abort,后续 Hook 不再执行 - 如果
hookA返回modifiedInput,hookB收到的是修改后的输入
::: warning Hook 回调中抛出的异常会被捕获并记录,但不会阻止工具执行。务必在回调内做好 try/catch: :::
async (input) => {
try {
await sendToMonitoring(input);
} catch (err) {
console.error('Hook 执行失败:', err);
}
return { action: 'continue' };
};