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¶
- Hook Registration:
addHook()registers aCallableHookwithHookTriggers::beforeToolUse() - Context Access:
HookContextprovidestoolCall()andstate()accessors - Priority: Higher priority (100) ensures this security check runs before other hooks
- Blocking:
$ctx->withToolExecutionBlocked($reason)blocks the tool call with a reason - Allowing: Returning
$ctxunchanged 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(),
)