diff --git a/automatic_updates.module b/automatic_updates.module
index 6109e4c..fc4fa61 100644
--- a/automatic_updates.module
+++ b/automatic_updates.module
@@ -7,6 +7,8 @@
 
 declare(strict_types=1);
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\automatic_updates\Hook\AutomaticUpdatesHooks1;
 use Drupal\automatic_updates\CronUpdateRunner;
 use Drupal\automatic_updates\ReleaseChooser;
 use Drupal\automatic_updates\UpdateSandboxManager;
@@ -17,38 +19,10 @@ use Drupal\Core\Utility\Error;
 /**
  * Implements hook_form_FORM_ID_alter() for update_settings.
  */
-function automatic_updates_form_update_settings_alter(array &$form): void {
-  $config = \Drupal::config('automatic_updates.settings');
-
-  $form['unattended_level'] = [
-    '#type' => 'radios',
-    '#title' => t('Unattended background updates'),
-    '#options' => [
-      CronUpdateRunner::DISABLED => t('Disabled'),
-      CronUpdateRunner::SECURITY => t('Security updates only'),
-      CronUpdateRunner::ALL => t('All patch releases'),
-    ],
-    '#default_value' => $config->get('unattended.level'),
-    '#description' => t('When background updates are applied, your site will be briefly put into maintenance mode.'),
-  ];
-  $form['unattended_method'] = [
-    '#type' => 'radios',
-    '#title' => t('How unattended updates should be run'),
-    '#options' => [
-      'web' => t('By using the Automated Cron module or a request to /system/cron'),
-      'console' => t('By the <code>auto-update</code> command-line utility'),
-    ],
-    '#default_value' => $config->get('unattended.method'),
-    '#states' => [
-      'invisible' => [
-        'input[name="unattended_level"]' => [
-          'value' => CronUpdateRunner::DISABLED,
-        ],
-      ],
-    ],
-    '#description' => t('To use the <code>/system/cron</code> method <a href="http://drupal.org/docs/user_guide/en/security-cron.html">ensure cron is set up correctly</a>.'),
-  ];
-  $form['#submit'][] = '_automatic_updates_submit_update_settings';
+#[LegacyHook]
+function automatic_updates_form_update_settings_alter(array &$form): void
+{
+    \Drupal::service(AutomaticUpdatesHooks1::class)->formUpdateSettingsAlter($form);
 }
 
 /**
@@ -72,64 +46,8 @@ function _automatic_updates_submit_update_settings(array &$form, FormStateInterf
 /**
  * Implements hook_preprocess_update_project_status().
  */
-function automatic_updates_preprocess_update_project_status(array &$variables) {
-  $project = &$variables['project'];
-  if ($project['name'] !== 'drupal') {
-    return;
-  }
-  $stage = \Drupal::service(UpdateSandboxManager::class);
-  $supported_target_versions = [];
-  /** @var \Drupal\automatic_updates\ReleaseChooser $recommender */
-  $recommender = \Drupal::service(ReleaseChooser::class);
-  try {
-    if ($installed_minor_release = $recommender->getLatestInInstalledMinor($stage)) {
-      $supported_target_versions[] = $installed_minor_release->getVersion();
-    }
-    if ($next_minor_release = $recommender->getLatestInNextMinor($stage)) {
-      $supported_target_versions[] = $next_minor_release->getVersion();
-    }
-  }
-  catch (RuntimeException $exception) {
-    // If for some reason we are not able to get the update recommendations
-    // do not alter the report.
-    Error::logException(\Drupal::logger('automatic_updates'), $exception);
-    return;
-  }
-  $variables['#attached']['library'][] = 'automatic_updates/update_status';
-
-  $update_form_url = Url::fromRoute('automatic_updates.update_form')
-    ->setAbsolute()
-    ->toString();
-
-  $status = &$variables['status'];
-  if ($supported_target_versions && $status['label']) {
-    $status['label'] = [
-      '#markup' => t(
-        '@label <a href=":update-form">Update now</a>', [
-          '@label' => $status['label'],
-          ':update-form' => $update_form_url,
-        ]),
-    ];
-  }
-  // BEGIN: DELETE FROM CORE MERGE REQUEST
-  if (empty($variables['versions'])) {
-    return;
-  }
-  foreach ($variables['versions'] as &$themed_version) {
-    $version_info = &$themed_version['#version'];
-    if ($supported_target_versions && in_array($version_info['version'], $supported_target_versions, TRUE)) {
-      $version_info['download_link'] = $update_form_url;
-    }
-    else {
-      // If this version will not be displayed as an option on this module's
-      // update form replace the link to download the archive file with the
-      // release notes link. The release notes page will provide Composer
-      // instructions. While this isn't a perfect solution the Update module
-      // Twig templates do not check if 'download_link' is set, so we cannot
-      // unset it here.
-      $themed_version['#attributes']['class'][] = 'automatic-updates-unsupported-version';
-      $version_info['download_link'] = $version_info['release_link'];
-    }
-  }
-  // END: DELETE FROM CORE MERGE REQUEST
+#[LegacyHook]
+function automatic_updates_preprocess_update_project_status(array &$variables)
+{
+    \Drupal::service(AutomaticUpdatesHooks1::class)->preprocessUpdateProjectStatus($variables);
 }
diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index 4ebf873..30fb346 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -42,3 +42,7 @@ services:
   Drupal\automatic_updates\CommandExecutor:
     arguments:
       $appRoot: '%app.root%'
+
+  Drupal\automatic_updates\Hook\AutomaticUpdatesHooks1:
+    class: Drupal\automatic_updates\Hook\AutomaticUpdatesHooks1
+    autowire: true
diff --git a/scripts/src/ConverterCommand.php b/scripts/src/ConverterCommand.php
index c76a88b..cfce5d6 100644
--- a/scripts/src/ConverterCommand.php
+++ b/scripts/src/ConverterCommand.php
@@ -6,6 +6,7 @@ declare(strict_types=1);
 
 namespace Drupal\automatic_updates\Development;
 
+use Symfony\Component\Console\Attribute\AsCommand;
 use Composer\Script\Event;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputArgument;
@@ -63,6 +64,7 @@ use Symfony\Component\Filesystem\Filesystem;
  *
  * The core clone should already have the core merge request locally.
  */
+#[AsCommand]
 class ConverterCommand extends Command {
 
   private string $core_dir;
diff --git a/src/Commands/PostApplyCommand.php b/src/Commands/PostApplyCommand.php
index c029d99..0dc35b2 100644
--- a/src/Commands/PostApplyCommand.php
+++ b/src/Commands/PostApplyCommand.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\automatic_updates\Commands;
 
+use Symfony\Component\Console\Attribute\AsCommand;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\package_manager\ProjectInfo;
 use Symfony\Component\Console\Input\InputArgument;
@@ -16,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface;
  *   at any time without warning. This command should not be called directly,
  *   and this class should not be used by external code.
  */
+#[AsCommand(name: 'post-apply')]
 final class PostApplyCommand extends AutomaticUpdatesCommandBase {
 
   use StringTranslationTrait;
@@ -27,10 +29,6 @@ final class PostApplyCommand extends AutomaticUpdatesCommandBase {
     parent::configure();
 
     $this
-      ->setName('post-apply')
-      // This command is for internal use only and should never be called
-      // directly. We don't want it to show up in the application command list.
-      ->setHidden()
       ->addArgument('stage-id', InputArgument::REQUIRED);
   }
 
diff --git a/src/Commands/RunCommand.php b/src/Commands/RunCommand.php
index 29e8d8e..129a88a 100644
--- a/src/Commands/RunCommand.php
+++ b/src/Commands/RunCommand.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\automatic_updates\Commands;
 
+use Symfony\Component\Console\Attribute\AsCommand;
 use Drupal\automatic_updates\CronUpdateRunner;
 use Drupal\Core\Queue\QueueFactory;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -17,6 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface;
  *   at any time without warning. This command should not be called directly,
  *   and this class should not be used by external code.
  */
+#[AsCommand(name: 'run', description: 'Automatically updates Drupal core, if automatic updates are enabled (via the update settings form) and any updates are available.')]
 final class RunCommand extends AutomaticUpdatesCommandBase {
 
   use StringTranslationTrait;
@@ -26,12 +28,6 @@ final class RunCommand extends AutomaticUpdatesCommandBase {
    */
   protected function configure(): void {
     parent::configure();
-
-    $this->setName('run')
-      ->setDescription('Automatically updates Drupal core, if automatic updates are enabled (via the update settings form) and any updates are available.')
-      // For simplicity, we want people to invoke the `auto-update` command with
-      // no arguments, so don't show this command in the command list.
-      ->setHidden();
   }
 
   /**
diff --git a/src/Hook/AutomaticUpdatesHooks1.php b/src/Hook/AutomaticUpdatesHooks1.php
new file mode 100644
index 0000000..26285dd
--- /dev/null
+++ b/src/Hook/AutomaticUpdatesHooks1.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\automatic_updates\Hook;
+
+use Drupal\automatic_updates\CronUpdateRunner;
+use Drupal\automatic_updates\ReleaseChooser;
+use Drupal\automatic_updates\UpdateSandboxManager;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Utility\Error;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for automatic_updates.
+ */
+class AutomaticUpdatesHooks1
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_form_FORM_ID_alter() for update_settings.
+     */
+    #[Hook('form_update_settings_alter')]
+    public function formUpdateSettingsAlter(array &$form): void
+    {
+        $config = \Drupal::config('automatic_updates.settings');
+        $form['unattended_level'] = [
+            '#type' => 'radios',
+            '#title' => $this->t('Unattended background updates'),
+            '#options' => [
+                \Drupal\automatic_updates\CronUpdateRunner::DISABLED => $this->t('Disabled'),
+                \Drupal\automatic_updates\CronUpdateRunner::SECURITY => $this->t('Security updates only'),
+                \Drupal\automatic_updates\CronUpdateRunner::ALL => $this->t('All patch releases'),
+            ],
+            '#default_value' => $config->get('unattended.level'),
+            '#description' => $this->t('When background updates are applied, your site will be briefly put into maintenance mode.'),
+        ];
+        $form['unattended_method'] = [
+            '#type' => 'radios',
+            '#title' => $this->t('How unattended updates should be run'),
+            '#options' => [
+                'web' => $this->t('By using the Automated Cron module or a request to /system/cron'),
+                'console' => $this->t('By the <code>auto-update</code> command-line utility'),
+            ],
+            '#default_value' => $config->get('unattended.method'),
+            '#states' => [
+                'invisible' => [
+                    'input[name="unattended_level"]' => [
+                        'value' => \Drupal\automatic_updates\CronUpdateRunner::DISABLED,
+                    ],
+                ],
+            ],
+            '#description' => $this->t('To use the <code>/system/cron</code> method <a href="http://drupal.org/docs/user_guide/en/security-cron.html">ensure cron is set up correctly</a>.'),
+        ];
+        $form['#submit'][] = '_automatic_updates_submit_update_settings';
+    }
+    /**
+     * Implements hook_preprocess_update_project_status().
+     */
+    #[Hook('preprocess_update_project_status')]
+    public function preprocessUpdateProjectStatus(array &$variables)
+    {
+        $project =& $variables['project'];
+        if ($project['name'] !== 'drupal') {
+            return;
+        }
+        $stage = \Drupal::service(\Drupal\automatic_updates\UpdateSandboxManager::class);
+        $supported_target_versions = [
+        ];
+        /** @var \Drupal\automatic_updates\ReleaseChooser $recommender */
+        $recommender = \Drupal::service(\Drupal\automatic_updates\ReleaseChooser::class);
+        try {
+            if ($installed_minor_release = $recommender->getLatestInInstalledMinor($stage)) {
+                $supported_target_versions[] = $installed_minor_release->getVersion();
+            }
+            if ($next_minor_release = $recommender->getLatestInNextMinor($stage)) {
+                $supported_target_versions[] = $next_minor_release->getVersion();
+            }
+        } catch (\RuntimeException $exception) {
+            // If for some reason we are not able to get the update recommendations
+            // do not alter the report.
+            \Drupal\Core\Utility\Error::logException(\Drupal::logger('automatic_updates'), $exception);
+            return;
+        }
+        $variables['#attached']['library'][] = 'automatic_updates/update_status';
+        $update_form_url = \Drupal\Core\Url::fromRoute('automatic_updates.update_form')->setAbsolute()->toString();
+        $status =& $variables['status'];
+        if ($supported_target_versions && $status['label']) {
+            $status['label'] = [
+                '#markup' => $this->t('@label <a href=":update-form">Update now</a>', [
+                    '@label' => $status['label'],
+                    ':update-form' => $update_form_url,
+                ]),
+            ];
+        }
+        // BEGIN: DELETE FROM CORE MERGE REQUEST
+        if (empty($variables['versions'])) {
+            return;
+        }
+        foreach ($variables['versions'] as &$themed_version) {
+            $version_info =& $themed_version['#version'];
+            if ($supported_target_versions && in_array($version_info['version'], $supported_target_versions, TRUE)) {
+                $version_info['download_link'] = $update_form_url;
+            } else {
+                // If this version will not be displayed as an option on this module's
+                // update form replace the link to download the archive file with the
+                // release notes link. The release notes page will provide Composer
+                // instructions. While this isn't a perfect solution the Update module
+                // Twig templates do not check if 'download_link' is set, so we cannot
+                // unset it here.
+                $themed_version['#attributes']['class'][] = 'automatic-updates-unsupported-version';
+                $version_info['download_link'] = $version_info['release_link'];
+            }
+        }
+        // END: DELETE FROM CORE MERGE REQUEST
+    }
+}
diff --git a/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.module b/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.module
index 4c23695..9bc9dd2 100644
--- a/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.module
+++ b/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.module
@@ -7,11 +7,13 @@
 
 declare(strict_types=1);
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\automatic_updates_extensions_test\Hook\AutomaticUpdatesExtensionsTestHooks;
+
 /**
  * Implements hook_update_status_alter().
  */
+#[LegacyHook]
 function automatic_updates_extensions_test_update_status_alter(&$projects) {
-  if (\Drupal::state()->get('testUninstallableRelease') && !empty($projects['semver_test'])) {
-    $projects['semver_test']['recommended'] = $projects['semver_test']['existing_version'];
-  }
+  \Drupal::service(AutomaticUpdatesExtensionsTestHooks::class)->updateStatusAlter($projects);
 }
diff --git a/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.services.yml b/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.services.yml
new file mode 100644
index 0000000..d4b8b09
--- /dev/null
+++ b/tests/modules/automatic_updates_extensions_test/automatic_updates_extensions_test.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\automatic_updates_extensions_test\Hook\AutomaticUpdatesExtensionsTestHooks:
+    class: Drupal\automatic_updates_extensions_test\Hook\AutomaticUpdatesExtensionsTestHooks
+    autowire: true
diff --git a/tests/modules/automatic_updates_extensions_test/src/Hook/AutomaticUpdatesExtensionsTestHooks.php b/tests/modules/automatic_updates_extensions_test/src/Hook/AutomaticUpdatesExtensionsTestHooks.php
new file mode 100644
index 0000000..b8d7098
--- /dev/null
+++ b/tests/modules/automatic_updates_extensions_test/src/Hook/AutomaticUpdatesExtensionsTestHooks.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\automatic_updates_extensions_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for automatic_updates_extensions_test.
+ */
+class AutomaticUpdatesExtensionsTestHooks
+{
+    /**
+     * Implements hook_update_status_alter().
+     */
+    #[Hook('update_status_alter')]
+    public static function updateStatusAlter(&$projects)
+    {
+        if (\Drupal::state()->get('testUninstallableRelease') && !empty($projects['semver_test'])) {
+            $projects['semver_test']['recommended'] = $projects['semver_test']['existing_version'];
+        }
+    }
+}
diff --git a/tests/modules/automatic_updates_test/automatic_updates_test.module b/tests/modules/automatic_updates_test/automatic_updates_test.module
index 12b63ec..963ab22 100644
--- a/tests/modules/automatic_updates_test/automatic_updates_test.module
+++ b/tests/modules/automatic_updates_test/automatic_updates_test.module
@@ -7,25 +7,14 @@
 
 declare(strict_types=1);
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\automatic_updates_test\Hook\AutomaticUpdatesTestHooks;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 
 /**
  * Implements hook_mail_alter().
  */
+#[LegacyHook]
 function automatic_updates_test_mail_alter(array &$message): void {
-  if (str_starts_with($message['id'], 'automatic_updates_')) {
-    $line_langcodes = [];
-
-    // Get the langcode of every translated line in the message, including the
-    // subject line.
-    $lines = array_merge($message['body'], [
-      $message['subject'],
-    ]);
-    foreach ($lines as $line) {
-      if ($line instanceof TranslatableMarkup) {
-        $line_langcodes[] = $line->getOption('langcode');
-      }
-    }
-    $message['line_langcodes'] = array_unique($line_langcodes);
-  }
+  \Drupal::service(AutomaticUpdatesTestHooks::class)->mailAlter($message);
 }
diff --git a/tests/modules/automatic_updates_test/automatic_updates_test.services.yml b/tests/modules/automatic_updates_test/automatic_updates_test.services.yml
index f3ade6a..6f1256c 100644
--- a/tests/modules/automatic_updates_test/automatic_updates_test.services.yml
+++ b/tests/modules/automatic_updates_test/automatic_updates_test.services.yml
@@ -13,3 +13,7 @@ services:
     class: Drupal\automatic_updates_test\Datetime\TestTime
     decorates: datetime.time
     arguments: ['@automatic_updates_test.time.inner','@request_stack']
+
+  Drupal\automatic_updates_test\Hook\AutomaticUpdatesTestHooks:
+    class: Drupal\automatic_updates_test\Hook\AutomaticUpdatesTestHooks
+    autowire: true
diff --git a/tests/modules/automatic_updates_test/src/Hook/AutomaticUpdatesTestHooks.php b/tests/modules/automatic_updates_test/src/Hook/AutomaticUpdatesTestHooks.php
new file mode 100644
index 0000000..13386d7
--- /dev/null
+++ b/tests/modules/automatic_updates_test/src/Hook/AutomaticUpdatesTestHooks.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\automatic_updates_test\Hook;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for automatic_updates_test.
+ */
+class AutomaticUpdatesTestHooks
+{
+    /**
+     * Implements hook_mail_alter().
+     */
+    #[Hook('mail_alter')]
+    public static function mailAlter(array &$message): void
+    {
+        if (str_starts_with($message['id'], 'automatic_updates_')) {
+            $line_langcodes = [
+            ];
+            // Get the langcode of every translated line in the message, including the
+            // subject line.
+            $lines = array_merge($message['body'], [
+                $message['subject'],
+            ]);
+            foreach ($lines as $line) {
+                if ($line instanceof \Drupal\Core\StringTranslation\TranslatableMarkup) {
+                    $line_langcodes[] = $line->getOption('langcode');
+                }
+            }
+            $message['line_langcodes'] = array_unique($line_langcodes);
+        }
+    }
+}
diff --git a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
index c43419c..8a0f11e 100644
--- a/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
+++ b/tests/src/Functional/AutomaticUpdatesFunctionalTestBase.php
@@ -128,7 +128,7 @@ abstract class AutomaticUpdatesFunctionalTestBase extends BrowserTestBase {
         // @see drupal_valid_test_ua()
         'HTTP_USER_AGENT' => $this->getSession()->getCookie('SIMPLETEST_USER_AGENT'),
       ])
-      ->setWorkingDirectory($this->getDrupalRoot())
+      ->setWorkingDirectory($this->root)
       ->mustRun();
   }
 
