diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7744e3745..ce7eac2e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
   warnings described at https://github.com/certbot/josepy/issues/13.
 * Apache plugin now respects CERTBOT_DOCS environment variable when adding
   command line defaults.
+* The running of manual plugin hooks is now always included in Certbot's log
+  output.
 * Tests execution for certbot, certbot-apache and certbot-nginx packages now relies on pytest.
 
 ### Fixed
diff --git a/certbot/hooks.py b/certbot/hooks.py
index d5239a437..7d2e42fcd 100644
--- a/certbot/hooks.py
+++ b/certbot/hooks.py
@@ -93,8 +93,7 @@ def _run_pre_hook_if_necessary(command):
     if command in executed_pre_hooks:
         logger.info("Pre-hook command already run, skipping: %s", command)
     else:
-        logger.info("Running pre-hook command: %s", command)
-        _run_hook(command)
+        _run_hook("pre-hook", command)
         executed_pre_hooks.add(command)
 
 
@@ -126,8 +125,7 @@ def post_hook(config):
             _run_eventually(cmd)
     # certonly / run
     elif cmd:
-        logger.info("Running post-hook command: %s", cmd)
-        _run_hook(cmd)
+        _run_hook("post-hook", cmd)
 
 
 post_hooks = []  # type: List[str]
@@ -149,8 +147,7 @@ def _run_eventually(command):
 def run_saved_post_hooks():
     """Run any post hooks that were saved up in the course of the 'renew' verb"""
     for cmd in post_hooks:
-        logger.info("Running post-hook command: %s", cmd)
-        _run_hook(cmd)
+        _run_hook("post-hook", cmd)
 
 
 def deploy_hook(config, domains, lineage_path):
@@ -220,23 +217,30 @@ def _run_deploy_hook(command, domains, lineage_path, dry_run):
 
     os.environ["RENEWED_DOMAINS"] = " ".join(domains)
     os.environ["RENEWED_LINEAGE"] = lineage_path
-    logger.info("Running deploy-hook command: %s", command)
-    _run_hook(command)
+    _run_hook("deploy-hook", command)
 
 
-def _run_hook(shell_cmd):
+def _run_hook(cmd_name, shell_cmd):
     """Run a hook command.
 
-    :returns: stderr if there was any"""
+    :param str cmd_name: the user facing name of the hook being run
+    :param shell_cmd: shell command to execute
+    :type shell_cmd: `list` of `str` or `str`
 
-    err, _ = execute(shell_cmd)
+    :returns: stderr if there was any"""
+    err, _ = execute(cmd_name, shell_cmd)
     return err
 
 
-def execute(shell_cmd):
+def execute(cmd_name, shell_cmd):
     """Run a command.
 
+    :param str cmd_name: the user facing name of the hook being run
+    :param shell_cmd: shell command to execute
+    :type shell_cmd: `list` of `str` or `str`
+
     :returns: `tuple` (`str` stderr, `str` stdout)"""
+    logger.info("Running %s command: %s", cmd_name, shell_cmd)
 
     # universal_newlines causes Popen.communicate()
     # to return str objects instead of bytes in Python 3
@@ -245,12 +249,12 @@ def execute(shell_cmd):
     out, err = cmd.communicate()
     base_cmd = os.path.basename(shell_cmd.split(None, 1)[0])
     if out:
-        logger.info('Output from %s:\n%s', base_cmd, out)
+        logger.info('Output from %s command %s:\n%s', cmd_name, base_cmd, out)
     if cmd.returncode != 0:
-        logger.error('Hook command "%s" returned error code %d',
-                     shell_cmd, cmd.returncode)
+        logger.error('%s command "%s" returned error code %d',
+                     cmd_name, shell_cmd, cmd.returncode)
     if err:
-        logger.error('Error output from %s:\n%s', base_cmd, err)
+        logger.error('Error output from %s command %s:\n%s', cmd_name, base_cmd, err)
     return (err, out)
 
 
diff --git a/certbot/plugins/manual.py b/certbot/plugins/manual.py
index 8723a1c62..123a5bfea 100644
--- a/certbot/plugins/manual.py
+++ b/certbot/plugins/manual.py
@@ -202,7 +202,7 @@ permitted by DNS standards.)
             os.environ.pop('CERTBOT_KEY_PATH', None)
             os.environ.pop('CERTBOT_SNI_DOMAIN', None)
         os.environ.update(env)
-        _, out = hooks.execute(self.conf('auth-hook'))
+        _, out = self._execute_hook('auth-hook')
         env['CERTBOT_AUTH_OUTPUT'] = out.strip()
         self.env[achall] = env
 
@@ -243,5 +243,8 @@ permitted by DNS standards.)
                 if 'CERTBOT_TOKEN' not in env:
                     os.environ.pop('CERTBOT_TOKEN', None)
                 os.environ.update(env)
-                hooks.execute(self.conf('cleanup-hook'))
+                self._execute_hook('cleanup-hook')
         self.reverter.recovery_routine()
+
+    def _execute_hook(self, hook_name):
+        return hooks.execute(self.option_name(hook_name), self.conf(hook_name))

Log doesn't mention hook scripts being called
## My operating system is (include version):
Debian unstable

## I installed Certbot with (certbot-auto, OS package manager, pip, etc):
certbot 0.11.1 from Debian unstable

## I ran this command and it produced this output:
certbot renew with lots of --verbose... and it produces tons of output

## Certbot's behavior differed from what I expected because:
I expected it to log when/where it invokes my manual-auth and manual-cleanup scripts.  It didn't.

## Here is a Certbot log showing the issue (if available):
###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.

Here is nothing because the problem is that it *DOESNT* show what I want it to

## Here is the relevant nginx server block or Apache virtualhost for the domain I am configuring:
Irrelevant since I'm doing "manual" (scripted) DNS auth... it ends up used by lighttpd, but that's irrelevant since the scripts don't depend on lighttpd.

I'm trying to find out (docs are mumm about this) when auth/cleanup scripts get called for a cert WITH MULTIPLE NAMES.  Namely, jaerhard.com and www.jaerhard.com.  And it looks (but the log doesn't actually have that info hence this PR) like it calls manual auth separately for both names, then does the online part for one of the names, then does the cleanup, and then tries the other names.  Both names are in the zone file at one point (I have the script copy the zone file), so both auths should succeed.  But one doesn't.

Again, I need enough info to even debug this.
diff --git a/certbot/tests/hook_test.py b/certbot/tests/hook_test.py
index f5bb0c8b5..90f639958 100644
--- a/certbot/tests/hook_test.py
+++ b/certbot/tests/hook_test.py
@@ -121,7 +121,7 @@ class PreHookTest(HookTest):
 
     def _test_nonrenew_common(self):
         mock_execute = self._call_with_mock_execute(self.config)
-        mock_execute.assert_called_once_with(self.config.pre_hook)
+        mock_execute.assert_called_once_with("pre-hook", self.config.pre_hook)
         self._test_no_executions_common()
 
     def test_no_hooks(self):
@@ -137,21 +137,21 @@ class PreHookTest(HookTest):
     def test_renew_disabled_dir_hooks(self):
         self.config.directory_hooks = False
         mock_execute = self._call_with_mock_execute(self.config)
-        mock_execute.assert_called_once_with(self.config.pre_hook)
+        mock_execute.assert_called_once_with("pre-hook", self.config.pre_hook)
         self._test_no_executions_common()
 
     def test_renew_no_overlap(self):
         self.config.verb = "renew"
         mock_execute = self._call_with_mock_execute(self.config)
-        mock_execute.assert_any_call(self.dir_hook)
-        mock_execute.assert_called_with(self.config.pre_hook)
+        mock_execute.assert_any_call("pre-hook", self.dir_hook)
+        mock_execute.assert_called_with("pre-hook", self.config.pre_hook)
         self._test_no_executions_common()
 
     def test_renew_with_overlap(self):
         self.config.pre_hook = self.dir_hook
         self.config.verb = "renew"
         mock_execute = self._call_with_mock_execute(self.config)
-        mock_execute.assert_called_once_with(self.dir_hook)
+        mock_execute.assert_called_once_with("pre-hook", self.dir_hook)
         self._test_no_executions_common()
 
     def _test_no_executions_common(self):
@@ -193,7 +193,7 @@ class PostHookTest(HookTest):
         for verb in ("certonly", "run",):
             self.config.verb = verb
             mock_execute = self._call_with_mock_execute(self.config)
-            mock_execute.assert_called_once_with(self.config.post_hook)
+            mock_execute.assert_called_once_with("post-hook", self.config.post_hook)
             self.assertFalse(self._get_eventually())
 
     def test_cert_only_and_run_without_hook(self):
@@ -277,12 +277,12 @@ class RunSavedPostHooksTest(HookTest):
 
         calls = mock_execute.call_args_list
         for actual_call, expected_arg in zip(calls, self.eventually):
-            self.assertEqual(actual_call[0][0], expected_arg)
+            self.assertEqual(actual_call[0][1], expected_arg)
 
     def test_single(self):
         self.eventually = ["foo"]
         mock_execute = self._call_with_mock_execute_and_eventually()
-        mock_execute.assert_called_once_with(self.eventually[0])
+        mock_execute.assert_called_once_with("post-hook", self.eventually[0])
 
 
 class RenewalHookTest(HookTest):
@@ -360,7 +360,7 @@ class DeployHookTest(RenewalHookTest):
         self.config.deploy_hook = "foo"
         mock_execute = self._call_with_mock_execute(
             self.config, domains, lineage)
-        mock_execute.assert_called_once_with(self.config.deploy_hook)
+        mock_execute.assert_called_once_with("deploy-hook", self.config.deploy_hook)
 
 
 class RenewHookTest(RenewalHookTest):
@@ -384,7 +384,7 @@ class RenewHookTest(RenewalHookTest):
         self.config.directory_hooks = False
         mock_execute = self._call_with_mock_execute(
             self.config, ["example.org"], "/foo/bar")
-        mock_execute.assert_called_once_with(self.config.renew_hook)
+        mock_execute.assert_called_once_with("deploy-hook", self.config.renew_hook)
 
     @mock.patch("certbot.hooks.logger")
     def test_dry_run(self, mock_logger):
@@ -408,13 +408,13 @@ class RenewHookTest(RenewalHookTest):
         self.config.renew_hook = self.dir_hook
         mock_execute = self._call_with_mock_execute(
             self.config, ["example.net", "example.org"], "/foo/bar")
-        mock_execute.assert_called_once_with(self.dir_hook)
+        mock_execute.assert_called_once_with("deploy-hook", self.dir_hook)
 
     def test_no_overlap(self):
         mock_execute = self._call_with_mock_execute(
             self.config, ["example.org"], "/foo/bar")
-        mock_execute.assert_any_call(self.dir_hook)
-        mock_execute.assert_called_with(self.config.renew_hook)
+        mock_execute.assert_any_call("deploy-hook", self.dir_hook)
+        mock_execute.assert_called_with("deploy-hook", self.config.renew_hook)
 
 
 class ExecuteTest(unittest.TestCase):
@@ -433,18 +433,22 @@ class ExecuteTest(unittest.TestCase):
 
     def _test_common(self, returncode, stdout, stderr):
         given_command = "foo"
+        given_name = "foo-hook"
         with mock.patch("certbot.hooks.Popen") as mock_popen:
             mock_popen.return_value.communicate.return_value = (stdout, stderr)
             mock_popen.return_value.returncode = returncode
             with mock.patch("certbot.hooks.logger") as mock_logger:
-                self.assertEqual(self._call(given_command), (stderr, stdout))
+                self.assertEqual(self._call(given_name, given_command), (stderr, stdout))
 
         executed_command = mock_popen.call_args[1].get(
             "args", mock_popen.call_args[0][0])
         self.assertEqual(executed_command, given_command)
 
+        mock_logger.info.assert_any_call("Running %s command: %s",
+                                         given_name, given_command)
         if stdout:
-            self.assertTrue(mock_logger.info.called)
+            mock_logger.info.assert_any_call(mock.ANY, mock.ANY,
+                                             mock.ANY, stdout)
         if stderr or returncode:
             self.assertTrue(mock_logger.error.called)
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist
apt-get update apt-get install -y gcc libaugeas0
3.9
local-oldest-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
acme==0.29.0
astroid==1.3.5
asttokens==3.0.0
backports.tarfile==1.2.0
cachetools==5.5.2
-e git+https://github.com/certbot/certbot.git@b10ceb7d907cdcc36ec2d745e7802127919ec043#egg=certbot
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.1
colorama==0.4.6
ConfigArgParse==1.7
configobj==5.0.9
coverage==7.8.0
cryptography==44.0.2
decorator==5.2.1
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
execnet==2.1.1
executing==2.2.0
filelock==3.18.0
id==1.5.0
idna==3.10
importlib-metadata==6.11.0
iniconfig==2.1.0
ipdb==0.13.13
ipython==8.18.1
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jedi==0.19.2
jeepney==0.9.0
josepy==2.0.0
keyring==25.6.0
logilab-common==2.1.0
markdown-it-py==3.0.0
matplotlib-inline==0.1.7
mdurl==0.1.2
mock==5.2.0
more-itertools==10.6.0
mypy-extensions==1.0.0
nh3==0.2.21
packaging==24.2
parsedatetime==2.6
parso==0.8.4
pexpect==4.9.0
platformdirs==4.3.7
pluggy==1.5.0
prompt_toolkit==3.0.50
ptyprocess==0.7.0
pure_eval==0.2.3
pycparser==2.22
Pygments==2.19.1
pylint==1.4.2
pyOpenSSL==25.0.0
pyproject-api==1.9.0
pyRFC3339==2.0.1
pytest==8.3.5
pytest-cov==6.0.0
pytest-xdist==3.6.1
pytz==2025.2
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
SecretStorage==3.3.3
six==1.17.0
stack-data==0.6.3
tomli==2.2.1
tox==4.25.0
traitlets==5.14.3
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
wcwidth==0.2.13
zipp==3.21.0
zope.component==6.0
zope.event==5.0
zope.hookable==7.0
zope.interface==7.2

name: certbot
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - acme==0.29.0
      - astroid==1.3.5
      - asttokens==3.0.0
      - backports-tarfile==1.2.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - colorama==0.4.6
      - configargparse==1.7
      - configobj==5.0.9
      - coverage==7.8.0
      - cryptography==44.0.2
      - decorator==5.2.1
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - executing==2.2.0
      - filelock==3.18.0
      - id==1.5.0
      - idna==3.10
      - importlib-metadata==6.11.0
      - iniconfig==2.1.0
      - ipdb==0.13.13
      - ipython==8.18.1
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jedi==0.19.2
      - jeepney==0.9.0
      - josepy==2.0.0
      - keyring==25.6.0
      - logilab-common==2.1.0
      - markdown-it-py==3.0.0
      - matplotlib-inline==0.1.7
      - mdurl==0.1.2
      - mock==5.2.0
      - more-itertools==10.6.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - packaging==24.2
      - parsedatetime==2.6
      - parso==0.8.4
      - pexpect==4.9.0
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - prompt-toolkit==3.0.50
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - pycparser==2.22
      - pygments==2.19.1
      - pylint==1.4.2
      - pyopenssl==25.0.0
      - pyproject-api==1.9.0
      - pyrfc3339==2.0.1
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-xdist==3.6.1
      - pytz==2025.2
      - readme-renderer==44.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - six==1.17.0
      - stack-data==0.6.3
      - tomli==2.2.1
      - tox==4.25.0
      - traitlets==5.14.3
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - wcwidth==0.2.13
      - zipp==3.21.0
      - zope-component==6.0
      - zope-event==5.0
      - zope-hookable==7.0
      - zope-interface==7.2
prefix: /opt/conda/envs/certbot

certbot/tests/hook_test.py::PreHookTest::test_certonly certbot/tests/hook_test.py::PreHookTest::test_renew_disabled_dir_hooks certbot/tests/hook_test.py::PreHookTest::test_renew_no_overlap certbot/tests/hook_test.py::PreHookTest::test_renew_with_overlap certbot/tests/hook_test.py::PreHookTest::test_run certbot/tests/hook_test.py::PostHookTest::test_certonly_and_run_with_hook certbot/tests/hook_test.py::RunSavedPostHooksTest::test_multiple certbot/tests/hook_test.py::RunSavedPostHooksTest::test_single certbot/tests/hook_test.py::DeployHookTest::test_success certbot/tests/hook_test.py::RenewHookTest::test_disabled_dir_hooks certbot/tests/hook_test.py::RenewHookTest::test_no_overlap certbot/tests/hook_test.py::RenewHookTest::test_overlap certbot/tests/hook_test.py::ExecuteTest::test_it
certbot/tests/hook_test.py::ValidateHooksTest::test_it certbot/tests/hook_test.py::ValidateHookTest::test_not_executable certbot/tests/hook_test.py::ValidateHookTest::test_not_found certbot/tests/hook_test.py::ValidateHookTest::test_unset certbot/tests/hook_test.py::PreHookTest::test_no_hooks certbot/tests/hook_test.py::PostHookTest::test_cert_only_and_run_without_hook certbot/tests/hook_test.py::PostHookTest::test_renew_disabled_dir_hooks certbot/tests/hook_test.py::PostHookTest::test_renew_no_config_hook certbot/tests/hook_test.py::PostHookTest::test_renew_no_dir_hook certbot/tests/hook_test.py::PostHookTest::test_renew_no_hooks certbot/tests/hook_test.py::PostHookTest::test_renew_no_overlap certbot/tests/hook_test.py::PostHookTest::test_renew_with_overlap certbot/tests/hook_test.py::RunSavedPostHooksTest::test_empty certbot/tests/hook_test.py::DeployHookTest::test_dry_run certbot/tests/hook_test.py::DeployHookTest::test_no_hook certbot/tests/hook_test.py::RenewHookTest::test_dry_run certbot/tests/hook_test.py::RenewHookTest::test_no_hooks certbot/tests/hook_test.py::ListHooksTest::test_empty certbot/tests/hook_test.py::ListHooksTest::test_multiple certbot/tests/hook_test.py::ListHooksTest::test_single
certbot/plugins/manual.py CHANGELOG.md certbot/hooks.py
certbot/plugins/manual.py CHANGELOG.md certbot/hooks.py