CrewAI tool call hooks: how to block a tool call or ask a person first

CrewAI runs hooks before and after every tool call. A before hook can stop the call, change its input or ask a person in the terminal. Here's how each works, and three traps from the docs.

The short answer

Register a PRE_TOOL_CALL hook. It runs before every tool execution, sees the tool’s name and a mutable copy of its input, and can stop the call by raising HookAborted(reason, source) (or, with the older @before_tool_call decorator, by returning False).1 To ask a person, call ctx.request_human_input(...) inside the hook and abort unless they agree.1

Two cautions before you rely on it: a blocked call doesn’t stop the crew, it tells the agent “Tool execution blocked by hook” and the run continues; and edits to the input only count if you change the dictionary in place.11

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

Where the hooks run

CrewAI has two interception points: PRE_TOOL_CALL, before every tool execution, and POST_TOOL_CALL, after it.1

Decorator Status Blocks by
@on(InterceptionPoint.PRE_TOOL_CALL) Preferred Raising HookAborted(reason, source)
@before_tool_call Legacy, still works, same chain Returning False
@on(InterceptionPoint.POST_TOOL_CALL) / @after_tool_call After the call Can’t block; sees the result

The legacy registration functions (register_before_tool_call_hook and friends) still exist alongside newer get_hooks, clear_hooks and unregister_hook.1

What a hook receives

A ToolCallHookContext with:1

Field What it is
tool_name The tool being called
tool_input Its input parameters, mutable
tool The tool instance
agent The agent calling it
task The current task
crew The crew
tool_result, raw_tool_result Only in POST_TOOL_CALL

Having agent and task is useful: the same refund tool might be fine for a billing agent on a refund task and not for a research agent.

Three things a before hook can do

1. Block. A sketch, using the names from CrewAI’s docs (we didn’t run it against a live crew):

Code Program code, for developers. The text around it says what it does.

from crewai.hooks import on, InterceptionPoint, HookAborted

BLOCKED = ("DROP ", "TRUNCATE ", "DELETE FROM")

@on(InterceptionPoint.PRE_TOOL_CALL)
def no_destructive_sql(ctx):
    if ctx.tool_name == "run_sql":
        query = str(ctx.tool_input.get("query", "")).upper()
        if any(word in query for word in BLOCKED):
            raise HookAborted("Destructive SQL needs a person", "sql-guard")

Check the import path against your installed version before use.

2. Change the input. Add a limit, force a dry run, redact a field. The docs are explicit that you must modify tool_input in place, for example ctx.tool_input['query'] = ....1

3. Ask a person. ctx.request_human_input(prompt, default_message) pauses live console output and reads terminal input; the docs’ approval-gate example aborts unless the answer is “yes.”1

Code Program code, for developers. The text around it says what it does.

@on(InterceptionPoint.PRE_TOOL_CALL)
def approve_refunds(ctx):
    if ctx.tool_name == "refund" and ctx.tool_input.get("amount", 0) > 100:
        answer = ctx.request_human_input(f"Approve refund {ctx.tool_input}? (yes/no)", "no")
        if str(answer).strip().lower() != "yes":
            raise HookAborted("Refund not approved", "refund-approval")

Terminal input works for a developer at the keyboard. For a crew running on a server, the “person” needs to be reached another way, such as a queue or a chat approval; the hook would wait on that instead.

Three traps

Trap 1: blocked doesn’t mean stopped. The agent receives “Tool execution blocked by hook. Tool: ” and the run continues; POST_TOOL_CALL hooks still fire on blocked calls.1 A capable agent may try another tool that does the same thing. If your crew has both run_sql and a generic python tool, blocking one leaves the other. Put a clear reason in HookAborted and give the agent a legitimate next step (“ask the user”), or it will improvise.

Trap 2: reassigning tool_input is silently ignored. ctx.tool_input = {...} replaces the reference, so the tool never sees the change.1 A redaction hook written that way looks right in review and does nothing. Edit keys in place, and test that the tool receives the edited value.

Trap 3: False from the wrong decorator. Returning False blocks only in the legacy @before_tool_call style; @on hooks block by raising HookAborted.1 Mixing the two styles in one codebase is how a guard ends up returning False from an @on hook and blocking nothing. Pick one style and test a blocked call.

Test the hook, not just the code

A guard that never fires looks identical to one that works, until the day it matters. Before trusting a hook, write three small tests that call your hook function directly with a fake context object:

  1. A call it should block (DROP TABLE users through run_sql) raises HookAborted.
  2. A call it should allow (SELECT 1) returns without raising and leaves tool_input unchanged.
  3. A call it should edit (a query with no LIMIT) ends with the edited value in the same dictionary object the test passed in.

The third test is the one that catches trap 2. Run all three in CI, so a refactor that switches decorator styles can’t quietly turn a guard off.

Global or per crew

A plain @on hook applies across all crews, narrowed with tools= and agents= filters; the same decorator on a method inside a @CrewBase class applies only to that crew.1

  • Global for rules that must always hold: no destructive SQL, no shell outside the project.
  • Per crew for business rules: the billing crew’s refund threshold.

Hooks live in your Python process. A tool called by another program, or a crew built without importing your hooks module, skips them. For rules that hold for every client, an outside check in front of the tools does the same job; Deedglass’s docs include a CrewAI recipe that routes a crew’s tools through its MCP server, one of nine framework recipes of 15 lines or fewer, with no live model run behind them.2 The tool-call policy entry explains what such rules look like.

A checklist

  • One hook style across the codebase (@on with HookAborted preferred).
  • Destructive tools blocked globally; business thresholds per crew.
  • Every block carries a reason and a suggested next step.
  • Input edits done in place, with a test that the tool sees them.
  • Generic tools (python, shell) removed if a block on a specific tool is meant to hold.
  • Human approval reaches a person when the crew runs unattended.
  • Blocked and approved calls logged with agent, task and input.

For the same pattern in other frameworks, see LangGraph interrupt vs middleware and Vercel AI SDK toolApproval; human-in-the-loop approval covers the idea. To see what the agents configured on your machine can change, paste the config into the check page or run npx deedglass scan (how the grade works); to try rules in your browser, open the playground.

Written with AI assistance and checked against our claims register.

The takeaway

A CrewAI PRE_TOOL_CALL hook can block a call with HookAborted, but the agent carries on, and reassigning tool_input does nothing. Edit in place.

Read nextLangGraph interrupt() or HumanInTheLoopMiddleware: which to use for tool approval Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1CrewAI docs: Tool call hooks · docs.crewai.com · checked 26 October 2026
  2. 2Deedglass docs: CrewAI · docs.deedglass.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing