Hide posts of a certain category unless logged in

I would just set the post status for all posts for that particular category to private. If you already have a ton of posts, write yourself a small script using wp_update_post() to update all those posts’ post status to private. Any posts published after this you can then just set to private (in the publish meta box) before publishing them.

The main query by default only shows private posts to logged in users

EDIT from COMMENTS

An addition from the OP in comments that might be useful in future

This worked out fine, except that it required users to have too much power(be Editors) just to read the private posts. I resolved that by adding the following in a plugin: $subRole = get_role( 'subscriber' ); $subRole->add_cap( 'read_private_pages' ); $subRole->add_cap( 'read_private_posts' );

SECOND ADDITION

I wonder if we can just set $posts to an empty array for all category pages where 4 is the category, and also on any single page if the post belongs to category 4

add_filter( 'the_posts', function ( $posts, $q )
{
    if ( is_user_logged_in() )
        return $posts;

    if (    $q->is_main_query()
         && (    $q->is_category( 4 )
              || (    $q->is_single() 
                   && has_category( 4, $posts[0] )
                 )
             )
    ) {
        return $posts = [];
    }
    return $posts;
}, 10, 2 );

This should return no posts in the loop. This is all untested so it would need proper testing off line before you use it in a production site

Just a note, this will fail if a post belongs to a category which is a descendant of category 4 and the post is not explicitly assigned to category 4. This also goes for category pages which are descendants of category 4. For this to work, you would need to add additional checks to check the top level parent of the category, but it can become quite a big overhead to run

FINAL EDITION

The above filter code is now tested and working on the main queries on single pages and category pages. On category pages the loop displays no posts, and on single pages it does lead to a 404 page. You can use pre_get_posts to remove any other instances of posts belonging to category 2

add_action( 'pre_get_posts', function ( $q )
{
    if ( is_user_logged_in() )
        return;

    if (    !is_admin() // Target only front end
         && !$q->is_singular() // Do not target singular pages
         && $q->is_main_query() // Target only the main query
    ) {
        $q->set( 'cat', -4 );
    }
});

FLAWS IN DESIGN

Although the code works, it does have loopholes like custom queries and pages which are used to list posts from categories. It is a dangerous route to take and private info might be unintentionally displayed (through a bad custom query for example) to non-logged in users

Conclusion

The safest way to tackle this issue is to make posts private that should only be displayed to logged in users. As you have already shown (in comments), it is easy to adjust roles to show them private posts once they are logged in