Skip to content

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

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

Supported Events

EventTrigger Timing
PreToolUseBefore tool execution
PostToolUseAfter successful tool execution
UserPromptSubmitWhen user submits a message
StopWhen main Agent response ends
SubagentStopWhen sub-agent ends
PreCompactBefore context compression

Hook Configuration

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

Basic Structure

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
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

FieldTypeDescription
matcherstringMatch pattern, supports regex. * or empty string matches all
hooksHookCallback[]Array of callback functions
timeoutnumberTimeout (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.

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
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.

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
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.

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
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.

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
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])]
}

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
}

Hook Output

The output returned by hook callbacks controls subsequent behavior.

Basic Output Fields

FieldTypeDescription
continue / continue_booleanWhether to continue execution (default true)
decision'block'Set to 'block' to prevent operation
reasonstringReason for blocking
stopReasonstringStop message displayed when continue is false
suppressOutputbooleanHide output

PreToolUse Special Output

Can modify tool input:

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

Examples

Complete Example: Bash Command Auditing

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
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

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
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])
    ]
}