paginate_links() outputs extra empty pages on custom loop

You use your custom WP_Query to query posts, but then in pagination you use global $wp_query variable, which contains different query.

You should use your custom query variable $works in your pagination, or change global $wp_query variable before your pagination function call.

Since you use universal function to output pagination, second method will be better/easier. Do it like this:

global $wp_query;
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$works = new WP_Query('category_name=work&posts_per_page=9&paged=' . $paged);

$original_query = $wp_query;  // store original query for later use
$wp_query = $works;  // overwrite it with your custom query

... your code

... your pagination code

$wp_query = $original_query;  // restore original query