Skip to content

Agent Hooks - Tool Interception

Overview

Hooks allow you to intercept tool calls before and after execution. This example demonstrates using a BeforeToolUse hook to block dangerous bash commands - a practical security pattern for agentic applications.

Key concepts: - CallableHook: Wraps a closure as a hook - HookContext: Provides access to tool call and agent state - HookTriggers: Defines when the hook fires (e.g., beforeToolUse()) - addHook(): Registers a hook on the builder with priority - AgentConsoleLogger: Provides visibility into agent execution stages

Example

<?php
require 'examples/boot.php';

use Cognesy\Agents\AgentBuilder\AgentBuilder;
use Cognesy\Agents\AgentBuilder\Capabilities\Bash\UseBash;
use Cognesy\Agents\Core\Data\AgentState;
use Cognesy\Agents\Events\AgentConsoleLogger;
use Cognesy\Agents\Hooks\Collections\HookTriggers;
use Cognesy\Agents\Hooks\Data\HookContext;
use Cognesy\Agents\Hooks\Defaults\CallableHook;

// Create console logger for execution visibility
$logger = new AgentConsoleLogger(
    useColors: true,
    showTimestamps: true,
    showContinuation: true,
    showToolArgs: false,  // We'll show args in our custom hook output
);

// Dangerous patterns to block
$blockedPatterns = [
    'rm -rf',
    'rm -r /',
    'sudo rm',
    '> /dev/sda',
    'mkfs',
    'dd if=',
    ':(){:|:&};:',  // Fork bomb
];

// Build agent with bash capability and security hook
$agent = AgentBuilder::base()
    ->withCapability(new UseBash())
    ->addHook(
        hook: new CallableHook(function (HookContext $ctx) use ($blockedPatterns): HookContext {
            $toolCall = $ctx->toolCall();
            if ($toolCall === null) {
                return $ctx;
            }

            $command = $toolCall->args()['command'] ?? '';

            // Check for dangerous patterns
            foreach ($blockedPatterns as $pattern) {
                if (str_contains($command, $pattern)) {
                    echo "         [HOOK] BLOCKED - Dangerous pattern detected: {$pattern}\n";
                    return $ctx->withToolExecutionBlocked("Dangerous command: {$pattern}");
                }
            }

            echo "         [HOOK] ALLOWED - {$command}\n";
            return $ctx;
        }),
        triggers: HookTriggers::beforeToolUse(),
        priority: 100,       // High priority = runs first
    )
    ->build()
    ->wiretap($logger->wiretap());

// Test with safe commands
$state = AgentState::empty()->withUserMessage(
    'List the files in the current directory and show the date'
);

echo "=== Test 1: Safe Commands ===\n\n";
$finalState = $agent->execute($state);

echo "\n=== Result ===\n";
$response = $finalState->finalResponse()->toString() ?: 'No response';
echo "Answer: {$response}\n";
echo "Steps: {$finalState->stepCount()}\n";
echo "Status: {$finalState->status()->value}\n";

// Test with dangerous command (simulated prompt)
echo "\n=== Test 2: Dangerous Command Detection ===\n\n";
$state2 = AgentState::empty()->withUserMessage(
    'Delete all files with: rm -rf /'
);

$finalState2 = $agent->execute($state2);

echo "\n=== Result ===\n";
$hasErrors = $finalState2->currentStep()?->hasErrors() ?? false;
echo "Command was " . ($hasErrors ? "BLOCKED (security hook worked!)" : "executed") . "\n";
echo "Steps: {$finalState2->stepCount()}\n";
echo "Status: {$finalState2->status()->value}\n";
?>

How It Works

  1. Hook Registration: addHook() registers a CallableHook with HookTriggers::beforeToolUse()
  2. Context Access: HookContext provides toolCall() and state() accessors
  3. Priority: Higher priority (100) ensures this security check runs before other hooks
  4. Blocking: $ctx->withToolExecutionBlocked($reason) blocks the tool call with a reason
  5. Allowing: Returning $ctx unchanged allows execution to proceed

Other Hook Types

// After tool execution - for logging/metrics
->addHook(
    hook: new CallableHook(function (HookContext $ctx): HookContext {
        $exec = $ctx->toolExecution();
        if ($exec !== null) {
            echo "Tool {$exec->name()} completed\n";
        }
        return $ctx;
    }),
    triggers: HookTriggers::afterToolUse(),
)

// Before each step - modify state
->addHook(
    hook: new CallableHook(function (HookContext $ctx): HookContext {
        $state = $ctx->state()->withMetadata('step_started', microtime(true));
        return $ctx->withState($state);
    }),
    triggers: HookTriggers::beforeStep(),
)

// After each step
->addHook(
    hook: new CallableHook(function (HookContext $ctx): HookContext {
        $started = $ctx->state()->metadata()->get('step_started');
        if ($started !== null) {
            $duration = microtime(true) - $started;
            echo "Step took {$duration}s\n";
        }
        return $ctx;
    }),
    triggers: HookTriggers::afterStep(),
)