跳到主要内容

钩子开发指南

本指南将指导您设置 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/shared
  • strict: 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)

何时使用工厂模式​

当你的钩子需要:

  1. 访问工作区路径 - 知道文件被修改的位置
  2. 跟踪任务/用户上下文 - 根据任务/用户进行日志记录或审计
  3. 调用 API - 使用 RPC 方法,如 createAgentBuilder()
  4. 跨钩子共享状态 - 在工厂中使用闭包变量

直接导出与工厂模式​

直接导出 (简单,向后兼容)

// 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 后,您将获得:

  1. 自动补全 :输入 input. 查看可用属性
  2. 类型提示 :将鼠标悬停在类型上即可查看文档
  3. 错误检测 :立即捕获拼写错误
  4. 跳转到定义 :导航到类型源

示例:

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' };
}

最佳实践​

  1. 类型安全 : 从正确的包中导入类型(@anthropic-ai/claude-agent-sdk 用于 Claude SDK 钩子输入,@agentrix/shared 用于 HookFactory 和 AgentrixContext)
  2. 错误处理 : 使用 try/catch 包裹,优雅地失败
  3. 性能 : 保持钩子快速(通常小于 1 秒)
  4. 日志记录 : 使用 console.log 进行调试
  5. 测试 : 为复杂逻辑编写单元测试
  6. 文档 : 注释钩子行为和边界情况

高级主题​

多文件组织​

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' };
}

警告 :外部依赖会增加构建大小和启动时间。

下一步​

相关文档​