diff --git a/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php
new file mode 100644
index 000000000..140ca007e
--- /dev/null
+++ b/includes/Checker/Checks/Plugin_Repo/External_Admin_Menu_Links_Check.php
@@ -0,0 +1,149 @@
+look_for_external_menu_links( $result, $php_files );
+ }
+
+ /**
+ * Looks for external URLs in admin menu functions and amends the given result with an error if found.
+ *
+ * @since 1.9.0
+ *
+ * @param Check_Result $result The check result to amend, including the plugin context to check.
+ * @param array $php_files List of absolute PHP file paths.
+ */
+ protected function look_for_external_menu_links( Check_Result $result, array $php_files ) {
+ // Build regex pattern for all menu functions.
+ $functions_pattern = implode( '|', array_map( 'preg_quote', $this->menu_functions ) );
+
+ // Pattern to match menu function calls with external URLs in the 4th parameter.
+ // This regex matches:
+ // - Function name from the list.
+ // - Opening parenthesis.
+ // - First 3 parameters (non-greedy, can be strings with single/double quotes or variables).
+ // - 4th parameter containing http://, https://, or // at the start of a string.
+ $pattern = '/\b(' . $functions_pattern . ')\s*\(\s*' .
+ // First parameter.
+ '(?:[^,]+)\s*,\s*' .
+ // Second parameter.
+ '(?:[^,]+)\s*,\s*' .
+ // Third parameter.
+ '(?:[^,]+)\s*,\s*' .
+ // Fourth parameter - look for external URL (http://, https://, or //).
+ '[\'"](?:https?:)?\/\/[^\'"]+[\'"]/i';
+
+ $files = self::files_preg_match_all( $pattern, $php_files );
+
+ if ( ! empty( $files ) ) {
+ foreach ( $files as $file ) {
+ $this->add_result_error_for_file(
+ $result,
+ __(
+ 'External URL used in top-level admin menu.
Plugins should not use add_menu_page() for external links as it disrupts the WordPress navigation experience.',
+ 'plugin-check'
+ ),
+ 'external_admin_menu_link',
+ $file['file'],
+ $file['line'],
+ $file['column'],
+ 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#11-plugins-should-not-hijack-the-admin',
+ 7
+ );
+ }
+ }
+ }
+
+ /**
+ * Gets the description for the check.
+ *
+ * Every check must have a short description explaining what the check does.
+ *
+ * @since 1.9.0
+ *
+ * @return string Description.
+ */
+ public function get_description(): string {
+ return __( 'Detects external URLs used in top-level WordPress admin menu, which disrupts the expected user experience.', 'plugin-check' );
+ }
+
+ /**
+ * Gets the documentation URL for the check.
+ *
+ * Every check must have a URL with further information about the check.
+ *
+ * @since 1.9.0
+ *
+ * @return string The documentation URL.
+ */
+ public function get_documentation_url(): string {
+ return __( 'https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#11-plugins-should-not-hijack-the-admin', 'plugin-check' );
+ }
+}
diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php
index d4e53f940..0a8d03226 100644
--- a/includes/Checker/Default_Check_Repository.php
+++ b/includes/Checker/Default_Check_Repository.php
@@ -100,6 +100,7 @@ private function register_default_checks() {
'direct_db' => new Checks\Security\Direct_DB_Check(),
'minified_files' => new Checks\Plugin_Repo\Minified_Files_Check(),
'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(),
+ 'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(),
)
);
diff --git a/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php
new file mode 100644
index 000000000..350102bb3
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-external-admin-menu-links-with-errors/load.php
@@ -0,0 +1,54 @@
+