diff --git a/mypy/build.py b/mypy/build.py
index 1720eedaad10..018a6abcd230 100644
--- a/mypy/build.py
+++ b/mypy/build.py
@@ -668,16 +668,10 @@ def __init__(
                 raise CompileError(
                     [f"Failed to find builtin module {module}, perhaps typeshed is broken?"]
                 )
-            if is_typeshed_file(path):
+            if is_typeshed_file(options.abs_custom_typeshed_dir, path) or is_stub_package_file(
+                path
+            ):
                 continue
-            if is_stub_package_file(path):
-                continue
-            if options.custom_typeshed_dir is not None:
-                # Check if module lives under custom_typeshed_dir subtree
-                custom_typeshed_dir = os.path.abspath(options.custom_typeshed_dir)
-                path = os.path.abspath(path)
-                if os.path.commonpath((path, custom_typeshed_dir)) == custom_typeshed_dir:
-                    continue
 
             raise CompileError(
                 [
diff --git a/mypy/checker.py b/mypy/checker.py
index 539cd7a443e0..32b8d5a5a170 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -387,7 +387,7 @@ def __init__(
         self.pass_num = 0
         self.current_node_deferred = False
         self.is_stub = tree.is_stub
-        self.is_typeshed_stub = is_typeshed_file(path)
+        self.is_typeshed_stub = is_typeshed_file(options.abs_custom_typeshed_dir, path)
         self.inferred_attribute_types = None
 
         # If True, process function definitions. If False, don't. This is used
diff --git a/mypy/errors.py b/mypy/errors.py
index a6f50ff34de2..00f715a0c4d6 100644
--- a/mypy/errors.py
+++ b/mypy/errors.py
@@ -617,7 +617,10 @@ def clear_errors_in_targets(self, path: str, targets: set[str]) -> None:
                 self.has_blockers.remove(path)
 
     def generate_unused_ignore_errors(self, file: str) -> None:
-        if is_typeshed_file(file) or file in self.ignored_files:
+        if (
+            is_typeshed_file(self.options.abs_custom_typeshed_dir if self.options else None, file)
+            or file in self.ignored_files
+        ):
             return
         ignored_lines = self.ignored_lines[file]
         used_ignored_lines = self.used_ignored_lines[file]
@@ -658,7 +661,10 @@ def generate_unused_ignore_errors(self, file: str) -> None:
     def generate_ignore_without_code_errors(
         self, file: str, is_warning_unused_ignores: bool
     ) -> None:
-        if is_typeshed_file(file) or file in self.ignored_files:
+        if (
+            is_typeshed_file(self.options.abs_custom_typeshed_dir if self.options else None, file)
+            or file in self.ignored_files
+        ):
             return
 
         used_ignored_lines = self.used_ignored_lines[file]
diff --git a/mypy/main.py b/mypy/main.py
index 3dce045be75b..dcae77f24f8a 100644
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -1264,6 +1264,10 @@ def set_strict_flags() -> None:
     # Enabling an error code always overrides disabling
     options.disabled_error_codes -= options.enabled_error_codes
 
+    # Compute absolute path for custom typeshed (if present).
+    if options.custom_typeshed_dir is not None:
+        options.abs_custom_typeshed_dir = os.path.abspath(options.custom_typeshed_dir)
+
     # Set build flags.
     if special_opts.find_occurrences:
         state.find_occurrences = special_opts.find_occurrences.split(".")
diff --git a/mypy/options.py b/mypy/options.py
index fb7bb8e43bbb..b129303c304c 100644
--- a/mypy/options.py
+++ b/mypy/options.py
@@ -77,6 +77,8 @@ def __init__(self) -> None:
         self.platform = sys.platform
         self.custom_typing_module: str | None = None
         self.custom_typeshed_dir: str | None = None
+        # The abspath() version of the above, we compute it once as an optimization.
+        self.abs_custom_typeshed_dir: str | None = None
         self.mypy_path: list[str] = []
         self.report_dirs: dict[str, str] = {}
         # Show errors in PEP 561 packages/site-packages modules
diff --git a/mypy/semanal.py b/mypy/semanal.py
index 623f660010f6..4685a9b9da8f 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -741,7 +741,9 @@ def file_context(
         self.cur_mod_id = file_node.fullname
         with scope.module_scope(self.cur_mod_id):
             self._is_stub_file = file_node.path.lower().endswith(".pyi")
-            self._is_typeshed_stub_file = is_typeshed_file(file_node.path)
+            self._is_typeshed_stub_file = is_typeshed_file(
+                options.abs_custom_typeshed_dir, file_node.path
+            )
             self.globals = file_node.names
             self.tvar_scope = TypeVarLikeScope()
 
diff --git a/mypy/semanal_main.py b/mypy/semanal_main.py
index 406fd93139d1..9e3aeaa7fa4b 100644
--- a/mypy/semanal_main.py
+++ b/mypy/semanal_main.py
@@ -367,7 +367,11 @@ def check_type_arguments(graph: Graph, scc: list[str], errors: Errors) -> None:
     for module in scc:
         state = graph[module]
         assert state.tree
-        analyzer = TypeArgumentAnalyzer(errors, state.options, is_typeshed_file(state.path or ""))
+        analyzer = TypeArgumentAnalyzer(
+            errors,
+            state.options,
+            is_typeshed_file(state.options.abs_custom_typeshed_dir, state.path or ""),
+        )
         with state.wrap_context():
             with mypy.state.state.strict_optional_set(state.options.strict_optional):
                 state.tree.accept(analyzer)
@@ -381,7 +385,11 @@ def check_type_arguments_in_targets(
     This mirrors the logic in check_type_arguments() except that we process only
     some targets. This is used in fine grained incremental mode.
     """
-    analyzer = TypeArgumentAnalyzer(errors, state.options, is_typeshed_file(state.path or ""))
+    analyzer = TypeArgumentAnalyzer(
+        errors,
+        state.options,
+        is_typeshed_file(state.options.abs_custom_typeshed_dir, state.path or ""),
+    )
     with state.wrap_context():
         with mypy.state.state.strict_optional_set(state.options.strict_optional):
             for target in targets:
diff --git a/mypy/util.py b/mypy/util.py
index 686a71c4331b..5bb130c255c4 100644
--- a/mypy/util.py
+++ b/mypy/util.py
@@ -769,9 +769,10 @@ def format_error(
         return self.style(msg, "red", bold=True)
 
 
-def is_typeshed_file(file: str) -> bool:
+def is_typeshed_file(typeshed_dir: str | None, file: str) -> bool:
+    typeshed_dir = typeshed_dir if typeshed_dir is not None else TYPESHED_DIR
     try:
-        return os.path.commonpath((TYPESHED_DIR, os.path.abspath(file))) == TYPESHED_DIR
+        return os.path.commonpath((typeshed_dir, os.path.abspath(file))) == typeshed_dir
     except ValueError:  # Different drives on Windows
         return False