钩子开发指南
本指南将指导您设置 TypeScript 项目,用于开发具有完整类型安全的 Agentrix 钩子。
先决条件
- Node.js 18+和 npm/Bun
- TypeScript 知识
- 对钩子概念的基本理解
项目设置
步骤1:初始化项目
# From your agent root directory
mkdir -p claude/hooks/src
cd claude/hooks
npm init -y
步骤2:安装依赖项
npm install @agentrix/shared
npm install --save-dev @anthropic-ai/claude-agent-sdk @types/node typescript
依赖项说明 :
@agentrix/shared: 当钩子需要任务/工作区上下文时,提供HookFactory和AgentrixContext@anthropic-ai/claude-agent-sdk: 提供 Claude SDK 钩子类型(PreToolUse、PostToolUse 等)@types/node: Node.js 类型定义typescript: TypeScript 编译器
步骤 3:配置 TypeScript
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
关键设置 :
module: "ES2022":使用 ES 模块moduleResolution: "bundler":正确解析@agentrix/sharedstrict: true: 启用所有 TypeScript 严格检查outDir: "./dist": 输出到 dist/目录
步骤 4: 更新 package.json
{
"name": "my-agent-hooks",
"version": "1.0.0",
"type": "module",
"description": "Custom hooks for my-agent",
"main": "dist/index.mjs",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentrix/shared": "^1.0.0"
},
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.30",
"@types/node": "^20.0.0",
"typescript": "^5.0.0"
}
}
重要 : 为 ES 模块支持设置 "type": "module"
第 5 步:创建.gitignore 文件
node_modules/
dist/
*.log
.DS_Store
创建你的第一个钩子
基本钩子模板
创建 src/index.ts:
// Claude SDK hooks
import type {
PreToolUseHookInput,
PostToolUseHookInput,
} from '@anthropic-ai/claude-agent-sdk';
/**
* PreToolUse: Called before each tool execution
*/
export async function PreToolUse(
input: PreToolUseHookInput,
toolUseID: string,
options: { signal: AbortSignal }
) {
console.log(`[PreToolUse] ${input.tool_name}`);
return { decision: 'approve' as const };
}
/**
* PostToolUse: Called after each tool execution
*/
export async function PostToolUse(
input: PostToolUseHookInput,
toolUseID: string,
options: { signal: AbortSignal }
) {
console.log(`[PostToolUse] ${input.tool_name} completed`);
return {};
}
构建和测试
# Build hooks
npm run build
# Check output
ls -la dist/
# Expected output:
# index.mjs - Compiled JavaScript
# index.d.ts - Type definitions
# index.d.ts.map - Source map for types
# index.mjs.map - Source map for code
在钩子中使用 AgentrixContext
钩子可以通过 AgentrixContext 获取工作区信息、任务/用户 ID,并调用 API 服务器的 RPC。这是通过工厂模式实现的。
工厂模式
不是直接导出钩子,而是导出一个默认函数,该函数接收上下文并返回你的钩子:
// src/index.ts
import type { HookFactory, AgentrixContext } from '@agentrix/shared';
import type { PreToolUseHookInput, PostToolUseHookInput } from '@anthropic-ai/claude-agent-sdk';
/**
* Factory function that receives AgentrixContext
*/
const createHooks: HookFactory = (context: AgentrixContext) => ({
PreToolUse: async (
input: PreToolUseHookInput,
toolUseID: string,
options: { signal: AbortSignal }
) => {
// Access context methods
const workspace = context.getWorkspace();
const taskId = context.getTaskId();
const userId = context.getUserId();
console.log(`[PreToolUse] Task ${taskId} by user ${userId}`);
console.log(`[PreToolUse] Workspace: ${workspace}`);
console.log(`[PreToolUse] Tool: ${input.tool_name}`);
return { decision: 'approve' as const };
},
PostToolUse: async (
input: PostToolUseHookInput,
toolUseID: string,
options: { signal: AbortSignal }
) => {
const taskId = context.getTaskId();
console.log(`[PostToolUse] Task ${taskId}: ${input.tool_name} completed`);
return {};
},
});
export default createHooks;
AgentrixContext 方法
AgentrixContext 接口提供:
| 方法 | 返回类型 | 描述 |
|---|---|---|
| getWorkspace() | string | 绝对路径到工作区目录 |
| getTaskId() | string | 当前任务 ID |
| getUserId() | string | 当前用户 ID |
| createAgentBuilder(params) | Promise<{agentId, displayName}> | 创建一个新的代理构建器(RPC) |
何时使用工厂模式
当你的钩子需要:
- 访问工作区路径 - 知道文件被修改的位置
- 跟踪任务/用户上下文 - 根据任务/用户进行日志记录或审计
- 调用 API - 使用 RPC 方法,如
createAgentBuilder() - 跨钩子共享状态 - 在工厂中使用闭包变量
直接导出与工厂模式
直接导出 (简单,向后兼容)
// No context access
export async function PreToolUse(input: PreToolUseHookInput) {
return { decision: 'approve' as const };
}
工厂模式 (带上下文访问):
// Full context access
const createHooks: HookFactory = (context) => ({
PreToolUse: async (input) => {
const workspace = context.getWorkspace();
// ...
},
});
export default createHooks;
两种模式都完全支持。对于简单的钩子,使用直接导出;当需要上下文时,使用工厂模式。
类型安全
导入类型
钩子类型来自两个包:
// Claude SDK hooks (11 types)
import type {
PreToolUseHookInput,
PostToolUseHookInput,
SessionStartHookInput,
SessionEndHookInput,
UserPromptSubmitHookInput,
StopHookInput,
SubagentStartHookInput,
SubagentStopHookInput,
PreCompactHookInput,
NotificationHookInput,
PermissionRequestHookInput,
} from '@anthropic-ai/claude-agent-sdk';
// Agentrix context helper types
import type {
HookFactory,
AgentrixContext,
} from '@agentrix/shared';
类型检查
# Type-check without building
npm run typecheck
# Watch mode for development
npm run dev
IDE 支持
配置 TypeScript 后,您将获得:
- 自动补全 :输入
input.查看可用属性 - 类型提示 :将鼠标悬停在类型上即可查看文档
- 错误检测 :立即捕获拼写错误
- 跳转到定义 :导航到类型源
示例:
export async function PreToolUse(input: PreToolUseHookInput) {
// TypeScript knows input has these properties:
input.tool_name // ✓ string
input.tool_input // ✓ any
input./* cursor */ // IDE shows autocomplete
// TypeScript catches errors:
input.toolName // ✗ Property 'toolName' does not exist
}
钩子输入类型参考
PreToolUseHookInput
interface PreToolUseHookInput {
// BaseHookInput fields
hook_event_name: 'PreToolUse';
session_id: string;
transcript_path: string;
cwd: string;
permission_mode?: string;
// PreToolUse-specific fields
tool_name: string; // Tool being called (e.g., "Read", "Bash")
tool_input: unknown; // Tool-specific input parameters
tool_use_id: string; // Unique tool use identifier
}
// Return type
type PreToolUseResult = {
decision?: 'approve' | 'block';
systemMessage?: string; // Shown to user if blocked
hookSpecificOutput?: {
hookEventName: 'PreToolUse';
permissionDecision?: 'allow' | 'deny' | 'ask';
permissionDecisionReason?: string;
updatedInput?: Record<string, unknown>;
};
};
PostToolUseHookInput
interface PostToolUseHookInput {
// BaseHookInput fields
hook_event_name: 'PostToolUse';
session_id: string;
transcript_path: string;
cwd: string;
permission_mode?: string;
// PostToolUse-specific fields
tool_name: string; // Tool that was called
tool_input: unknown; // Original tool input
tool_use_id: string; // Unique tool use identifier
tool_response: unknown; // Tool execution result
}
// Return type
type PostToolUseResult = {
hookSpecificOutput?: {
hookEventName: 'PostToolUse';
additionalContext?: string;
updatedMCPToolOutput?: unknown;
};
};
参见钩子类型参考以获取完整的类型定义。
开发流程
1. 编写钩子
// src/index.ts
export async function PreToolUse(input: PreToolUseHookInput) {
// Your logic here
return { decision: 'approve' };
}
2. 类型检查
npm run typecheck
3. 构建
npm run build
4. 本地测试
# From agent root
agentrix test .
5. 迭代
使用监视模式进行快速开发:
npm run dev # Rebuilds on file changes
常见模式
模式1:安全批准逻辑
export async function PreToolUse(
input: PreToolUseHookInput,
toolUseID: string,
options: { signal: AbortSignal }
) {
try {
// Your logic
if (shouldDeny(input)) {
return { decision: 'deny' as const, message: 'Reason' };
}
return { decision: 'approve' as const };
} catch (error) {
console.error('[PreToolUse] Error:', error);
// Fail open: approve on error
return { decision: 'approve' as const };
}
}
模式2:中止信号处理
export async function PreToolUse(
input: PreToolUseHookInput,
toolUseID: string,
options: { signal: AbortSignal }
) {
// Check if aborted
if (options.signal.aborted) {
return { decision: 'approve' as const };
}
// Listen for abort
const controller = new AbortController();
options.signal.addEventListener('abort', () => {
controller.abort();
});
// Use signal in async operations
const response = await fetch('https://api.example.com', {
signal: controller.signal
});
return { decision: 'approve' as const };
}
模式3:类型安全工具输入访问
export async function PreToolUse(input: PreToolUseHookInput) {
// Type guard for Bash tool
if (input.tool_name === 'Bash') {
const command = input.tool_input?.command as string | undefined;
if (command?.includes('rm -rf')) {
return { decision: 'deny' as const };
}
}
// Type guard for Read tool
if (input.tool_name === 'Read') {
const filePath = input.tool_input?.file_path as string | undefined;
if (filePath?.endsWith('.env')) {
return { decision: 'pending' as const, message: 'Reading .env file' };
}
}
return { decision: 'approve' as const };
}
模式4:辅助函数
// src/index.ts
import { isDangerousCommand, logToolUse } from './utils';
export async function PreToolUse(input: PreToolUseHookInput) {
if (isDangerousCommand(input)) {
return { decision: 'deny' as const };
}
await logToolUse(input);
return { decision: 'approve' as const };
}
// src/utils.ts
import type { PreToolUseHookInput } from '@anthropic-ai/claude-agent-sdk';
export function isDangerousCommand(input: PreToolUseHookInput): boolean {
if (input.tool_name !== 'Bash') return false;
const cmd = input.tool_input?.command as string;
const dangerousPatterns = [
/rm\s+-rf\s+\//,
/dd\s+if=/,
/mkfs/,
/:\(\)\{ :\|:\& \};:/ // fork bomb
];
return dangerousPatterns.some(pattern => pattern.test(cmd));
}
export async function logToolUse(input: PreToolUseHookInput) {
// Implementation
}
测试钩子
单元测试
npm install --save-dev vitest @vitest/ui
创建 vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
创建 src/index.test.ts:
import { describe, it, expect } from 'vitest';
import { PreToolUse } from './index';
import type { PreToolUseHookInput } from '@anthropic-ai/claude-agent-sdk';
describe('PreToolUse', () => {
it('approves safe commands', async () => {
const input: PreToolUseHookInput = {
hook_event_name: 'PreToolUse',
session_id: 'test-session',
transcript_path: '/tmp/transcript',
cwd: '/tmp',
tool_name: 'Read',
tool_input: { file_path: 'test.txt' },
tool_use_id: 'test-tool-use-id'
};
const result = await PreToolUse(input, 'test-id', {
signal: new AbortController().signal
});
expect(result.decision).toBe('approve');
});
it('denies dangerous commands', async () => {
const input: PreToolUseHookInput = {
hook_event_name: 'PreToolUse',
session_id: 'test-session',
transcript_path: '/tmp/transcript',
cwd: '/tmp',
tool_name: 'Bash',
tool_input: { command: 'rm -rf /' },
tool_use_id: 'test-tool-use-id'
};
const result = await PreToolUse(input, 'test-id', {
signal: new AbortController().signal
});
expect(result.decision).toBe('deny');
});
it('handles abort signal', async () => {
const input: PreToolUseHookInput = {
tool_name: 'Read',
tool_input: {}
};
const controller = new AbortController();
controller.abort();
const result = await PreToolUse(input, 'test-id', {
signal: controller.signal
});
// Should still return a result
expect(result).toHaveProperty('decision');
});
});
运行测试:
npx vitest
集成测试
使用真实代理测试钩子:
agentrix test --agent=. --task="Create a file called test.txt"
查看钩子输出:
[PreToolUse] Write
[PreToolUse] Approved: Write tool
[PostToolUse] Write completed
故障排除
模块解析错误
错误 : Cannot find module '@agentrix/shared'
解决方案 : 确保 tsconfig.json 中有 moduleResolution: "bundler"
{
"compilerOptions": {
"moduleResolution": "bundler" // Not "node"
}
}
构建错误
错误 : error TS2307: Cannot find module '@agentrix/shared'
解决方案 : 安装依赖项
npm install
运行时错误
错误 : 钩子失败:超时
解决方案 : 确保钩子在 30 秒内完成
export async function PreToolUse(input: PreToolUseHookInput) {
// ✗ Slow: External API call
await fetch('https://slow-api.com');
// ✓ Fast: Local check
const isAllowed = checkLocally(input);
return { decision: isAllowed ? 'approve' : 'deny' };
}
最佳实践
- 类型安全 : 从正确的包中导入类型(
@anthropic-ai/claude-agent-sdk用于 Claude SDK 钩子输入,@agentrix/shared用于HookFactory和AgentrixContext) - 错误处理 : 使用 try/catch 包裹,优雅地失败
- 性能 : 保持钩子快速(通常小于 1 秒)
- 日志记录 : 使用 console.log 进行调试
- 测试 : 为复杂逻辑编写单元测试
- 文档 : 注释钩子行为和边界情况
高级主题
多文件组织
src/
├── index.ts # Main exports
├── preToolUse.ts # PreToolUse logic
├── postToolUse.ts # PostToolUse logic
├── utils.ts # Shared utilities
└── types.ts # Custom type definitions
// src/index.ts
export { PreToolUse } from './preToolUse';
export { PostToolUse } from './postToolUse';
外部依赖
您可以在钩子中使用 npm 包:
npm install axios
import axios from 'axios';
export async function PreToolUse(input: PreToolUseHookInput) {
const response = await axios.post('https://api.example.com', input);
return { decision: response.data.allowed ? 'approve' : 'deny' };
}
警告 :外部依赖会增加构建大小和启动时间。
下一步
- 钩子类型参考 - 完整类型定义
- 示例 - 复制粘贴模式
- PreToolUse & PostToolUse - 工具控制钩子