diff --git a/reframe/core/builtins.py b/reframe/core/builtins.py
index 8d4eb599..6f32c3a7 100644
--- a/reframe/core/builtins.py
+++ b/reframe/core/builtins.py
@@ -37,7 +37,7 @@ def final(fn):
 
 # Hook-related builtins
 
-def run_before(stage):
+def run_before(stage, *, always_last=False):
     '''Attach the decorated function before a certain pipeline stage.
 
     The function will run just before the specified pipeline stage and it
@@ -47,14 +47,25 @@ def run_before(stage):
 
     :param stage: The pipeline stage where this function will be attached to.
         See :ref:`pipeline-hooks` for the list of valid stage values.
+
+    :param always_last: Run this hook always as the last one of the stage. In
+        a whole test hierarchy, only a single hook can be explicitly pinned at
+        the end of the same-stage sequence of hooks. If another hook is
+        declared as ``always_last`` in the same stage, an error will be
+        issued.
+
+    .. versionchanged:: 4.4
+       The ``always_last`` argument was added.
+
     '''
-    return hooks.attach_to('pre_' + stage)
+
+    return hooks.attach_to('pre_' + stage, always_last)
 
 
-def run_after(stage):
+def run_after(stage, *, always_last=False):
     '''Attach the decorated function after a certain pipeline stage.
 
-    This is analogous to :func:`~RegressionMixin.run_before`, except that the
+    This is analogous to :func:`run_before`, except that the
     hook will execute right after the stage it was attached to. This decorator
     also supports ``'init'`` as a valid ``stage`` argument, where in this
     case, the hook will execute right after the test is initialized (i.e.
@@ -81,7 +92,7 @@ def run_after(stage):
        Add support for post-init hooks.
 
     '''
-    return hooks.attach_to('post_' + stage)
+    return hooks.attach_to('post_' + stage, always_last)
 
 
 require_deps = hooks.require_deps
diff --git a/reframe/core/hooks.py b/reframe/core/hooks.py
index 4fde8932..74fab55a 100644
--- a/reframe/core/hooks.py
+++ b/reframe/core/hooks.py
@@ -9,16 +9,16 @@ import inspect
 import reframe.utility as util
 
 
-def attach_to(phase):
+def attach_to(phase, always_last):
     '''Backend function to attach a hook to a given phase.
 
     :meta private:
     '''
     def deco(func):
         if hasattr(func, '_rfm_attach'):
-            func._rfm_attach.append(phase)
+            func._rfm_attach.append((phase, always_last))
         else:
-            func._rfm_attach = [phase]
+            func._rfm_attach = [(phase, always_last)]
 
         try:
             # no need to resolve dependencies independently; this function is
@@ -124,6 +124,7 @@ class Hook:
     @property
     def stages(self):
         return self._rfm_attach
+        # return [stage for stage, _ in self._rfm_attach]
 
     def __getattr__(self, attr):
         return getattr(self.__fn, attr)
@@ -179,7 +180,7 @@ class HookRegistry:
             self.__hooks.discard(h)
             self.__hooks.add(h)
         elif hasattr(v, '_rfm_resolve_deps'):
-            v._rfm_attach = ['post_setup']
+            v._rfm_attach = [('post_setup', None)]
             self.__hooks.add(Hook(v))
 
     def update(self, other, *, denied_hooks=None):
diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py
index ee72e4a0..ac08e2fc 100644
--- a/reframe/core/pipeline.py
+++ b/reframe/core/pipeline.py
@@ -179,12 +179,32 @@ class RegressionTest(RegressionMixin, jsonext.JSONSerializable):
     @classmethod
     def pipeline_hooks(cls):
         ret = {}
+        last = {}
         for hook in cls._rfm_hook_registry:
-            for stage in hook.stages:
-                try:
-                    ret[stage].append(hook.fn)
-                except KeyError:
-                    ret[stage] = [hook.fn]
+            for stage, always_last in hook.stages:
+                if always_last:
+                    if stage in last:
+                        hook_name = hook.__qualname__
+                        pinned_name = last[stage].__qualname__
+                        raise ReframeSyntaxError(
+                            f'cannot pin hook {hook_name!r} as last '
+                            f'of stage {stage!r} as {pinned_name!r} '
+                            f'is already pinned last'
+                        )
+
+                    last[stage] = hook
+                else:
+                    try:
+                        ret[stage].append(hook.fn)
+                    except KeyError:
+                        ret[stage] = [hook.fn]
+
+        # Append the last hooks
+        for stage, hook in last.items():
+            try:
+                ret[stage].append(hook.fn)
+            except KeyError:
+                ret[stage] = [hook.fn]
 
         return ret
 

diff --git a/docs/regression_test_api.rst b/docs/regression_test_api.rst
index a42dcd62..dc5695eb 100644
--- a/docs/regression_test_api.rst
+++ b/docs/regression_test_api.rst
@@ -67,9 +67,9 @@ The use of this module is required only when creating new tests programmatically
 
 .. autodecorator:: reframe.core.builtins.require_deps
 
-.. autodecorator:: reframe.core.builtins.run_after(stage)
+.. autodecorator:: reframe.core.builtins.run_after(stage, *, always_last=False)
 
-.. autodecorator:: reframe.core.builtins.run_before(stage)
+.. autodecorator:: reframe.core.builtins.run_before(stage, *, always_last=False)
 
 .. autodecorator:: reframe.core.builtins.sanity_function
 
diff --git a/unittests/test_meta.py b/unittests/test_meta.py
index 53c7095a..060df0bf 100644
--- a/unittests/test_meta.py
+++ b/unittests/test_meta.py
@@ -189,7 +189,7 @@ def test_hook_attachments(MyMeta):
         def hook_a(self):
             pass
 
-        @run_before('compile')
+        @run_before('compile', always_last=True)
         def hook_b(self):
             pass
 
@@ -198,11 +198,11 @@ def test_hook_attachments(MyMeta):
             pass
 
         @classmethod
-        def hook_in_stage(cls, hook, stage):
+        def hook_in_stage(cls, hook, stage, always_last=False):
             '''Assert that a hook is in a given registry stage.'''
             for h in cls._rfm_hook_registry:
                 if h.__name__ == hook:
-                    if stage in h.stages:
+                    if (stage, always_last) in h.stages:
                         return True
 
                     break
@@ -210,7 +210,7 @@ def test_hook_attachments(MyMeta):
             return False
 
     assert Foo.hook_in_stage('hook_a', 'post_setup')
-    assert Foo.hook_in_stage('hook_b', 'pre_compile')
+    assert Foo.hook_in_stage('hook_b', 'pre_compile', True)
     assert Foo.hook_in_stage('hook_c', 'post_run')
 
     class Bar(Foo):
diff --git a/unittests/test_pipeline.py b/unittests/test_pipeline.py
index 0bb95574..56dfb457 100644
--- a/unittests/test_pipeline.py
+++ b/unittests/test_pipeline.py
@@ -1162,6 +1162,61 @@ def test_overriden_hook_different_stages(HelloTest, local_exec_ctx):
     assert test.pipeline_hooks() == {'post_setup': [MyTest.foo]}
 
 
+def test_pinned_hooks():
+    @test_util.custom_prefix('unittests/resources/checks')
+    class X(rfm.RunOnlyRegressionTest):
+        @run_before('run', always_last=True)
+        def foo(self):
+            pass
+
+        @run_after('sanity', always_last=True)
+        def fooX(self):
+            '''Check that a single `always_last` hook is registered
+            correctly.'''
+
+    class Y(X):
+        @run_before('run')
+        def bar(self):
+            pass
+
+    test = Y()
+    assert test.pipeline_hooks() == {
+        'pre_run': [Y.bar, X.foo],
+        'post_sanity': [X.fooX]
+    }
+
+
+def test_pinned_hooks_multiple_last():
+    @test_util.custom_prefix('unittests/resources/checks')
+    class X(rfm.RunOnlyRegressionTest):
+        @run_before('run', always_last=True)
+        def foo(self):
+            pass
+
+    class Y(X):
+        @run_before('run', always_last=True)
+        def bar(self):
+            pass
+
+    with pytest.raises(ReframeSyntaxError):
+        test = Y()
+
+
+def test_pinned_hooks_multiple_last_inherited():
+    @test_util.custom_prefix('unittests/resources/checks')
+    class X(rfm.RunOnlyRegressionTest):
+        @run_before('run', always_last=True)
+        def foo(self):
+            pass
+
+        @run_before('run', always_last=True)
+        def bar(self):
+            pass
+
+    with pytest.raises(ReframeSyntaxError):
+        test = X()
+
+
 def test_disabled_hooks(HelloTest, local_exec_ctx):
     @test_util.custom_prefix('unittests/resources/checks')
     class BaseTest(HelloTest):

Allow defining pipeline hooks that run after any same-stage subclass hooks
Currently, hooks in the same stage follow the reverse MRO order, i.e., hooks from the parent classes execute first. This is problematic in cases that the base class hooks provide final functionality needed by the subclasses and the subclasses simply need to set some test variables; this is a typical scenario in library tests. Due to this limitation, tests inheriting from a a base class have to be aware of where the base class hooks are placed and define their hooks accordingly. This harms portability. I would like to be able to define a base class test as such:

```python
class base_func(...):
    some_var = variable(str)

    @run_before('run', when='last')
    def prepare_run(self):
        self.executable_opts = ['-f', self.some_var]


@rfm.simple_test
class my_test(base_func):
    @run_before('run')
    def i_cannot_set_this_before(self):
        self.some_var = 'foo'
```

This example currently fails as `some_var` is undefined when the `prepare_run` hook is executed, but I would like to be able specify that this hooks should be the last of all to run in that phase.
unittests/test_meta.py::test_hook_attachments unittests/test_pipeline.py::test_pinned_hooks_multiple_last unittests/test_pipeline.py::test_pinned_hooks_multiple_last_inherited
unittests/test_meta.py::test_class_attr_access unittests/test_meta.py::test_directives unittests/test_meta.py::test_bind_directive unittests/test_meta.py::test_sanity_function_decorator unittests/test_meta.py::test_deferrable_decorator unittests/test_meta.py::test_final unittests/test_meta.py::test_callable_attributes unittests/test_meta.py::test_performance_function unittests/test_meta.py::test_double_define_performance_function unittests/test_meta.py::test_performance_function_errors unittests/test_meta.py::test_setting_variables_on_instantiation unittests/test_meta.py::test_variants unittests/test_meta.py::test_get_info unittests/test_meta.py::test_get_variant_nums unittests/test_meta.py::test_loggable_attrs unittests/test_meta.py::test_inherited_loggable_attrs unittests/test_meta.py::test_deprecated_loggable_attrs unittests/test_pipeline.py::test_hellocheck_local_prepost_run unittests/test_pipeline.py::test_run_only_set_sanity_in_a_hook unittests/test_pipeline.py::test_run_only_decorated_sanity unittests/test_pipeline.py::test_run_only_no_srcdir unittests/test_pipeline.py::test_run_only_srcdir_set_to_none unittests/test_pipeline.py::test_executable_is_required unittests/test_pipeline.py::test_compile_only_failure unittests/test_pipeline.py::test_compile_only_warning unittests/test_pipeline.py::test_pinned_test unittests/test_pipeline.py::test_supports_sysenv unittests/test_pipeline.py::test_sourcesdir_none unittests/test_pipeline.py::test_sourcesdir_build_system unittests/test_pipeline.py::test_sourcesdir_none_generated_sources unittests/test_pipeline.py::test_sourcesdir_none_compile_only unittests/test_pipeline.py::test_sourcesdir_none_run_only unittests/test_pipeline.py::test_sourcepath_abs unittests/test_pipeline.py::test_sourcepath_upref unittests/test_pipeline.py::test_sourcepath_non_existent unittests/test_pipeline.py::test_extra_resources unittests/test_pipeline.py::test_unkown_pre_hook unittests/test_pipeline.py::test_unkown_post_hook unittests/test_pipeline.py::test_pre_init_hook unittests/test_pipeline.py::test_post_init_hook unittests/test_pipeline.py::test_setup_hooks unittests/test_pipeline.py::test_compile_hooks unittests/test_pipeline.py::test_run_hooks unittests/test_pipeline.py::test_multiple_hooks unittests/test_pipeline.py::test_stacked_hooks unittests/test_pipeline.py::test_multiple_inheritance unittests/test_pipeline.py::test_inherited_hooks unittests/test_pipeline.py::test_inherited_hooks_order unittests/test_pipeline.py::test_inherited_hooks_from_instantiated_tests unittests/test_pipeline.py::test_overriden_hooks unittests/test_pipeline.py::test_overriden_hook_different_stages unittests/test_pipeline.py::test_disabled_hooks unittests/test_pipeline.py::test_require_deps unittests/test_pipeline.py::test_trap_job_errors_without_sanity_patterns unittests/test_pipeline.py::test_trap_job_errors_with_sanity_patterns unittests/test_pipeline.py::test_sanity_success unittests/test_pipeline.py::test_sanity_failure unittests/test_pipeline.py::test_sanity_failure_noassert unittests/test_pipeline.py::test_sanity_multiple_patterns unittests/test_pipeline.py::test_sanity_multiple_files unittests/test_pipeline.py::test_performance_failure unittests/test_pipeline.py::test_reference_unknown_tag unittests/test_pipeline.py::test_reference_unknown_system unittests/test_pipeline.py::test_reference_empty unittests/test_pipeline.py::test_reference_default unittests/test_pipeline.py::test_reference_tag_resolution unittests/test_pipeline.py::test_required_reference[classic] unittests/test_pipeline.py::test_required_reference[modern] unittests/test_pipeline.py::test_reference_deferrable[classic] unittests/test_pipeline.py::test_reference_deferrable[modern] unittests/test_pipeline.py::test_performance_invalid_value unittests/test_pipeline.py::test_perf_patterns_evaluation unittests/test_pipeline.py::test_validate_default_perf_variables unittests/test_pipeline.py::test_perf_vars_without_reference unittests/test_pipeline.py::test_perf_vars_with_reference unittests/test_pipeline.py::test_incompat_perf_syntax unittests/test_pipeline.py::test_unknown_container_platform unittests/test_pipeline.py::test_not_configured_container_platform unittests/test_pipeline.py::test_skip_if_no_topo unittests/test_pipeline.py::test_make_test_without_builtins unittests/test_pipeline.py::test_make_test_with_module unittests/test_pipeline.py::test_make_test_with_builtins unittests/test_pipeline.py::test_make_test_with_builtins_inline