Hide directly viewing content for custom post type

register_post_type allows you to specify whether a post is ‘publicly_queryable’

publicly_queryable
(boolean) (optional) Whether queries can be performed on the front end as part of parse_request().

Default: value of public argument
Note: The queries affected include the following (also initiated when rewrites are handled)

  • ?post_type={post_type_key}
  • ?{post_type_key}={single_post_slug}
  • ?{post_type_query_var}={single_post_slug}

It doesn’t mention anything about queries of the form ?p=, and from memory, these will still serve your custom post types event with the publicly_queryable set to false. To prevent this you can hook onto template_redirect and redirect to a different (404?) page.