Google ADK before_tool_callback: tool-call guardrails, and when a plugin is better

In Google's Agent Development Kit, a before_tool_callback runs after the model asks for a tool and before the tool runs. Return None and it runs; return a dict and it doesn't. Plugins do the same for every agent.

The short answer

before_tool_callback is called just before a tool’s run_async, after the model has generated a function call for it. It receives tool, args and tool_context, and can inspect or change the arguments and run authorization checks. Return None and the tool runs, with the possibly modified arguments. Return a dictionary and the tool is skipped; the dictionary becomes the tool call’s result.1

For guardrails that must apply to every agent, ADK’s docs recommend plugins over callbacks: a plugin registered once applies to every agent, tool and model call managed by that runner, and its callbacks run before, and can override, the agent-level ones.2

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.

When the callback runs

Callback Called Arguments
before_tool_callback After the model asks for the tool, before run_async tool, args, tool_context
after_tool_callback After run_async completes successfully tool, args, tool_context, tool_response

Both are fields on LlmAgent.1 Plugins add an on_tool_error hook as well.2

What the return value means

From the docs:1

Callback returns Effect
before: None The tool runs, with the (possibly modified) args
before: a dictionary The tool is skipped; the dictionary is used as the result of the tool call
after: None The original tool_response is used
after: a new dictionary It replaces the original tool_response

The docs add a Python-specific note to the first row: “only None lets the tool run.”1 That makes the dictionary return a block and a reply at once. The model doesn’t see an exception; it sees a tool result. So write that result for the model: say what happened and what to do instead.

A guardrail callback

A sketch for a database tool, using the documented signature (check names against your installed ADK; we didn’t run it against a live model):

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

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

def sql_guard(tool, args, tool_context):
    if tool.name != "run_sql":
        return None
    query = str(args.get("query", "")).upper()
    if any(word in query for word in BLOCKED):
        return {
            "status": "blocked",
            "reason": "Destructive SQL needs a person. Tell the user what you wanted to run and why.",
        }
    if "LIMIT" not in query and query.lstrip().startswith("SELECT"):
        args["query"] = args["query"].rstrip("; ") + " LIMIT 100"
    return None

agent = LlmAgent(
    name="analyst",
    model="gemini-2.5-flash",
    tools=[run_sql],
    before_tool_callback=sql_guard,
)

Three choices in it:

  • Return None for every tool you don’t handle. Anything else skips the tool, in Python.1
  • The block result tells the model what to do next. Without that, models tend to retry with different spelling.
  • Edits happen on args. The tool runs with the modified arguments.1

For an approval rather than a block, the callback can return a result that says “waiting for approval” and record the request; the tool runs later, when a person agrees, through your own flow. The callback runs inline, while the tool call waits, so a long wait for a person is better handled outside it.

Testing a callback without a model

Because a callback is a plain function, you can test it without ADK running at all. Build a small stand-in for tool with a name attribute, pass a dictionary as args and None as tool_context, and assert on what comes back:

Input Expected return Expected args afterwards
run_sql, DROP TABLE users A dict with status: blocked Unchanged
run_sql, SELECT * FROM orders None Query ends with LIMIT 100
send_email, anything None Unchanged

The third row is the one that catches the empty-dict trap: a callback that accidentally returns {} for tools it doesn’t handle fails that test immediately, instead of silently blocking email in production.

Callbacks or plugins

Agent callback Plugin
Registered on One LlmAgent The runner (an App in Python), once2
Applies to That agent’s tools Every agent, tool and model call the runner manages2
Order After plugin callbacks First; if it returns a value, the agent-level callback is skipped2
Good for Agent-specific logic Security guardrails, logging, policy that must hold everywhere

ADK’s docs recommend plugins for security guardrails and describe prebuilt ones, including a Model Armor plugin that screens input and output, and a Tool Call Integrity plugin that signs function calls stored in the session and runs a tool only when its signature verifies.2 In Python, adk web and adk api_server can load a plugin with --extra_plugins.2

The practical rule: a sub-agent added next month by someone else won’t have your agent-level callback. It will be managed by the same runner, so it will have your plugin.

Traps

  • An empty dict isn’t “allow.” In Python, only None lets the tool run.1 A helper that returns {} by default blocks every tool.
  • Lists of callbacks chain on falsy values. When you pass a list, ADK moves to the next callback when one returns a falsy value such as None or an empty dict.1 Test the combination, not each callback alone.
  • The model sees your block message. It’s a tool result. Don’t put internal details in it.
  • Callbacks are in-process. Another program calling the same tools skips them. For rules that hold for every client, put a check in front of the tools; Deedglass’s docs include an ADK recipe that does this through its MCP server, one of nine framework recipes, with no live model run behind them.3

A checklist

  • Guardrails that must always hold live in a plugin on the runner.
  • Every callback returns None for tools it doesn’t handle.
  • Block results explain the block and the next step.
  • Argument edits are tested: the tool receives the edited value.
  • Callback lists tested together.
  • Blocks and edits logged with agent, tool and arguments.

For the same pattern elsewhere, see CrewAI tool call hooks and LangGraph interrupts; the tool-call policy entry explains what such rules look like. 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

An ADK before_tool_callback that returns a dict skips the tool and gives the model that dict as the result. Put guardrails in a plugin for every agent.

Read nextCrewAI tool call hooks: how to block a tool call or ask a person first Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1ADK docs: Types of callbacks · adk.dev · checked 26 October 2026
  2. 2ADK docs: Plugins · adk.dev · checked 26 October 2026
  3. 3Deedglass docs: Google ADK · 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