Hooks let you intercept agent behaviour at key points in the execution lifecycle. Use them for logging, auditing, blocking dangerous actions, or injecting custom logic.
Hook Events
deputy fires hooks at these events:
| Event | Callback Signature | Purpose |
|---|---|---|
PreToolUse |
function(tool_name, tool_input, context) |
Before a tool runs (can allow/deny) |
PostToolUse |
function(tool_name, tool_result, tool_error, context) |
After a tool completes |
Notification |
function(message, context) |
Informational runtime notices |
Stop |
function(reason, context) |
When the agent finishes |
SubagentStop |
function(agent_name, task, result, context) |
When a sub-agent finishes |
UserPromptSubmit |
function(prompt, context) |
When user submits input |
PreCompact |
function(turns_to_compact, turns_to_keep, context) |
Before conversation compaction |
SessionStart |
function(context) |
When a session starts |
SessionEnd |
function(reason, context) |
When a session ends |
Creating Hooks
Hooks are created with HookMatcher$new():
library(deputy)
hook <- HookMatcher$new(
event = "PostToolUse",
callback = function(tool_name, tool_result, tool_error, context) {
cli::cli_alert_info("Tool {tool_name} completed")
HookResultPostToolUse()
}
)Add hooks to an agent with add_hook():
agent$add_hook(hook)Hooks run in the caller’s R process by default, so callbacks can use
ordinary R state and side effects. Set a positive timeout
only when you deliberately want a clean callr subprocess
with a hard deadline. An isolated callback cannot rely on caller-process
state; qualify package functions such as
deputy::HookResultPostToolUse() when using that mode.
Filtering by Tool Name
The optional pattern argument is a regex that filters
which tools the hook applies to:
# Only fires for bash commands
HookMatcher$new(
event = "PreToolUse",
pattern = "^run_bash$",
callback = function(tool_name, tool_input, context) {
cli::cli_alert_warning("Bash command: {tool_input$command}")
HookResultPreToolUse(permission = "allow")
}
)Pre-Built Hooks
deputy includes several ready-made hooks:
Logging Tool Calls
hook_log_tools() logs every tool call using cli:
library(deputy)
chat <- ellmer::chat_openai(model = "gpt-4o-mini")
agent <- Agent$new(
chat = chat,
tools = tools_file(),
permissions = permissions_readonly()
)
agent$add_hook(hook_log_tools(verbose = TRUE))
result <- agent$run_sync("What files are in the current directory?")Blocking Dangerous Bash Commands
hook_block_dangerous_bash() blocks patterns like
rm -rf, sudo, chmod 777, and
more:
agent$add_hook(hook_block_dangerous_bash())
# Optionally add your own patterns
agent$add_hook(hook_block_dangerous_bash(
additional_patterns = c("DROP\\s+TABLE", "TRUNCATE")
))Limiting File Writes
Configure the Agent’s Permissions when a directory is an
authority boundary. hook_limit_file_writes() is a
defense-in-depth convenience hook that delegates to the same canonical
path policy for write_file, edit_file, and
multi_edit. The directory must already exist:
output_dir <- file.path(getwd(), "output")
dir.create(output_dir, showWarnings = FALSE)
agent$add_hook(hook_limit_file_writes(output_dir))Custom PreToolUse Hooks
PreToolUse hooks can allow or deny tool calls. Return
HookResultPreToolUse() with
permission = "allow" or "deny":
hook_no_secrets <- HookMatcher$new(
event = "PreToolUse",
pattern = "^write_file$",
callback = function(tool_name, tool_input, context) {
path <- tool_input$path
if (is.null(path)) {
path <- ""
}
if (grepl("\\.env$|secrets", path)) {
HookResultPreToolUse(
permission = "deny",
reason = "Cannot write to secret files"
)
} else {
HookResultPreToolUse(permission = "allow")
}
}
)Custom PostToolUse Hooks
PostToolUse hooks run after a tool completes. Use them
for logging, metrics, or conditional stopping:
hook_audit <- HookMatcher$new(
event = "PostToolUse",
callback = function(tool_name, tool_result, tool_error, context) {
if (!is.null(tool_error)) {
cli::cli_alert_danger("{tool_name} failed: {tool_error}")
} else {
cli::cli_alert_success("{tool_name} completed")
}
HookResultPostToolUse()
}
)
chat <- ellmer::chat_openai(model = "gpt-4o-mini")
agent <- Agent$new(
chat = chat,
tools = tools_file(),
permissions = permissions_readonly()
)
agent$add_hook(hook_audit)
result <- agent$run_sync("What files are in the current directory?")
# For post-hoc analysis, use AgentResult instead of hook state:
result$tool_calls()Set continue = FALSE to stop the agent after a tool
call:
HookResultPostToolUse(continue = FALSE)Session Lifecycle Hooks
Session hooks fire at the start and end of a session:
HookMatcher$new(
event = "SessionStart",
callback = function(context) {
message("Session started at ", Sys.time())
NULL
}
)Notification Hooks
Notification hooks are useful for informational events
that should not alter control flow, such as permission-denied guidance,
session-load notices, or compaction fallbacks:
agent$add_hook(HookMatcher$new(
event = "Notification",
callback = function(message, context) {
cli::cli_alert_info("[{context$code}] {message}")
NULL
}
))