diff --git a/core/lib/Drupal/Core/Command/GenerateTheme.php b/core/lib/Drupal/Core/Command/GenerateTheme.php
index 66b373d12086bf6182db61da9256f8c52171f15c..e613358f2b19cfa09d7787a37bb9b9d5e84dd869 100644
--- a/core/lib/Drupal/Core/Command/GenerateTheme.php
+++ b/core/lib/Drupal/Core/Command/GenerateTheme.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Command;
 
 use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
 use Drupal\Component\Serialization\Yaml;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ExtensionDiscovery;
@@ -14,7 +15,9 @@
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
 use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Process\Process;
 use Twig\Util\TemplateDirIterator;
 
 /**
@@ -128,6 +131,43 @@ protected function execute(InputInterface $input, OutputInterface $output): int
 
     $info['core_version_requirement'] = '^' . $this->getVersion();
 
+    if (!array_key_exists('version', $info)) {
+      $confirm_versionless_source_theme = new ConfirmationQuestion(sprintf('The source theme %s does not have a version specified. This makes tracking changes in the source theme difficult. Are you sure you want to continue?', $source_theme->getName()));
+      if (!$io->askQuestion($confirm_versionless_source_theme)) {
+        return 0;
+      }
+    }
+
+    $source_version = $info['version'] ?? 'unknown-version';
+    if ($source_version === 'VERSION') {
+      $source_version = \Drupal::VERSION;
+    }
+    // A version in the generator string like "9.4.0-dev" is not very helpful.
+    // When this occurs, generate a version string that points to a commit.
+    if (VersionParser::parseStability($source_version) === 'dev') {
+      $git_check = Process::fromShellCommandline('git --help');
+      $git_check->run();
+      if ($git_check->getExitCode()) {
+        $io->error(sprintf('The source theme %s has a development version number (%s). Determining a specific commit is not possible because git is not installed. Either install git or use a tagged release to generate a theme.', $source_theme->getName(), $source_version));
+        return 1;
+      }
+
+      // Get the git commit for the source theme.
+      $git_get_commit = Process::fromShellCommandline("git rev-list --max-count=1 --abbrev-commit HEAD -C $source");
+      $git_get_commit->run();
+      if ($git_get_commit->getOutput() === '') {
+        $confirm_packaged_dev_release = new ConfirmationQuestion(sprintf('The source theme %s has a development version number (%s). Because it is not a git checkout, a specific commit could not be identified. This makes tracking changes in the source theme difficult. Are you sure you want to continue?', $source_theme->getName(), $source_version));
+        if (!$io->askQuestion($confirm_packaged_dev_release)) {
+          return 0;
+        }
+        $source_version .= '#unknown-commit';
+      }
+      else {
+        $source_version .= '#' . trim($git_get_commit->getOutput());
+      }
+    }
+    $info['generator'] = "$source_theme_name:$source_version";
+
     if ($description = $input->getOption('description')) {
       $info['description'] = $description;
     }
diff --git a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php
index 054e174886fc1c799f830aa3696211a23a98f04d..792c707e1e209c8b67a08e834b8cecf1830d9a95 100644
--- a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php
+++ b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php
@@ -40,9 +40,12 @@ protected function setUp(): void {
   }
 
   /**
-   * Tests the generate-theme command.
+   * Generates PHP process to generate a theme from core's starterkit theme.
+   *
+   * @return \Symfony\Component\Process\Process
+   *   The PHP process
    */
-  public function test() {
+  private function generateThemeFromStarterkit() : Process {
     $install_command = [
       $this->php,
       'core/scripts/drupal',
@@ -53,15 +56,48 @@ public function test() {
     ];
     $process = new Process($install_command, NULL);
     $process->setTimeout(60);
+    return $process;
+  }
+
+  /**
+   * Asserts the theme exists. Returns the parsed *.info.yml file.
+   *
+   * @param string $theme_path_relative
+   *   The core-relative path to the theme.
+   *
+   * @return array
+   *   The parsed *.info.yml file.
+   */
+  private function assertThemeExists(string $theme_path_relative): array {
+    $theme_path_absolute = $this->getWorkspaceDirectory() . "/$theme_path_relative";
+    $theme_name = basename($theme_path_relative);
+    $info_yml_filename = "$theme_name.info.yml";
+    $this->assertFileExists($theme_path_absolute . '/' . $info_yml_filename);
+    $info = Yaml::decode(file_get_contents($theme_path_absolute . '/' . $info_yml_filename));
+    return $info;
+  }
+
+  /**
+   * Tests the generate-theme command.
+   */
+  public function test() {
+    // Do not rely on \Drupal::VERSION: change the version to a concrete version
+    // number, to simulate using a tagged core release.
+    $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml';
+    $info = Yaml::decode(file_get_contents($starterkit_info_yml));
+    $info['version'] = '9.4.0';
+    file_put_contents($starterkit_info_yml, Yaml::encode($info));
+
+    $process = $this->generateThemeFromStarterkit();
     $result = $process->run();
     $this->assertEquals('Theme generated successfully to themes/test_custom_theme', trim($process->getOutput()), $process->getErrorOutput());
     $this->assertSame(0, $result);
 
     $theme_path_relative = 'themes/test_custom_theme';
-    $theme_path_absolute = $this->getWorkspaceDirectory() . "/$theme_path_relative";
-    $this->assertFileExists($theme_path_absolute . '/test_custom_theme.info.yml');
-    $info = Yaml::decode(file_get_contents($theme_path_absolute . '/test_custom_theme.info.yml'));
+    $info = $this->assertThemeExists($theme_path_relative);
     self::assertArrayNotHasKey('hidden', $info);
+    self::assertArrayHasKey('generator', $info);
+    self::assertEquals('starterkit_theme:9.4.0', $info['generator']);
 
     // Ensure that the generated theme can be installed.
     $this->installQuickStart('minimal');
@@ -72,10 +108,12 @@ public function test() {
     $this->getMink()->getSession()->getPage()->clickLink('Install "Test custom starterkit theme" theme');
     $this->getMink()->assertSession()->pageTextContains('The "Test custom starterkit theme" theme has been installed.');
 
+    // Ensure that a new theme cannot be generated when the destination
+    // directory already exists.
+    $theme_path_absolute = $this->getWorkspaceDirectory() . "/$theme_path_relative";
     $this->assertFileExists($theme_path_absolute . '/test_custom_theme.theme');
     unlink($theme_path_absolute . '/test_custom_theme.theme');
-    $process = new Process($install_command, NULL);
-    $process->setTimeout(60);
+    $process = $this->generateThemeFromStarterkit();
     $result = $process->run();
     $this->assertStringContainsString('Theme could not be generated because the destination directory', $process->getErrorOutput());
     $this->assertStringContainsString($theme_path_relative, $process->getErrorOutput());
@@ -83,6 +121,145 @@ public function test() {
     $this->assertFileDoesNotExist($theme_path_absolute . '/test_custom_theme.theme');
   }
 
+  /**
+   * Tests the generate-theme command on a dev snapshot of Drupal core.
+   */
+  public function testDevSnapshot() {
+    // Do not rely on \Drupal::VERSION: change the version to a development
+    // snapshot version number, to simulate using a branch snapshot of core.
+    $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml';
+    $info = Yaml::decode(file_get_contents($starterkit_info_yml));
+    $info['version'] = '9.4.0-dev';
+    file_put_contents($starterkit_info_yml, Yaml::encode($info));
+
+    $process = $this->generateThemeFromStarterkit();
+    $result = $process->run();
+    $this->assertEquals('Theme generated successfully to themes/test_custom_theme', trim($process->getOutput()), $process->getErrorOutput());
+    $this->assertSame(0, $result);
+
+    $theme_path_relative = 'themes/test_custom_theme';
+    $info = $this->assertThemeExists($theme_path_relative);
+    self::assertArrayNotHasKey('hidden', $info);
+    self::assertArrayHasKey('generator', $info);
+    self::assertMatchesRegularExpression('/^starterkit_theme\:9.4.0-dev#[0-9a-f]+$/', $info['generator']);
+  }
+
+  /**
+   * Tests the generate-theme command on a theme with a release version number.
+   */
+  public function testContribStarterkit(): void {
+    // Change the version to a concrete version number, to simulate using a
+    // contrib theme as the starterkit.
+    $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml';
+    $info = Yaml::decode(file_get_contents($starterkit_info_yml));
+    $info['version'] = '1.20';
+    file_put_contents($starterkit_info_yml, Yaml::encode($info));
+
+    $process = $this->generateThemeFromStarterkit();
+    $result = $process->run();
+    $this->assertEquals('Theme generated successfully to themes/test_custom_theme', trim($process->getOutput()), $process->getErrorOutput());
+    $this->assertSame(0, $result);
+    $info = $this->assertThemeExists('themes/test_custom_theme');
+    self::assertArrayNotHasKey('hidden', $info);
+    self::assertArrayHasKey('generator', $info);
+    self::assertEquals('starterkit_theme:1.20', $info['generator']);
+  }
+
+  /**
+   * Tests the generate-theme command on a theme with a dev version number.
+   */
+  public function testContribStarterkitDevSnapshot(): void {
+    // Change the version to a development snapshot version number, to simulate
+    // using a contrib theme as the starterkit.
+    $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml';
+    $info = Yaml::decode(file_get_contents($starterkit_info_yml));
+    $info['core_version_requirement'] = '*';
+    $info['version'] = '7.x-dev';
+    file_put_contents($starterkit_info_yml, Yaml::encode($info));
+
+    // Avoid the core git commit from being considered the source theme's: move
+    // it out of core.
+    Process::fromShellCommandline('mv core/themes/starterkit_theme themes/', $this->getWorkspaceDirectory())->run();
+
+    $process = $this->generateThemeFromStarterkit();
+    $result = $process->run();
+    $this->assertEquals("The source theme starterkit_theme has a development version number (7.x-dev). Because it is not a git checkout, a specific commit could not be identified. This makes tracking changes in the source theme difficult. Are you sure you want to continue? (yes/no) [yes]:\n > Theme generated successfully to themes/test_custom_theme", trim($process->getOutput()), $process->getErrorOutput());
+    $this->assertSame(0, $result);
+    $info = $this->assertThemeExists('themes/test_custom_theme');
+    self::assertArrayNotHasKey('hidden', $info);
+    self::assertArrayHasKey('generator', $info);
+    self::assertEquals('starterkit_theme:7.x-dev#unknown-commit', $info['generator']);
+  }
+
+  /**
+   * Tests the generate-theme command on a theme with a dev version without git.
+   */
+  public function testContribStarterkitDevSnapshotWithGitNotInstalled(): void {
+    // Change the version to a development snapshot version number, to simulate
+    // using a contrib theme as the starterkit.
+    $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml';
+    $info = Yaml::decode(file_get_contents($starterkit_info_yml));
+    $info['core_version_requirement'] = '*';
+    $info['version'] = '7.x-dev';
+    file_put_contents($starterkit_info_yml, Yaml::encode($info));
+
+    // Avoid the core git commit from being considered the source theme's: move
+    // it out of core.
+    Process::fromShellCommandline('mv core/themes/starterkit_theme themes/', $this->getWorkspaceDirectory())->run();
+
+    // Confirm that 'git' is available.
+    $output = [];
+    exec('git --help', $output, $status);
+    $this->assertEquals(0, $status);
+    // Modify our $PATH so that it begins with a path that contains an
+    // executable script named 'git' that always exits with 127, as if git were
+    // not found. Note that we run our tests using process isolation, so we do
+    // not need to restore the PATH when we are done.
+    $unavailableGitPath = $this->getWorkspaceDirectory() . '/bin';
+    mkdir($unavailableGitPath);
+    $bash = <<<SH
+#!/bin/bash
+exit 127
+
+SH;
+    file_put_contents($unavailableGitPath . '/git', $bash);
+    chmod($unavailableGitPath . '/git', 0755);
+    $oldPath = getenv('PATH');
+    putenv('PATH=' . $unavailableGitPath . ':' . getenv('PATH'));
+    // Confirm that 'git' is no longer available.
+    $output = [];
+    exec('git --help', $output, $status);
+    $this->assertEquals(127, $status);
+
+    $process = $this->generateThemeFromStarterkit();
+    $result = $process->run();
+    $this->assertEquals("[ERROR] The source theme starterkit_theme has a development version number     \n         (7.x-dev). Determining a specific commit is not possible because git is\n         not installed. Either install git or use a tagged release to generate a\n         theme.", trim($process->getOutput()), $process->getErrorOutput());
+    $this->assertSame(1, $result);
+    $this->assertFileDoesNotExist($this->getWorkspaceDirectory() . "/themes/test_custom_theme");
+
+    putenv('PATH=' . $oldPath . ':' . getenv('PATH'));
+  }
+
+  /**
+   * Tests the generate-theme command on a theme without a version number.
+   */
+  public function testCustomStarterkit(): void {
+    // Omit the version, to simulate using a custom theme as the starterkit.
+    $starterkit_info_yml = $this->getWorkspaceDirectory() . '/core/themes/starterkit_theme/starterkit_theme.info.yml';
+    $info = Yaml::decode(file_get_contents($starterkit_info_yml));
+    unset($info['version']);
+    file_put_contents($starterkit_info_yml, Yaml::encode($info));
+
+    $process = $this->generateThemeFromStarterkit();
+    $result = $process->run();
+    $this->assertEquals("The source theme starterkit_theme does not have a version specified. This makes tracking changes in the source theme difficult. Are you sure you want to continue? (yes/no) [yes]:\n > Theme generated successfully to themes/test_custom_theme", trim($process->getOutput()), $process->getErrorOutput());
+    $this->assertSame(0, $result);
+    $info = $this->assertThemeExists('themes/test_custom_theme');
+    self::assertArrayNotHasKey('hidden', $info);
+    self::assertArrayHasKey('generator', $info);
+    self::assertEquals('starterkit_theme:unknown-version', $info['generator']);
+  }
+
   /**
    * Tests themes that do not exist return an error.
    */
diff --git a/core/themes/starterkit_theme/starterkit_theme.info.yml b/core/themes/starterkit_theme/starterkit_theme.info.yml
index b1673d73f6833d807e34d31d30b9014adf4ee1b3..aa46cbc9b089b9a52150ef4e8d9f97f2d8233345 100644
--- a/core/themes/starterkit_theme/starterkit_theme.info.yml
+++ b/core/themes/starterkit_theme/starterkit_theme.info.yml
@@ -3,6 +3,7 @@ type: theme
 'base theme': stable9
 hidden: true
 starterkit: true
+version: VERSION
 libraries:
   - starterkit_theme/base
   - starterkit_theme/messages