diff --git a/lib/crewai/src/crewai/hooks/decorators.py b/lib/crewai/src/crewai/hooks/decorators.py index 6007f19bb..4f1da08f5 100644 --- a/lib/crewai/src/crewai/hooks/decorators.py +++ b/lib/crewai/src/crewai/hooks/decorators.py @@ -5,6 +5,8 @@ from functools import wraps import inspect from typing import TYPE_CHECKING, Any, TypeVar, overload +from crewai.utilities.string_utils import sanitize_tool_name + if TYPE_CHECKING: from crewai.hooks.llm_hooks import LLMCallHookContext @@ -37,6 +39,9 @@ def _create_hook_decorator( tools: list[str] | None = None, agents: list[str] | None = None, ) -> Callable[..., Any]: + if tools: + tools = [sanitize_tool_name(t) for t in tools] + def decorator(f: Callable[..., Any]) -> Callable[..., Any]: setattr(f, marker_attribute, True) diff --git a/lib/crewai/src/crewai/utilities/tool_utils.py b/lib/crewai/src/crewai/utilities/tool_utils.py index 027f136ed..42bdf52cd 100644 --- a/lib/crewai/src/crewai/utilities/tool_utils.py +++ b/lib/crewai/src/crewai/utilities/tool_utils.py @@ -96,7 +96,7 @@ async def aexecute_tool_and_check_finality( if tool: tool_input = tool_calling.arguments if tool_calling.arguments else {} hook_context = ToolCallHookContext( - tool_name=tool_calling.tool_name, + tool_name=sanitized_tool_name, tool_input=tool_input, tool=tool, agent=agent, @@ -120,7 +120,7 @@ async def aexecute_tool_and_check_finality( tool_result = await tool_usage.ause(tool_calling, agent_action.text) after_hook_context = ToolCallHookContext( - tool_name=tool_calling.tool_name, + tool_name=sanitized_tool_name, tool_input=tool_input, tool=tool, agent=agent, diff --git a/lib/crewai/tests/hooks/test_decorators.py b/lib/crewai/tests/hooks/test_decorators.py index ec147068d..a19a0f740 100644 --- a/lib/crewai/tests/hooks/test_decorators.py +++ b/lib/crewai/tests/hooks/test_decorators.py @@ -192,6 +192,38 @@ class TestToolHookDecorators: # Should still be 1 (hook didn't execute for read_file) assert len(execution_log) == 1 + def test_before_tool_call_tool_filter_sanitizes_names(self): + """Tool filter should auto-sanitize names so users can pass BaseTool.name directly.""" + execution_log = [] + + # User passes the human-readable tool name (e.g. BaseTool.name) + @before_tool_call(tools=["Delete File", "Execute Code"]) + def filtered_hook(context): + execution_log.append(context.tool_name) + return None + + hooks = get_before_tool_call_hooks() + assert len(hooks) == 1 + + mock_tool = Mock() + # Context uses the sanitized name (as set by the executor) + context = ToolCallHookContext( + tool_name="delete_file", + tool_input={}, + tool=mock_tool, + ) + hooks[0](context) + assert execution_log == ["delete_file"] + + # Non-matching tool still filtered out + context2 = ToolCallHookContext( + tool_name="read_file", + tool_input={}, + tool=mock_tool, + ) + hooks[0](context2) + assert execution_log == ["delete_file"] + def test_before_tool_call_with_combined_filters(self): """Test that combined tool and agent filters work.""" execution_log = []