# SDK Hook System

> **Version Requirements**: This documentation is for CodeBuddy Agent SDK v0.1.0 and above.

This document explains how to use the Hook system in the SDK to insert custom logic before and after tool execution.

## Table of Contents

- [Overview](#overview)
- [Hook Configuration](#hook-configuration)
- [Event Types](#event-types)
- [Hook Input](#hook-input)
- [Hook Output](#hook-output)
- [Examples](#examples)
- [Related Documentation](#related-documentation)

## Overview

Hooks allow you to insert custom logic during CodeBuddy's session lifecycle, enabling:

- Validation and interception before tool calls
- Logging after tool execution
- Review of user-submitted content
- Initialization and cleanup at session start/end
- Custom workflows for `worktree` creation and cleanup

### Supported Events

| Event | Trigger Timing |
|-------|----------------|
| `PreToolUse` | Before tool execution |
| `PostToolUse` | After successful tool execution |
| `UserPromptSubmit` | When user submits a message |
| `Stop` | When main Agent response ends |
| `SubagentStop` | When sub-agent ends |
| `PreCompact` | Before context compression |
| `WorktreeCreate` | When creating an isolated `worktree` |
| `WorktreeRemove` | When deleting an isolated `worktree` |
| `unstable_Checkpoint` | When a checkpoint is automatically created after file modifications |

## Hook Configuration

Configure hooks through the `hooks` option. Each event can have multiple matchers, and each matcher can have multiple hook callbacks.

### Basic Structure

::: code-group

```typescript [TypeScript]
import { query } from '@tencent-ai/agent-sdk';

const q = query({
  prompt: 'Help me analyze the code',
  options: {
    model: 'deepseek-v3.1',
    hooks: {
      PreToolUse: [
        {
          matcher: 'Bash',  // Only match Bash tool
          hooks: [
            async (input, toolUseId, ctx) => {
              console.log('About to execute:', input);
              return { continue: true };
            }
          ],
          timeout: 5000  // Timeout in milliseconds
        }
      ]
    }
  }
});
```

```python [Python]
from codebuddy_agent_sdk import query, CodeBuddyAgentOptions, HookMatcher

async def pre_tool_hook(input_data, tool_use_id, context):
    print(f"About to execute: {input_data}")
    return {"continue_": True}

options = CodeBuddyAgentOptions(
    model="deepseek-v3.1",
    hooks={
        "PreToolUse": [
            HookMatcher(
                matcher="Bash",  # Only match Bash tool
                hooks=[pre_tool_hook],
                timeout=5.0  # Timeout in seconds
            )
        ]
    }
)

async for msg in query(prompt="Help me analyze the code", options=options):
    print(msg)
```

:::

### HookMatcher Structure

| Field | Type | Description |
|-------|------|-------------|
| `matcher` | `string` | Match pattern, supports regex. `*` or empty string matches all |
| `hooks` | `HookCallback[]` | Array of callback functions |
| `timeout` | `number` | Timeout (milliseconds in TypeScript, seconds in Python) |

### Matcher Patterns

- **Exact match**: `"Bash"` only matches Bash tool
- **Regex match**: `"Edit|Write"` matches Edit or Write
- **Wildcard**: `"*"` or `""` matches all tools
- **Prefix match**: `"mcp__.*"` matches all MCP tools

## Event Types

### PreToolUse

Triggered before tool execution, can block execution or modify input.

::: code-group

```typescript [TypeScript]
hooks: {
  PreToolUse: [{
    matcher: 'Bash',
    hooks: [
      async (input, toolUseId, ctx) => {
        const command = input.command as string;

        // Block dangerous commands
        if (command.includes('rm -rf')) {
          return {
            decision: 'block',
            reason: 'Dangerous command blocked'
          };
        }

        return { continue: true };
      }
    ]
  }]
}
```

```python [Python]
async def pre_bash_hook(input_data, tool_use_id, context):
    command = input_data.get("command", "")

    # Block dangerous commands
    if "rm -rf" in command:
        return {
            "decision": "block",
            "reason": "Dangerous command blocked"
        }

    return {"continue_": True}

hooks = {
    "PreToolUse": [
        HookMatcher(matcher="Bash", hooks=[pre_bash_hook])
    ]
}
```

:::

### PostToolUse

Triggered after successful tool execution, can add extra context.

::: code-group

```typescript [TypeScript]
hooks: {
  PostToolUse: [{
    matcher: 'Write|Edit',
    hooks: [
      async (input, toolUseId) => {
        console.log(`File modified: ${input.file_path}`);
        // Log file changes
        await logFileChange(input.file_path);
        return { continue: true };
      }
    ]
  }]
}
```

```python [Python]
async def post_write_hook(input_data, tool_use_id, context):
    print(f"File modified: {input_data.get('file_path')}")
    # Log file changes
    await log_file_change(input_data.get("file_path"))
    return {"continue_": True}

hooks = {
    "PostToolUse": [
        HookMatcher(matcher="Write|Edit", hooks=[post_write_hook])
    ]
}
```

:::

### UserPromptSubmit

Triggered when user submits a message, can add context or block processing.

::: code-group

```typescript [TypeScript]
hooks: {
  UserPromptSubmit: [{
    hooks: [
      async (input) => {
        const prompt = input.prompt as string;

        // Sensitive word check
        if (containsSensitiveWords(prompt)) {
          return {
            decision: 'block',
            reason: 'Message contains sensitive content'
          };
        }

        return { continue: true };
      }
    ]
  }]
}
```

```python [Python]
async def prompt_check_hook(input_data, tool_use_id, context):
    prompt = input_data.get("prompt", "")

    # Sensitive word check
    if contains_sensitive_words(prompt):
        return {
            "decision": "block",
            "reason": "Message contains sensitive content"
        }

    return {"continue_": True}

hooks = {
    "UserPromptSubmit": [
        HookMatcher(hooks=[prompt_check_hook])
    ]
}
```

:::

### Stop / SubagentStop

Triggered when Agent response ends, can block stopping and request continuation.

::: code-group

```typescript [TypeScript]
hooks: {
  Stop: [{
    hooks: [
      async (input) => {
        // Check if task is truly complete
        if (!isTaskComplete()) {
          return {
            decision: 'block',
            reason: 'Task incomplete, please continue'
          };
        }
        return { continue: true };
      }
    ]
  }]
}
```

```python [Python]
async def stop_hook(input_data, tool_use_id, context):
    # Check if task is truly complete
    if not is_task_complete():
        return {
            "decision": "block",
            "reason": "Task incomplete, please continue"
        }
    return {"continue_": True}

hooks = {
    "Stop": [HookMatcher(hooks=[stop_hook])]
}
```

:::

### unstable_Checkpoint (Experimental)

Automatically triggered after file modifications (when Write/Edit/MultiEdit tools execute successfully), providing file snapshots and change statistics.

::: warning Experimental API
This Hook is an experimental feature. The API may change in future versions.
:::

::: code-group

```typescript [TypeScript]
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('File change checkpoint:', {
          id: checkpoint.id,
          label: checkpoint.label,
          files: checkpoint.fileChangeStats?.files,
          additions: checkpoint.fileChangeStats?.additions,
          deletions: checkpoint.fileChangeStats?.deletions
        });
        
        // Access file snapshots
        for (const [filePath, version] of Object.entries(checkpoint.fileSnapshots)) {
          console.log(`  ${filePath} - version ${version.version}`);
        }
        
        return { continue: true };
      }
    ]
  }]
}
```

```python [Python]
async def checkpoint_hook(input_data, tool_use_id, context):
    checkpoint = input_data.get("checkpoint", {})
    
    file_change_stats = checkpoint.get("fileChangeStats", {})
    print(f"File change checkpoint:")
    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")
    
    # Access file snapshots
    for file_path, version in checkpoint.get("fileSnapshots", {}).items():
        print(f"  {file_path} - version {version.get('version')}")
    
    return {"continue_": True}

hooks = {
    "unstable_Checkpoint": [HookMatcher(hooks=[checkpoint_hook])]
}
```

:::

**Checkpoint Data Structure**:

- `id`: Unique checkpoint identifier
- `label`: Human-readable label (typically the user prompt)
- `createdAt`: Creation timestamp
- `fileSnapshots`: Mapping of file paths to version information
  - `filePath`: Absolute file path
  - `version`: Version number
  - `backupFileName`: Backup file name
  - `backupTime`: Backup timestamp
- `fileChangeStats`: File change statistics
  - `files`: List of changed file paths
  - `additions`: Number of lines added
  - `deletions`: Number of lines deleted

## Hook Input

The input structure received by hook callbacks varies by event type.

### Common Fields

```json
{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse"
}
```

### PreToolUse / PostToolUse Input

```json
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "ls -la"
  }
}
```

### UserPromptSubmit Input

```json
{
  "prompt": "Help me write a function"
}
```

### Stop / SubagentStop Input

```json
{
  "stop_hook_active": false
}
```

**WorktreeCreate Input Example**:

```json
{
  "hook_event_name": "WorktreeCreate",
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "transcript_path": "/path/to/transcript.jsonl",
  "name": "feature-auth"
}
```

**WorktreeRemove Input Example**:

```json
{
  "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 Input

```json
{
  "checkpoint": {
    "id": "ckpt_abc123",
    "label": "Help me write a function",
    "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 Output

The output returned by hook callbacks controls subsequent behavior.

### Basic Output Fields

| Field | Type | Description |
|-------|------|-------------|
| `continue` / `continue_` | `boolean` | Whether to continue execution (default true) |
| `decision` | `'block'` | Set to `'block'` to prevent operation |
| `reason` | `string` | Reason for blocking |
| `stopReason` | `string` | Stop message displayed when `continue` is false |
| `suppressOutput` | `boolean` | Hide output |

### PreToolUse Special Output

Can modify tool input:

::: code-group

```typescript [TypeScript]
return {
  continue: true,
  hookSpecificOutput: {
    hookEventName: 'PreToolUse',
    updatedInput: {
      command: `echo "Security check passed" && ${input.command}`
    }
  }
};
```

```python [Python]
return {
    "continue_": True,
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "updatedInput": {
            "command": f'echo "Security check passed" && {input_data["command"]}'
        }
    }
}
```

:::

## Examples

### Complete Example: Bash Command Auditing

::: code-group

```typescript [TypeScript]
import { query } from '@tencent-ai/agent-sdk';
import * as fs from 'fs';

const logFile = '/tmp/bash-audit.log';

const q = query({
  prompt: 'Help me clean temporary files',
  options: {
    model: 'deepseek-v3.1',
    hooks: {
      PreToolUse: [{
        matcher: 'Bash',
        hooks: [
          async (input, toolUseId) => {
            const command = input.command as string;
            const timestamp = new Date().toISOString();

            // Log command
            fs.appendFileSync(logFile, `${timestamp} [PRE] ${command}\n`);

            // Dangerous command check
            const dangerous = ['rm -rf /', 'mkfs', ':(){:|:&};:'];
            for (const d of dangerous) {
              if (command.includes(d)) {
                return {
                  decision: 'block',
                  reason: `Dangerous command blocked: ${d}`
                };
              }
            }

            return { continue: true };
          }
        ]
      }],
      PostToolUse: [{
        matcher: 'Bash',
        hooks: [
          async (input, toolUseId) => {
            const command = input.command as string;
            const timestamp = new Date().toISOString();

            // Log completion
            fs.appendFileSync(logFile, `${timestamp} [POST] ${command} - Complete\n`);

            return { continue: true };
          }
        ]
      }]
    }
  }
});

for await (const message of q) {
  console.log(message);
}
```

```python [Python]
import asyncio
from datetime import datetime
from codebuddy_agent_sdk import query, CodeBuddyAgentOptions, HookMatcher

log_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()

    # Log command
    with open(log_file, "a") as f:
        f.write(f"{timestamp} [PRE] {command}\n")

    # Dangerous command check
    dangerous = ["rm -rf /", "mkfs", ":(){:|:&};:"]
    for d in dangerous:
        if d in command:
            return {
                "decision": "block",
                "reason": f"Dangerous command blocked: {d}"
            }

    return {"continue_": True}

async def post_bash_hook(input_data, tool_use_id, context):
    command = input_data.get("command", "")
    timestamp = datetime.now().isoformat()

    # Log completion
    with open(log_file, "a") as f:
        f.write(f"{timestamp} [POST] {command} - Complete\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="Help me clean temporary files", options=options):
        print(message)

asyncio.run(main())
```

:::

### Example: Limiting File Modification Scope

::: code-group

```typescript [TypeScript]
hooks: {
  PreToolUse: [{
    matcher: 'Write|Edit',
    hooks: [
      async (input) => {
        const filePath = input.file_path as string;

        // Only allow modifying src directory
        if (!filePath.startsWith('/path/to/project/src/')) {
          return {
            decision: 'block',
            reason: `Not allowed to modify files outside src directory: ${filePath}`
          };
        }

        // Prohibit modifying config files
        if (filePath.endsWith('.env') || filePath.includes('.git/')) {
          return {
            decision: 'block',
            reason: 'Not allowed to modify sensitive files'
          };
        }

        return { continue: true };
      }
    ]
  }]
}
```

```python [Python]
async def file_scope_hook(input_data, tool_use_id, context):
    file_path = input_data.get("file_path", "")

    # Only allow modifying src directory
    if not file_path.startswith("/path/to/project/src/"):
        return {
            "decision": "block",
            "reason": f"Not allowed to modify files outside src directory: {file_path}"
        }

    # Prohibit modifying config files
    if file_path.endswith(".env") or ".git/" in file_path:
        return {
            "decision": "block",
            "reason": "Not allowed to modify sensitive files"
        }

    return {"continue_": True}

hooks = {
    "PreToolUse": [
        HookMatcher(matcher="Write|Edit", hooks=[file_scope_hook])
    ]
}
```

:::

### Example: File Change Tracking (Checkpoint Hook)

::: code-group

```typescript [TypeScript]
import { query, type CheckpointHookInput } from '@tencent-ai/agent-sdk';
import * as fs from 'fs';

const changeLog = '/tmp/file-changes.log';

const q = query({
  prompt: 'Refactor 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 };
            
            // Log file changes
            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);
            
            // Warn if changes are too large
            if (stats.additions + stats.deletions > 100) {
              console.warn('⚠️  Large amount of code changes, review recommended');
            }
            
            return { continue: true };
          }
        ]
      }]
    }
  }
});

for await (const message of q) {
  console.log(message);
}
```

```python [Python]
import asyncio
from datetime import datetime
from codebuddy_agent_sdk import query, CodeBuddyAgentOptions, HookMatcher

change_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}
    
    # Log file changes
    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)
    
    # Warn if changes are too large
    total_changes = stats.get("additions", 0) + stats.get("deletions", 0)
    if total_changes > 100:
        print("⚠️  Large amount of code changes, review recommended")
    
    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="Refactor src/utils.ts", options=options):
        print(message)

asyncio.run(main())
```

:::

## Related Documentation

- [SDK Overview](sdk.md) - Quick start and usage examples
- [SDK Permission Control](sdk-permissions.md) - canUseTool callback
- [Hook Reference Guide](hooks.md) - Complete CLI Hook reference
- [TypeScript SDK Reference](sdk-typescript.md) - Complete API reference
- [Python SDK Reference](sdk-python.md) - Complete API reference
