diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 0843453e..95a55daa 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -354,6 +354,7 @@ async def _handle_control_request(self, request: SDKControlRequest) -> None: or [], tool_use_id=permission_request.get("tool_use_id"), agent_id=permission_request.get("agent_id"), + blocked_path=permission_request.get("blocked_path"), ) response = await self.can_use_tool( diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 9c2be63f..2235f7cd 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -184,6 +184,11 @@ class ToolPermissionContext: Multiple tool calls in the same assistant message will have different tool_use_ids.""" agent_id: str | None = None """If running within the context of a sub-agent, the sub-agent's ID.""" + blocked_path: str | None = None + """Filesystem path that triggered this permission request, when the tool was + blocked by an `additionalDirectories`/`allowedDirectories` constraint or a + path-scoped permission rule. None when the request is not path-scoped (e.g. + the tool itself is restricted regardless of inputs).""" # Match TypeScript's PermissionResult structure diff --git a/tests/test_tool_callbacks.py b/tests/test_tool_callbacks.py index 749054b5..177d72bd 100644 --- a/tests/test_tool_callbacks.py +++ b/tests/test_tool_callbacks.py @@ -249,6 +249,81 @@ async def capture_callback( assert received_context.tool_use_id == "toolu_01XYZ789" assert received_context.agent_id is None + @pytest.mark.asyncio + async def test_permission_callback_receives_blocked_path(self): + """Test that blocked_path is forwarded to the context when set.""" + received_context = None + + async def capture_callback( + tool_name: str, input_data: dict, context: ToolPermissionContext + ) -> PermissionResultAllow: + nonlocal received_context + received_context = context + return PermissionResultAllow() + + transport = MockTransport() + query = Query( + transport=transport, + is_streaming_mode=True, + can_use_tool=capture_callback, + hooks=None, + ) + + request = { + "type": "control_request", + "request_id": "test-blockedpath", + "request": { + "subtype": "can_use_tool", + "tool_name": "Read", + "input": {"file_path": "/etc/passwd"}, + "permission_suggestions": [], + "tool_use_id": "toolu_01PATH", + "blocked_path": "/etc/passwd", + }, + } + + await query._handle_control_request(request) + + assert received_context is not None + assert received_context.blocked_path == "/etc/passwd" + + @pytest.mark.asyncio + async def test_permission_callback_missing_blocked_path(self): + """Test that blocked_path defaults to None when not sent.""" + received_context = None + + async def capture_callback( + tool_name: str, input_data: dict, context: ToolPermissionContext + ) -> PermissionResultAllow: + nonlocal received_context + received_context = context + return PermissionResultAllow() + + transport = MockTransport() + query = Query( + transport=transport, + is_streaming_mode=True, + can_use_tool=capture_callback, + hooks=None, + ) + + request = { + "type": "control_request", + "request_id": "test-nopath", + "request": { + "subtype": "can_use_tool", + "tool_name": "TestTool", + "input": {}, + "permission_suggestions": [], + "tool_use_id": "toolu_01NOPATH", + }, + } + + await query._handle_control_request(request) + + assert received_context is not None + assert received_context.blocked_path is None + @pytest.mark.asyncio async def test_callback_exception_handling(self): """Test that callback exceptions are properly handled."""