Skip to contents

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

Error Handling in Hooks

If a PreToolUse hook throws an error, Deputy fails closed and denies the tool call. Other hook errors are logged and the run continues. You can inspect recent hook errors with:

agent$hooks$last_errors()