diff --git a/doc/source/custom-ansible-playbooks.rst b/doc/source/custom-ansible-playbooks.rst
index e1a759759a5f558cb31b216feb20326310a59aaf..d409a8276ecc26ea0a0fb54346bd6017008b84c0 100644
--- a/doc/source/custom-ansible-playbooks.rst
+++ b/doc/source/custom-ansible-playbooks.rst
@@ -120,3 +120,84 @@ We should first install the Galaxy role dependencies, to download the
 Then, to run the ``foo.yml`` playbook::
 
     (kayobe) $ kayobe playbook run $KAYOBE_CONFIG_PATH/ansible/foo.yml
+
+Hooks
+=====
+
+.. warning::
+    Hooks are an experimental feature and the design could change in the future.
+    You may have to update your config if there are any changes to the design.
+    This warning will be removed when the design has been stabilised.
+
+Hooks allow you to automatically execute custom playbooks at certain points during
+the execution of a kayobe command. The point at which a hook is run is referred to
+as a ``target``. Please see the :ref:`list of available targets<Hook Targets>`.
+
+Hooks are created by symlinking an existing playbook into the the relevant directory under
+``$KAYOBE_CONFIG_PATH/hooks``. Kayobe will search the hooks directory for sub-directories
+matching ``<command>.<target>.d``, where ``command`` is the name of a kayobe command
+with any spaces replaced with dashes, and ``target`` is one of the supported targets for
+the command.
+
+For example, when using the command::
+
+    (kayobe) $ kayobe control host bootstrap
+
+kayobe will search the paths:
+
+- ``$KAYOBE_CONFIG_PATH/hooks/control-host-bootstrap/pre.d``
+- ``$KAYOBE_CONFIG_PATH/hooks/control-host-bootstrap/post.d``
+
+Any playbooks listed under the ``pre.d`` directory will be run before kayobe executes
+its own playbooks and any playbooks under ``post.d`` will be run after. You can affect
+the order of the playbooks by prefixing the symlink with a sequence number. The sequence
+number must be separated from the hook name with a dash. Playbooks with smaller sequence
+numbers are run before playbooks with larger ones. Any ties are broken by alphabetical
+ordering.
+
+For example to run the playbook ``foo.yml`` after ``kayobe overcloud host configure``,
+you could do the following::
+
+    (kayobe) $ mkdir -p $KAYOBE_CONFIG_PATH/hooks/overcloud-host-configure/post.d
+    (kayobe) $ ln -s  $KAYOBE_CONFIG_PATH/ansible/foo.yml \
+    $KAYOBE_CONFIG_PATH/hooks/overcloud-host-configure/post.d/10-foo.yml
+
+The sequence number for the ``foo.yml`` playbook is ``10``.
+
+Failure handling
+----------------
+
+If the exit status of any playbook, including built-in playbooks and custom hooks,
+is non-zero, kayobe will not run any subsequent hooks or built-in kayobe playbooks.
+Ansible provides several methods for preventing a task from producing a failure. Please
+see the `Ansible documentation <https://docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html>`_
+for more details. Below is an example showing how you can use the ``ignore_errors`` option
+to prevent a task from causing the playbook to report a failure::
+
+  ---
+  - name: Failure example
+    hosts: localhost
+    tasks:
+      - name: Deliberately fail
+        fail:
+        ignore_errors: true
+
+A failure in the ``Deliberately fail`` task would not prevent subsequent tasks, hooks,
+and playbooks from running.
+
+.. _Hook Targets:
+
+Targets
+-------
+The following targets are available for all commands:
+
+.. list-table:: all commands
+   :widths: 10 500
+   :header-rows: 1
+
+   * - Target
+     - Description
+   * - pre
+     - Runs before a kayobe command has start executing
+   * - post
+     - Runs after a kayobe command has finished executing
diff --git a/etc/kayobe/hooks/.gitkeep b/etc/kayobe/hooks/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index d43c2d84492bd49217937ad85e44971170c619ea..5ceac658157aea13c61fc474804284540e42a215 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -12,16 +12,22 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import glob
 import json
+import os
 import sys
 
 from cliff.command import Command
+from cliff.hooks import CommandHook
 
 from kayobe import ansible
 from kayobe import kolla_ansible
 from kayobe import utils
 from kayobe import vault
 
+# This is set to an arbitrary large number to simplify the sorting logic
+DEFAULT_SEQUENCE_NUMBER = sys.maxsize
+
 
 def _build_playbook_list(*playbooks):
     """Return a list of names of playbook files given their basenames."""
@@ -144,6 +150,73 @@ class KollaAnsibleMixin(object):
         return kolla_ansible.run_seed(*args, **kwargs)
 
 
+def _split_hook_sequence_number(hook):
+    parts = hook.split("-", 1)
+    if len(parts) < 2:
+        return (DEFAULT_SEQUENCE_NUMBER, hook)
+    try:
+        return (int(parts[0]), parts[1])
+    except ValueError:
+        return (DEFAULT_SEQUENCE_NUMBER, hook)
+
+
+class HookDispatcher(CommandHook):
+    """Runs custom playbooks before and after a command"""
+
+# Order of calls: get_epilog, get_parser, before, after
+
+    def __init__(self, *args, **kwargs):
+        self.command = kwargs["command"]
+        self.logger = self.command.app.LOG
+        cmd = self.command.cmd_name
+        # Replace white space with dashes for consistency with ansible
+        # playbooks. Example cmd: kayobe control host bootstrap
+        self.name = "-".join(cmd.split())
+
+    def get_epilog(self):
+        pass
+
+    def get_parser(self, prog_name):
+        pass
+
+    def _find_hooks(self, config_path, target):
+        name = self.name
+        path = os.path.join(config_path, "hooks", name, "%s.d" % target)
+        self.logger.debug("Discovering hooks in: %s" % path)
+        if not os.path.exists:
+            return []
+        hooks = glob.glob(os.path.join(path, "*.yml"))
+        self.logger.debug("Discovered the following hooks: %s" % hooks)
+        return hooks
+
+    def hooks(self, config_path, target):
+        hooks = self._find_hooks(config_path, target)
+        # Hooks can be prefixed with a sequence number to adjust running order,
+        # e.g 10-my-custom-playbook.yml. Sort by sequence number.
+        hooks = sorted(hooks, key=_split_hook_sequence_number)
+        # Resolve symlinks so that we can reference roles.
+        hooks = [os.path.realpath(hook) for hook in hooks]
+        return hooks
+
+    def run_hooks(self, parsed_args, target):
+        config_path = parsed_args.config_path
+        hooks = self.hooks(config_path, target)
+        if hooks:
+            self.logger.debug("Running hooks: %s" % hooks)
+            self.command.run_kayobe_playbooks(parsed_args, hooks)
+
+    def before(self, parsed_args):
+        self.run_hooks(parsed_args, "pre")
+        return parsed_args
+
+    def after(self, parsed_args, return_code):
+        if return_code == 0:
+            self.run_hooks(parsed_args, "post")
+        else:
+            self.logger.debug("Not running hooks due to non-zero return code")
+        return return_code
+
+
 class ControlHostBootstrap(KayobeAnsibleMixin, KollaAnsibleMixin, VaultMixin,
                            Command):
     """Bootstrap the Kayobe control environment.
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index a67d6ee26fe92d50759ebbf2ba09df4a49f10f50..e29d1da46b11d0bdd353d7b655fdc7893d9104fa 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -1969,3 +1969,31 @@ class TestCase(unittest.TestCase):
             ),
         ]
         self.assertEqual(expected_calls, mock_run.call_args_list)
+
+
+class TestHookDispatcher(unittest.TestCase):
+
+    @mock.patch('kayobe.cli.commands.os.path')
+    def test_hook_ordering(self, mock_path):
+        mock_command = mock.MagicMock()
+        dispatcher = commands.HookDispatcher(command=mock_command)
+        dispatcher._find_hooks = mock.MagicMock()
+        dispatcher._find_hooks.return_value = [
+            "10-hook.yml",
+            "5-hook.yml",
+            "z-test-alphabetical.yml",
+            "10-before-hook.yml",
+            "5-multiple-dashes-in-name.yml",
+            "no-prefix.yml"
+        ]
+        expected_result = [
+            "5-hook.yml",
+            "5-multiple-dashes-in-name.yml",
+            "10-before-hook.yml",
+            "10-hook.yml",
+            "no-prefix.yml",
+            "z-test-alphabetical.yml",
+        ]
+        mock_path.realpath.side_effect = lambda x: x
+        actual = dispatcher.hooks("config/path", "pre")
+        self.assertListEqual(actual, expected_result)
diff --git a/releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml b/releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4e958de85e42a2bfd4bf356e6d99956344a02001
--- /dev/null
+++ b/releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds an experimental mechanism to automatically run custom playbooks
+    before and after kayobe commands. Please see the ``Custom Ansible Playbooks``
+    section in the documentation for more details.
diff --git a/setup.cfg b/setup.cfg
index 60d9a5cbd80aa621dacc7310f4fd8b8bd968959f..f74400bf706bf97b64a00d85b08486cc7823e2ad 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -89,3 +89,108 @@ kayobe.cli=
     seed_service_upgrade = kayobe.cli.commands:SeedServiceUpgrade
     seed_vm_deprovision = kayobe.cli.commands:SeedVMDeprovision
     seed_vm_provision = kayobe.cli.commands:SeedVMProvision
+
+kayobe.cli.baremetal_compute_inspect =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.baremetal_compute_manage =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.baremetal_compute_provide =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.baremetal_compute_rename =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.baremetal_compute_update_deployment_image =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.baremetal_compute_serial_console_enable =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.baremetal_compute_serial_console_disable =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.control_host_bootstrap =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.control_host_upgrade =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.configuration_dump =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.kolla_ansible_run =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.network_connectivity_check =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_bios_raid_configure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_container_image_build =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_container_image_pull =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_database_backup =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_database_recover =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_deployment_image_build =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_deprovision =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_hardware_inspect =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_host_configure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_host_package_update =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_host_command_run =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_host_upgrade =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_introspection_data_save =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_inventory_discover =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_post_configure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_provision =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_configuration_save =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_configuration_generate =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_deploy =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_deploy_containers =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_destroy =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_reconfigure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_stop =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_service_upgrade =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.overcloud_swift_rings_generate =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.physical_network_configure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.playbook_run =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_container_image_build =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_deployment_image_build =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_host_configure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_host_package_update =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_host_command_run =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_host_upgrade =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_hypervisor_host_configure =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_hypervisor_host_command_run =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_hypervisor_host_upgrade =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_service_deploy =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_service_upgrade =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_vm_deprovision =
+    hooks = kayobe.cli.commands:HookDispatcher
+kayobe.cli.seed_vm_provision =
+    hooks = kayobe.cli.commands:HookDispatcher