Skip to contents

A hook is an R function that Deputy calls at a fixed point in a run: before a tool runs, after it returns, when the run stops, and so on. Use hooks to log what an agent does, keep an audit trail, block particular tool calls, or stop a run early.

Add a hook

Create a hook with HookMatcher(), naming the event and the callback, and add it to an agent:

library(deputy)

log_tool <- HookMatcher(
  event = "PostToolUse",
  callback = function(tool_name, tool_result, tool_error, context) {
    cli::cli_inform("{tool_name} finished")
    NULL
  }
)

agent <- Agent$new(
  chat = ellmer::chat("openai/gpt-6-luna"),
  tools = tools_file(),
  permissions = permissions_readonly()
)
agent$add_hook(log_tool)

For tool events, pattern is a regular expression that limits the hook to matching tool names. hook_matches() tells you whether a hook applies to a tool:

library(deputy)

watch_writes <- HookMatcher(
  event = "PreToolUse",
  pattern = "^(write_file|edit_file|multi_edit)$",
  callback = function(tool_name, tool_input, context) {
    cli::cli_inform("{tool_name}: {tool_input$path}")
    NULL
  }
)
hook_matches(watch_writes, "edit_file")
#> [1] TRUE
hook_matches(watch_writes, "read_file")
#> [1] FALSE

Events

Event Fires Callback arguments
PreToolUse before a tool runs tool_name, tool_input, context
PostToolUse after a tool returns tool_name, tool_result, tool_error, context
PostToolUseFailure after a tool fails tool_name, tool_result, tool_error, context
PermissionRequest when the permission policy denies a call tool_name, tool_input, permission_result, context
SessionStart at the start of each run context
UserPromptSubmit at the start of each run, with the task prompt, context
Stop at the end of each run reason, context
SessionEnd at the end of each run, after Stop reason, context
SubagentStart when a subagent starts a delegated task agent_name, task, context
SubagentStop when a subagent finishes agent_name, task, result, context
PreCompact before older turns are summarised turns_to_compact, turns_to_keep, context
PostCompact after a summary is installed result, context
Notification when Deputy reports something, such as a denied call message, context
ConfigChange when set_permission_mode() changes the mode key, old_value, new_value, context

context is a named list with the working directory, the agent and run identifiers, and fields specific to the event, such as the tool’s annotations for PreToolUse or the run’s usage for Stop. ?HookEvent lists them all.

Block a tool call

A PreToolUse hook runs after the permission policy has allowed a call, and can still deny it. Return HookResultPreToolUse():

no_secrets <- HookMatcher(
  event = "PreToolUse",
  pattern = "^write_file$",
  callback = function(tool_name, tool_input, context) {
    if (grepl("\\.env$|secrets", tool_input$path)) {
      return(HookResultPreToolUse("deny", reason = "Don't write to secret files."))
    }
    NULL
  }
)
agent$add_hook(no_secrets)

The model sees the reason and can try something else. A PreToolUse hook can only take permissions away: calls the policy denies never reach it.

Allow a denied call

A PermissionRequest hook runs only when the policy denies a call, and can allow the call anyway. Use it to ask a person before running something the policy would refuse:

ask_before_code <- HookMatcher(
  event = "PermissionRequest",
  pattern = "^run_r_code$",
  callback = function(tool_name, tool_input, permission_result, context) {
    cat(tool_input$code, sep = "\n")
    if (isTRUE(utils::askYesNo("Run this R code?", default = FALSE))) {
      return(PermissionResultAllow())
    }
    NULL
  }
)
agent$add_hook(ask_before_code)

Returning NULL keeps the denial, and PermissionResultDeny() replaces its reason. An allowed call runs with your R session’s access, so treat this hook as part of the permission policy.

Stop a run from a hook

PostToolUse hooks see each tool’s result or error. Return HookResultPostToolUse(continue = FALSE) to end the run after the current tool:

stop_on_error <- HookMatcher(
  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}")
      return(HookResultPostToolUse(continue = FALSE, stop_reason = "tool_failed"))
    }
    NULL
  }
)

HookResultPreToolUse() also takes continue and stop_reason. Both take additional_context, text that Deputy appends to the system prompt.

Built-in hooks

hook_log_tools() prints a line after each tool call saying whether it succeeded. verbose = TRUE adds a short preview of the result:

agent$add_hook(hook_log_tools(verbose = TRUE))

hook_block_dangerous_bash() denies shell commands that match patterns such as rm -rf, sudo and chmod 777. Add your own patterns with additional_patterns:

agent$add_hook(hook_block_dangerous_bash(
  additional_patterns = c("DROP\\s+TABLE", "TRUNCATE")
))

A pattern list catches accidents, not a determined adversary, so don’t rely on it to make run_bash safe.

hook_limit_file_writes(dir) denies writes and edits outside dir, applying the same path rules as Permissions(file_write = dir). Use it to add a second check, or to confine writes more tightly for one agent than its policy does.

Order and errors

Hooks for an event run in the order you added them, and the first hook that returns something other than NULL decides; later hooks for that event don’t run. So return NULL from hooks that only observe, and add hooks that can deny a call before hooks that might allow it. The built-in hooks return NULL unless they deny a call.

If a PreToolUse hook throws an error, Deputy denies the tool call. An error in any other hook is reported and the run carries on. agent$hooks$last_errors() lists recent hook errors.

Run a hook in a separate process

Hooks run in your R session by default, so they can read and update your objects. Give HookMatcher() a positive timeout (in seconds) to run the callback in a fresh R process that is killed if it takes too long. That process can’t see your session’s objects, so refer to package functions with ::, for example deputy::HookResultPreToolUse().

Hooks or results?

Hooks are for reacting while a run is in progress. To analyse a run afterwards, use the AgentResult instead: result_tool_calls(), result_tool_results() and result$events hold the same information, without any hook state to manage.

Human input and approvals shows hooks that ask a person to approve a tool call before it runs.