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] FALSEEvents
| 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.