Overview
Tool hooks are executed at two interception points:
Write them with the
@on decorator. The
legacy @before_tool_call / @after_tool_call decorators
keep working unchanged — both styles register on the same engine and run in one
ordered chain.
Hook Signature
ToolCallHookContext directly as the hook argument (there is no separate
ctx.payload): mutate ctx.tool_input in place before the call, and return a
string to replace the result after it.
When a call is blocked, the tool does not run and the agent receives
"Tool execution blocked by hook. Tool: <name>" as the result — the run
continues. POST_TOOL_CALL hooks still fire on blocked calls, so monitoring
hooks see every attempt.
Tool Hook Context
TheToolCallHookContext object provides comprehensive access to tool
execution state:
tool_result is the string the agent sees. By default,
this is JSON. If the tool uses custom formatting, it can be Markdown or another
string. Use raw_tool_result when your hook needs the typed object or
dictionary; it is not affected by result replacement.
The context also exposes request_human_input(prompt, default_message), which
pauses live console updates and collects input from the terminal — useful for
approval gates.
Modifying Tool Inputs
Important: Always modify tool inputs in-place:Registration Methods
1. Global Hooks
Apply to all tool calls across all crews. Usetools= / agents= filters to
scope a hook:
2. Crew-Scoped Hooks
Apply the same decorator to a method inside a@CrewBase class to scope the
hook to that crew only:
Common Use Cases
1. Safety Guardrails
2. Human Approval Gate
3. Input Validation and Sanitization
4. Result Sanitization
5. Tool Usage Analytics
6. Rate Limiting
Hook Management
register_before_tool_call_hook,
unregister_before_tool_call_hook, clear_before_tool_call_hooks,
clear_all_tool_call_hooks, get_before_tool_call_hooks, and their after_
counterparts) operates on the same underlying registries, so either API can
manage hooks registered by the other.
Legacy Decorators
The original per-point decorators keep working unchanged and run in the same registration-order chain as@on hooks:
@on:
- Blocking is
return Falsefrom a before hook — equivalent to raisingHookAborted, but without a custom reason or source for telemetry. The agent sees the same"Tool execution blocked by hook"message. - Signatures are point-specific: before hooks return
bool | None, after hooks returnstr | None. The context object is the sameToolCallHookContext. - Filters and crew-scoping work the same way:
@before_tool_call(tools=[...], agents=[...]), and applying the decorator to a@CrewBasemethod scopes it to that crew.
@on for new code; keep the legacy style where it is already in use —
there is no behavioral penalty.
Best Practices
- Keep hooks focused and fast — they run on every tool call
- Modify in-place — always mutate
ctx.tool_input, never replace the dict - Prefer filters over conditionals —
tools=/agents=keep hook bodies small - Abort loudly — raise
HookAbortedwith a meaningful reason and source; any other exception is swallowed (fail-open) - Use type hints — annotate with
ToolCallHookContextfor IDE support - Clear hooks in tests — call
clear_all_hooks()between test runs
Troubleshooting
Hook Not Executing
- Verify the hook is registered before crew execution
- Check whether an earlier hook blocked the call (subsequent pre hooks don’t run)
- Check
tools=/agents=filters against the actual tool name and agent role
Input Modifications Not Working
- Use in-place modifications:
ctx.tool_input['key'] = value - Don’t replace the dict:
ctx.tool_input = {}
Result Modifications Not Working
- Return the modified string from a
POST_TOOL_CALLhook - Returning
Nonekeeps the original result
Tool Blocked Unexpectedly
- Check all pre hooks for
HookAborted/return Falseconditions - The abort reason and source appear on the
HookDispatchedEventtelemetry
