diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php
index 54307bc98971b2b0391e47c2bf0fb1f37248c32c..888f54e4f42cfdea0afd0142c1c011dfa824efa3 100644
--- a/core/lib/Drupal/Core/Recipe/Recipe.php
+++ b/core/lib/Drupal/Core/Recipe/Recipe.php
@@ -6,6 +6,7 @@
 
 use Drupal\Core\DefaultContent\Finder;
 use Drupal\Core\Extension\Dependency;
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\Extension\ThemeExtensionList;
 use Drupal\Component\Serialization\Yaml;
@@ -60,6 +61,8 @@ final class Recipe {
    *   The default content finder.
    * @param string $path
    *   The recipe's path.
+   * @param array $extra
+   *   Any extra information to expose to specific modules.
    */
   public function __construct(
     public readonly string $name,
@@ -71,6 +74,7 @@ public function __construct(
     public readonly InputConfigurator $input,
     public readonly Finder $content,
     public readonly string $path,
+    private readonly array $extra,
   ) {}
 
   /**
@@ -90,7 +94,7 @@ public static function createFromDirectory(string $path): static {
     $config = new ConfigConfigurator($recipe_data['config'], $path, \Drupal::service('config.storage'));
     $input = new InputConfigurator($recipe_data['input'] ?? [], $recipes, basename($path), \Drupal::typedDataManager());
     $content = new Finder($path . '/content');
-    return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $input, $content, $path);
+    return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $input, $content, $path, $recipe_data['extra'] ?? []);
   }
 
   /**
@@ -296,6 +300,12 @@ private static function parse(string $file): array {
       'content' => new Optional([
         new Type('array'),
       ]),
+      'extra' => new Optional([
+        new Sequentially([
+          new Type('associative_array'),
+          new Callback(self::validateKeysAreValidExtensionNames(...)),
+        ]),
+      ]),
     ]);
 
     $recipe_data = Yaml::decode($recipe_contents);
@@ -423,4 +433,40 @@ private static function validateConfigActions(mixed $value, ExecutionContextInte
     }
   }
 
+  /**
+   * Validates that the keys of an array are valid extension names.
+   *
+   * Note that the keys do not have to be the names of extensions that are
+   * installed, or even extensions that exist. They just have to follow the
+   * form of a valid extension name.
+   *
+   * @param array $value
+   *   The array being validated.
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The validator execution context.
+   */
+  private static function validateKeysAreValidExtensionNames(array $value, ExecutionContextInterface $context): void {
+    $keys = array_keys($value);
+    foreach ($keys as $key) {
+      if (!preg_match(ExtensionDiscovery::PHP_FUNCTION_PATTERN, $key)) {
+        $context->addViolation('%name is not a valid extension name.', [
+          '%name' => $key,
+        ]);
+      }
+    }
+  }
+
+  /**
+   * Returns extra information to expose to a particular extension.
+   *
+   * @param string $extension_name
+   *   The name of a Drupal extension.
+   *
+   * @return mixed
+   *   The extra data exposed to the given extension, or NULL if there is none.
+   */
+  public function getExtra(string $extension_name): mixed {
+    return $this->extra[$extension_name] ?? NULL;
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php
index b63861276ff34092cfb8ddc522fa3a2a03d99144..e9f5d3e0bf018238a72f80588a5247e23e663977 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeTest.php
@@ -98,4 +98,19 @@ public function testImplicitlyRequiredModule(): void {
     $this->assertIsObject($recipe);
   }
 
+  /**
+   * Tests getting extra extension-specific info from a recipe.
+   *
+   * @covers ::getExtra
+   */
+  public function testExtra(): void {
+    $recipe = $this->createRecipe([
+      'name' => 'Getting extra info',
+      'extra' => [
+        'special_sauce' => 'Wasabi',
+      ],
+    ]);
+    $this->assertSame('Wasabi', $recipe->getExtra('special_sauce'));
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php
index 8e4f621b2ad3a8821657b3333754c559df360c0a..62f14df4d2020757c73f5ccc9faa943032266766 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php
@@ -719,6 +719,45 @@ public static function providerRecipeValidation(): iterable {
     default:
       source: config
       config: ['system.site', 'mail']
+YAML,
+      NULL,
+    ];
+    yield 'extra is present and not an array' => [
+      <<<YAML
+name: Bad extra
+extra: 'yes!'
+YAML,
+      [
+        '[extra]' => ['This value should be of type associative_array.'],
+      ],
+    ];
+    yield 'extra is an indexed array' => [
+      <<<YAML
+name: Bad extra
+extra:
+  - one
+  - two
+YAML,
+      [
+        '[extra]' => ['This value should be of type associative_array.'],
+      ],
+    ];
+    yield 'invalid key in extra' => [
+      <<<YAML
+name: Bad extra
+extra:
+  'not a valid extension name': true
+YAML,
+      [
+        '[extra]' => ['not a valid extension name is not a valid extension name.'],
+      ],
+    ];
+    yield 'valid extra' => [
+      <<<YAML
+name: Bad extra
+extra:
+  project_browser:
+    yes: sir
 YAML,
       NULL,
     ];