Hook Development Guide
This guide walks through setting up a TypeScript project for developing Agentrix hooks with full type safety.
Prerequisites
- Node.js 18+ and npm/Bun
- TypeScript knowledge
- Basic understanding of hooks concepts
Project Setup
Step 1: Initialize Project
# From your agent root directory
mkdir -p claude/hooks/src
cd claude/hooks
npm init -y
Step 2: Install Dependencies
npm install @agentrix/shared
npm install --save-dev @anthropic-ai/claude-agent-sdk @types/node typescript
Dependencies explained:
@agentrix/shared: ProvidesHookFactoryandAgentrixContextwhen hooks need task/workspace context@anthropic-ai/claude-agent-sdk: Provides Claude SDK hook types (PreToolUse, PostToolUse, etc.)@types/node: Node.js type definitionstypescript: TypeScript compiler
Step 3: Configure TypeScript
Create 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"]
}
Key settings:
module: "ES2022": Use ES modulesmoduleResolution: "bundler": Resolve@agentrix/sharedcorrectlystrict: true: Enable all TypeScript strict checksoutDir: "./dist": Output to dist/ directory
Step 4: Update 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"
}
}
Important: Set "type": "module" for ES module support.
Step 5: Create .gitignore
node_modules/
dist/
*.log
.DS_Store
Creating Your First Hook
Basic Hook Template
Create 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 and Test
# 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
Using AgentrixContext in Hooks
Hooks can access AgentrixContext to get workspace information, task/user IDs, and make RPC calls to the API server. This is done using the factory pattern.
Factory Pattern
Instead of exporting hooks directly, export a default function that receives the context and returns your hooks:
// 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 Methods
The AgentrixContext interface provides:
| Method | Return Type | Description |
|---|---|---|
getWorkspace() | string | Absolute path to the workspace directory |
getTaskId() | string | Current task ID |
getUserId() | string | Current user ID |
createAgentBuilder(params) | Promise<{agentId, displayName}> | Create a new agent builder (RPC) |
When to Use Factory Pattern
Use the factory pattern when your hooks need to:
- Access workspace path - Know where files are being modified
- Track task/user context - Log or audit based on task/user
- Make API calls - Use RPC methods like
createAgentBuilder() - Share state across hooks - Use closure variables in the factory
Direct Export vs Factory Pattern
Direct exports (simple, backwards-compatible):
// No context access
export async function PreToolUse(input: PreToolUseHookInput) {
return { decision: 'approve' as const };
}
Factory pattern (with context access):
// Full context access
const createHooks: HookFactory = (context) => ({
PreToolUse: async (input) => {
const workspace = context.getWorkspace();
// ...
},
});
export default createHooks;
Both patterns are fully supported. Use direct exports for simple hooks, factory pattern when you need context.
Type Safety
Importing Types
Hook types come from two packages:
// 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 Checking
# Type-check without building
npm run typecheck
# Watch mode for development
npm run dev
IDE Support
With TypeScript configured, you get:
- Autocomplete: Type
input.and see available properties - Type hints: Hover over types to see documentation
- Error detection: Typos caught immediately
- Jump to definition: Navigate to type source
Example:
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
}
Hook Input Types Reference
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;
};
};
See Hook Types Reference for complete type definitions.
Development Workflow
1. Write Hook
// src/index.ts
export async function PreToolUse(input: PreToolUseHookInput) {
// Your logic here
return { decision: 'approve' };
}
2. Type Check
npm run typecheck
3. Build
npm run build
4. Test Locally
# From agent root
agentrix test .
5. Iterate
Use watch mode for rapid development:
npm run dev # Rebuilds on file changes
Common Patterns
Pattern 1: Safe Approval Logic
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 };
}
}
Pattern 2: Abort Signal Handling
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 };
}
Pattern 3: Type-Safe Tool Input Access
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 };
}
Pattern 4: Helper Functions
// 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
}
Testing Hooks
Unit Tests
npm install --save-dev vitest @vitest/ui
Create vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
Create 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');
});
});
Run tests:
npx vitest
Integration Tests
Test hooks with real agent:
agentrix test --agent=. --task="Create a file called test.txt"
Watch hook output:
[PreToolUse] Write
[PreToolUse] Approved: Write tool
[PostToolUse] Write completed
Troubleshooting
Module Resolution Errors
Error: Cannot find module '@agentrix/shared'
Solution: Ensure moduleResolution: "bundler" in tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler" // Not "node"
}
}
Build Errors
Error: error TS2307: Cannot find module '@agentrix/shared'
Solution: Install dependencies
npm install
Runtime Errors
Error: Hook failed: timeout
Solution: Ensure hooks complete within 30 seconds
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' };
}
Best Practices
- Type Safety: Import types from correct packages (
@anthropic-ai/claude-agent-sdkfor Claude SDK hook inputs,@agentrix/sharedforHookFactoryandAgentrixContext) - Error Handling: Wrap in try/catch, fail gracefully
- Performance: Keep hooks fast (< 1s typically)
- Logging: Use console.log for debugging
- Testing: Write unit tests for complex logic
- Documentation: Comment hook behavior and edge cases
Advanced Topics
Multi-File Organization
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';
External Dependencies
You can use npm packages in hooks:
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' };
}
Warning: External dependencies add to build size and startup time.
Next Steps
- Hook Types Reference - Complete type definitions
- Examples - Copy-paste patterns
- PreToolUse & PostToolUse - Tool control hooks
Related Documentation
- Hooks Overview - Conceptual overview
- Agent Structure - Hooks directory structure
- Testing Guide - Testing agents and hooks