diff --git a/docs/source/reference/command_line.rst b/docs/source/reference/command_line.rst
index bf4d668928..1d7287e40c 100644
--- a/docs/source/reference/command_line.rst
+++ b/docs/source/reference/command_line.rst
@@ -80,7 +80,7 @@ Below is a list with all available subcommands.
       list       List the available codes.
       relabel    Relabel a code.
       reveal     Reveal one or more hidden codes in `verdi code list`.
-      setup      Setup a new code.
+      setup      (Deprecated) Setup a new code (use `verdi code create`).
       show       Display detailed information for a code.
       test       Run tests for the given code to check whether it is usable.
 
@@ -531,7 +531,7 @@ Below is a list with all available subcommands.
 
     Usage:  [OPTIONS]
 
-      Setup a new profile.
+      (Deprecated) Setup a new profile (use `verdi profile setup`).
 
       This method assumes that an empty PSQL database has been created and that the database
       user has been created.
diff --git a/src/aiida/cmdline/commands/cmd_code.py b/src/aiida/cmdline/commands/cmd_code.py
index 5b7b610a01..477d2f61ab 100644
--- a/src/aiida/cmdline/commands/cmd_code.py
+++ b/src/aiida/cmdline/commands/cmd_code.py
@@ -18,7 +18,7 @@
 from aiida.cmdline.params import arguments, options, types
 from aiida.cmdline.params.options.commands import code as options_code
 from aiida.cmdline.utils import echo, echo_tabulate
-from aiida.cmdline.utils.decorators import deprecated_command, with_dbenv
+from aiida.cmdline.utils.decorators import with_dbenv
 from aiida.common import exceptions
 
 
@@ -89,7 +89,7 @@ def set_code_builder(ctx, param, value):
 # because the ``LABEL`` option has a callback that relies on the computer being already set. Execution order is
 # guaranteed only for the interactive case, however. For the non-interactive case, the callback is called explicitly
 # once more in the command body to cover the case when the label is specified before the computer.
-@verdi_code.command('setup')
+@verdi_code.command('setup', deprecated='Please use `verdi code create` instead.')
 @options_code.ON_COMPUTER()
 @options_code.COMPUTER()
 @options_code.LABEL()
@@ -105,9 +105,8 @@ def set_code_builder(ctx, param, value):
 @options.CONFIG_FILE()
 @click.pass_context
 @with_dbenv()
-@deprecated_command('This command will be removed soon, use `verdi code create` instead.')
 def setup_code(ctx, non_interactive, **kwargs):
-    """Setup a new code."""
+    """Setup a new code (use `verdi code create`)."""
     from aiida.orm.utils.builders.code import CodeBuilder
 
     options_code.validate_label_uniqueness(ctx, None, kwargs['label'])
diff --git a/src/aiida/cmdline/commands/cmd_setup.py b/src/aiida/cmdline/commands/cmd_setup.py
index 2f76d56434..0c18ce4238 100644
--- a/src/aiida/cmdline/commands/cmd_setup.py
+++ b/src/aiida/cmdline/commands/cmd_setup.py
@@ -17,8 +17,7 @@
 from aiida.manage.configuration import Profile, load_profile
 
 
-@verdi.command('setup')
-@decorators.deprecated_command('This command is deprecated, use `verdi profile setup core.psql_dos` instead.')
+@verdi.command('setup', deprecated='Please use `verdi profile setup` instead.')
 @options.NON_INTERACTIVE()
 @options_setup.SETUP_PROFILE()
 @options_setup.SETUP_USER_EMAIL()
@@ -68,7 +67,7 @@ def setup(
     test_profile,
     profile_uuid,
 ):
-    """Setup a new profile.
+    """Setup a new profile (use `verdi profile setup`).
 
     This method assumes that an empty PSQL database has been created and that the database user has been created.
     """
diff --git a/src/aiida/cmdline/groups/verdi.py b/src/aiida/cmdline/groups/verdi.py
index a3c40e50b8..e156b8413d 100644
--- a/src/aiida/cmdline/groups/verdi.py
+++ b/src/aiida/cmdline/groups/verdi.py
@@ -9,6 +9,7 @@
 
 import click
 
+from aiida.cmdline.utils.echo import echo_deprecated
 from aiida.common.exceptions import ConfigurationError
 from aiida.common.extendeddicts import AttributeDict
 from aiida.manage.configuration import get_config
@@ -79,6 +80,43 @@ def __init__(self, *args, **kwargs):
             self.obj = LazyVerdiObjAttributeDict(self)
 
 
+class VerdiCommand(click.Command):
+    """Custom command implementation to customize the logic of printing deprecation messages.
+
+    If a command is deprecated, the :class:`click.Command` adds a deprecation marker in the short help and the full
+    help text, and prints a deprecation warning when the command is invoked. The problem is that the deprecation warning
+    is printed after the prompting for parameters, which for interactive commands mean the deprecation warning comes too
+    late, when the user has already provided all prompts.
+
+    Here, the :meth:`click.Command.parse_args` method is overridden, which is called before the interactive options
+    start to prompt, such that the deprecation warning can be printed. The :meth:`click.Command.invoke` method is also
+    overridden in order to skip the printing of the deprecation message handled by ``click`` as that would result in
+    the deprecation message being printed twice.
+    """
+
+    def parse_args(self, ctx: click.Context, args: t.List[str]) -> t.List[str]:
+        """Given a context and a list of arguments this creates the parser and parses the arguments.
+
+        Then context is modified as necessary.
+
+        This is automatically invoked by :meth:`click.BaseCommand.make_context`.
+        """
+        if self.deprecated:
+            # We are abusing click.Command `deprecated` member variable by using a
+            # string instead of a bool to also use it as optional deprecated message
+            echo_deprecated(
+                self.deprecated
+                if isinstance(self.deprecated, str)  # type: ignore[redundant-expr]
+                else 'This command is deprecated.'
+            )
+
+        return super().parse_args(ctx, args)
+
+    def invoke(self, ctx: click.Context) -> t.Any:
+        if self.callback is not None:
+            return ctx.invoke(self.callback, **ctx.params)
+
+
 class VerdiCommandGroup(click.Group):
     """Subclass of :class:`click.Group` for the ``verdi`` CLI.
 
@@ -87,6 +125,7 @@ class VerdiCommandGroup(click.Group):
     """
 
     context_class = VerdiContext
+    command_class = VerdiCommand
 
     @staticmethod
     def add_verbosity_option(cmd: click.Command) -> click.Command:
diff --git a/tests/cmdline/commands/test_code.py b/tests/cmdline/commands/test_code.py
index a36a7bb28a..8aeeb5cec8 100644
--- a/tests/cmdline/commands/test_code.py
+++ b/tests/cmdline/commands/test_code.py
@@ -46,6 +46,21 @@ def test_help(run_cli_command):
     run_cli_command(cmd_code.setup_code, ['--help'])
 
 
+def test_code_setup_deprecation(run_cli_command):
+    """Checks if a deprecation warning is printed in stdout and stderr."""
+    # Checks if the deprecation warning is present when invoking the help page
+    result = run_cli_command(cmd_code.setup_code, ['--help'])
+    assert 'Deprecated:' in result.output
+    assert 'Deprecated:' in result.stderr
+
+    # Checks if the deprecation warning is present when invoking the command
+    # Runs setup in interactive mode and sends Ctrl+D (\x04) as input so we exit the prompts.
+    # This way we can check if the deprecated message was printed with the first prompt.
+    result = run_cli_command(cmd_code.setup_code, user_input='\x04', use_subprocess=True, raises=True)
+    assert 'Deprecated:' in result.output
+    assert 'Deprecated:' in result.stderr
+
+
 @pytest.mark.usefixtures('aiida_profile_clean')
 def test_code_list_no_codes_error_message(run_cli_command):
     """Test ``verdi code list`` when no codes exist."""
diff --git a/tests/cmdline/commands/test_setup.py b/tests/cmdline/commands/test_setup.py
index 9a12e3fb88..6c7a668542 100644
--- a/tests/cmdline/commands/test_setup.py
+++ b/tests/cmdline/commands/test_setup.py
@@ -40,6 +40,20 @@ def init_profile(self, pg_test_cluster, empty_config, run_cli_command):
         self.pg_test = pg_test_cluster
         self.cli_runner = run_cli_command
 
+    def test_setup_deprecation(self):
+        """Checks if a deprecation warning is printed in stdout and stderr."""
+        # Checks if the deprecation warning is present when invoking the help page
+        result = self.cli_runner(cmd_setup.setup, ['--help'])
+        assert 'Deprecated:' in result.output
+        assert 'Deprecated:' in result.stderr
+
+        # Checks if the deprecation warning is present when invoking the command
+        # Runs setup in interactive mode and sends Ctrl+D (\x04) as input so we exit the prompts.
+        # This way we can check if the deprecation warning was printed with the first prompt.
+        result = self.cli_runner(cmd_setup.setup, user_input='\x04', use_subprocess=True, raises=True)
+        assert 'Deprecated:' in result.output
+        assert 'Deprecated:' in result.stderr
+
     def test_help(self):
         """Check that the `--help` option is eager, is not overruled and will properly display the help message.