[Lldb-commits] [lldb] [LLDB] Add module hook implementation (PR #185465)

Bar Soloveychik via lldb-commits lldb-commits at lists.llvm.org
Thu Mar 12 10:38:20 PDT 2026


https://github.com/barsolo2000 updated https://github.com/llvm/llvm-project/pull/185465

>From 69157a9d440450d35a792892d735c841d7a3f9a6 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Mon, 9 Mar 2026 10:24:52 -0700
Subject: [PATCH 1/7] [LLDB] Add module hook

---
 .../Interfaces/ScriptedModuleHookInterface.h  |  35 ++
 .../lldb/Interpreter/ScriptInterpreter.h      |   5 +
 lldb/include/lldb/Target/Target.h             | 104 ++++++
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/source/Commands/CommandObjectTarget.cpp  | 328 ++++++++++++++++++
 lldb/source/Commands/Options.td               |  11 +
 .../ScriptInterpreter/Python/CMakeLists.txt   |   1 +
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../ScriptedModuleHookPythonInterface.cpp     |  64 ++++
 .../ScriptedModuleHookPythonInterface.h       |  52 +++
 .../Python/ScriptInterpreterPython.cpp        |   5 +
 .../Python/ScriptInterpreterPythonImpl.h      |   3 +
 lldb/source/Target/Target.cpp                 | 269 ++++++++++++++
 .../delete/TestTargetModuleHookDelete.py      |  19 +
 .../disable/TestTargetModuleHookDisable.py    |  19 +
 .../enable/TestTargetModuleHookEnable.py      |  19 +
 .../Commands/command-module-hook-fire.test    |  10 +
 .../Shell/Commands/command-module-hook.test   | 113 ++++++
 19 files changed, 1063 insertions(+)
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
 create mode 100644 lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
 create mode 100644 lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
 create mode 100644 lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
 create mode 100644 lldb/test/Shell/Commands/command-module-hook-fire.test
 create mode 100644 lldb/test/Shell/Commands/command-module-hook.test

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
new file mode 100644
index 0000000000000..7a06349eb9161
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
@@ -0,0 +1,35 @@
+//===-- ScriptedModuleHookInterface.h ----------------------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDMODULEHOOKINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDMODULEHOOKINTERFACE_H
+
+#include "lldb/lldb-private.h"
+
+#include "ScriptedInterface.h"
+
+namespace lldb_private {
+class ScriptedModuleHookInterface : public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
+                     const StructuredDataImpl &args_sp) = 0;
+
+  /// Called when modules are loaded into the target. Unlike stop hooks,
+  /// module hooks do not control whether the process should stop.
+  virtual void HandleModuleLoaded(lldb::StreamSP &output_sp) {}
+
+  /// Called when modules are unloaded from the target. Optional for
+  /// scripted hooks; if not implemented, the hook silently does nothing
+  /// on unload.
+  virtual void HandleModuleUnloaded(lldb::StreamSP &output_sp) {}
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDMODULEHOOKINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 557d73a415452..402af875c7eb5 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -562,6 +562,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedModuleHookInterfaceSP
+  CreateScriptedModuleHookInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedBreakpointInterfaceSP
   CreateScriptedBreakpointInterface() {
     return {};
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 4f5b022765f9e..bf5582d30921d 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1641,6 +1641,106 @@ class Target : public std::enable_shared_from_this<Target>,
 
   typedef std::shared_ptr<StopHook> StopHookSP;
 
+  // Target Module Hooks
+  //
+  // Module hooks fire whenever modules are loaded or unloaded from the
+  // target (via ModulesDidLoad / ModulesDidUnload).
+  class ModuleHook : public UserID {
+  public:
+    ModuleHook(const ModuleHook &rhs);
+    virtual ~ModuleHook() = default;
+
+    enum class ModuleHookKind : uint32_t { CommandBased = 0, ScriptBased };
+
+    lldb::TargetSP &GetTarget() { return m_target_sp; }
+
+    bool IsActive() { return m_active; }
+    void SetIsActive(bool is_active) { m_active = is_active; }
+
+    bool GetFireOnUnload() const { return m_fire_on_unload; }
+    void SetFireOnUnload(bool fire) { m_fire_on_unload = fire; }
+
+    void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
+    virtual void GetSubclassDescription(Stream &s,
+                                        lldb::DescriptionLevel level) const = 0;
+
+    virtual void HandleModuleLoaded(lldb::StreamSP output) = 0;
+    virtual void HandleModuleUnloaded(lldb::StreamSP output) = 0;
+
+  protected:
+    lldb::TargetSP m_target_sp;
+    bool m_active = true;
+    bool m_fire_on_unload = false;
+
+    ModuleHook(lldb::TargetSP target_sp, lldb::user_id_t uid);
+  };
+
+  class ModuleHookCommandLine : public ModuleHook {
+  public:
+    ~ModuleHookCommandLine() override = default;
+
+    StringList &GetCommands() { return m_commands; }
+    void SetActionFromString(const std::string &string);
+    void SetActionFromStrings(const std::vector<std::string> &strings);
+
+    void HandleModuleLoaded(lldb::StreamSP output) override;
+    void HandleModuleUnloaded(lldb::StreamSP output) override;
+    void GetSubclassDescription(Stream &s,
+                                lldb::DescriptionLevel level) const override;
+
+  private:
+    StringList m_commands;
+
+    ModuleHookCommandLine(lldb::TargetSP target_sp, lldb::user_id_t uid)
+        : ModuleHook(target_sp, uid) {}
+    friend class Target;
+  };
+
+  class ModuleHookScripted : public ModuleHook {
+  public:
+    ~ModuleHookScripted() override = default;
+
+    void HandleModuleLoaded(lldb::StreamSP output) override;
+    void HandleModuleUnloaded(lldb::StreamSP output) override;
+
+    Status SetScriptCallback(std::string class_name,
+                             StructuredData::ObjectSP extra_args_sp);
+
+    void GetSubclassDescription(Stream &s,
+                                lldb::DescriptionLevel level) const override;
+
+  private:
+    std::string m_class_name;
+    StructuredDataImpl m_extra_args;
+    lldb::ScriptedModuleHookInterfaceSP m_interface_sp;
+
+    ModuleHookScripted(lldb::TargetSP target_sp, lldb::user_id_t uid)
+        : ModuleHook(target_sp, uid) {}
+    friend class Target;
+  };
+
+  typedef std::shared_ptr<ModuleHook> ModuleHookSP;
+
+  ModuleHookSP CreateModuleHook(ModuleHook::ModuleHookKind kind);
+
+  void UndoCreateModuleHook(lldb::user_id_t uid);
+
+  bool RemoveModuleHookByID(lldb::user_id_t uid);
+
+  void RemoveAllModuleHooks();
+
+  ModuleHookSP GetModuleHookByID(lldb::user_id_t uid);
+
+  bool SetModuleHookActiveStateByID(lldb::user_id_t uid, bool active_state);
+
+  void SetAllModuleHooksActiveState(bool active_state);
+
+  size_t GetNumModuleHooks() const { return m_module_hooks.size(); }
+
+  ModuleHookSP GetModuleHookAtIndex(size_t index);
+
+  void RunModuleHooks(bool is_load);
+
   /// Add an empty stop hook to the Target's stop hook list, and returns a
   /// shared pointer to the new hook.
   StopHookSP CreateStopHook(StopHook::StopHookKind kind, bool internal = false);
@@ -1826,6 +1926,10 @@ class Target : public std::enable_shared_from_this<Target>,
   bool m_valid;
   bool m_suppress_stop_hooks; /// Used to not run stop hooks for expressions
   bool m_is_dummy_target;
+
+  typedef std::map<lldb::user_id_t, ModuleHookSP> ModuleHookCollection;
+  ModuleHookCollection m_module_hooks;
+  lldb::user_id_t m_module_hook_next_id = 0;
   unsigned m_next_persistent_variable_index = 0;
   lldb::user_id_t m_target_unique_id =
       LLDB_INVALID_GLOBALLY_UNIQUE_TARGET_ID; ///< The globally unique ID
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index ccfe5efa19e1d..fcff135236ad0 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -191,6 +191,7 @@ class ScriptedFrameInterface;
 class ScriptedFrameProviderInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
+class ScriptedModuleHookInterface;
 class ScriptedPlatformInterface;
 class ScriptedProcessInterface;
 class ScriptedStopHookInterface;
@@ -425,6 +426,8 @@ typedef std::unique_ptr<lldb_private::ScriptedProcessInterface>
     ScriptedProcessInterfaceUP;
 typedef std::shared_ptr<lldb_private::ScriptedStopHookInterface>
     ScriptedStopHookInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedModuleHookInterface>
+    ScriptedModuleHookInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedThreadInterface>
     ScriptedThreadInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index 59ccf390dea31..b18705e0c6913 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5333,6 +5333,331 @@ class CommandObjectMultiwordTargetStopHooks : public CommandObjectMultiword {
   ~CommandObjectMultiwordTargetStopHooks() override = default;
 };
 
+#pragma mark CommandObjectTargetModuleHookAdd
+
+#define LLDB_OPTIONS_target_module_hook_add
+#include "CommandOptions.inc"
+
+class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
+                                         public IOHandlerDelegateMultiline {
+public:
+  class CommandOptions : public OptionGroup {
+  public:
+    CommandOptions() = default;
+    ~CommandOptions() override = default;
+
+    llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+      return llvm::ArrayRef(g_target_module_hook_add_options);
+    }
+
+    Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+                          ExecutionContext *execution_context) override {
+      Status error;
+      const int short_option =
+          g_target_module_hook_add_options[option_idx].short_option;
+      switch (short_option) {
+      case 'o':
+        m_use_one_liner = true;
+        m_one_liner.push_back(std::string(option_arg));
+        break;
+      case 'u':
+        m_on_unload = true;
+        break;
+      default:
+        llvm_unreachable("unhandled option");
+      }
+      return error;
+    }
+
+    void OptionParsingStarting(ExecutionContext *execution_context) override {
+      m_use_one_liner = false;
+      m_one_liner.clear();
+      m_on_unload = false;
+    }
+
+    std::vector<std::string> m_one_liner;
+    bool m_use_one_liner = false;
+    bool m_on_unload = false;
+  };
+
+  CommandObjectTargetModuleHookAdd(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target module-hook add",
+            "Add a hook to be executed whenever modules are loaded into the "
+            "target.",
+            "target module-hook add"),
+        IOHandlerDelegateMultiline("DONE",
+                                   IOHandlerDelegate::Completion::LLDBCommand),
+        m_python_class_options("scripted module hook", false, 'P') {
+    SetHelpLong(R"help(
+Command-based module hooks allow running LLDB commands every time modules are
+loaded into the target. For example:
+
+    target module-hook add -o "script print('module loaded')"
+
+Use --on-unload (-u) to also fire the hook when modules are unloaded:
+
+    target module-hook add -u -o "script print('module event')"
+
+Python-based module hooks allow running a Python class:
+
+    target module-hook add -P MyHook
+
+The Python class should implement:
+
+    class MyHook:
+        def __init__(self, target, extra_args, internal_dict):
+            self.target = target
+        def handle_module_loaded(self, stream):
+            pass
+        def handle_module_unloaded(self, stream):
+            pass
+
+handle_module_loaded is required. handle_module_unloaded is optional and only
+called when --on-unload is specified.
+)help");
+    m_all_options.Append(&m_python_class_options,
+                         LLDB_OPT_SET_1 | LLDB_OPT_SET_2, LLDB_OPT_SET_2);
+    m_all_options.Append(&m_options);
+    m_all_options.Finalize();
+  }
+
+  ~CommandObjectTargetModuleHookAdd() override = default;
+
+  Options *GetOptions() override { return &m_all_options; }
+
+protected:
+  void IOHandlerActivated(IOHandler &io_handler, bool interactive) override {
+    if (interactive) {
+      if (lldb::LockableStreamFileSP output_sp =
+              io_handler.GetOutputStreamFileSP()) {
+        LockedStreamFile locked_stream = output_sp->Lock();
+        locked_stream.PutCString(
+            "Enter your module hook command(s). Type 'DONE' to end.\n");
+      }
+    }
+  }
+
+  void IOHandlerInputComplete(IOHandler &io_handler,
+                              std::string &line) override {
+    if (m_module_hook_sp) {
+      if (line.empty()) {
+        if (lldb::LockableStreamFileSP error_sp =
+                io_handler.GetErrorStreamFileSP()) {
+          LockedStreamFile locked_stream = error_sp->Lock();
+          locked_stream.Printf("error: module hook #%" PRIu64
+                               " aborted, no commands.\n",
+                               m_module_hook_sp->GetID());
+        }
+        GetTarget().UndoCreateModuleHook(m_module_hook_sp->GetID());
+      } else {
+        auto *hook = static_cast<Target::ModuleHookCommandLine *>(
+            m_module_hook_sp.get());
+        hook->SetActionFromString(line);
+        if (lldb::LockableStreamFileSP output_sp =
+                io_handler.GetOutputStreamFileSP()) {
+          LockedStreamFile locked_stream = output_sp->Lock();
+          locked_stream.Printf("Module hook #%" PRIu64 " added.\n",
+                               m_module_hook_sp->GetID());
+        }
+      }
+      m_module_hook_sp.reset();
+    }
+    io_handler.SetIsDone(true);
+  }
+
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    m_module_hook_sp.reset();
+    Target &target = GetTarget();
+
+    Target::ModuleHook::ModuleHookKind hook_kind;
+    if (m_python_class_options.GetName().empty())
+      hook_kind = Target::ModuleHook::ModuleHookKind::CommandBased;
+    else
+      hook_kind = Target::ModuleHook::ModuleHookKind::ScriptBased;
+
+    Target::ModuleHookSP new_hook_sp = target.CreateModuleHook(hook_kind);
+
+    if (m_options.m_on_unload)
+      new_hook_sp->SetFireOnUnload(true);
+
+    if (m_options.m_use_one_liner) {
+      auto *hook =
+          static_cast<Target::ModuleHookCommandLine *>(new_hook_sp.get());
+      hook->SetActionFromStrings(m_options.m_one_liner);
+      result.AppendMessageWithFormat("Module hook #%" PRIu64 " added.\n",
+                                     new_hook_sp->GetID());
+    } else if (!m_python_class_options.GetName().empty()) {
+      auto *hook = static_cast<Target::ModuleHookScripted *>(new_hook_sp.get());
+      Status callback_error =
+          hook->SetScriptCallback(m_python_class_options.GetName(),
+                                  m_python_class_options.GetStructuredData());
+      if (callback_error.Fail()) {
+        result.AppendErrorWithFormat("error: couldn't add module hook: %s\n",
+                                     callback_error.AsCString());
+        target.UndoCreateModuleHook(new_hook_sp->GetID());
+        return;
+      }
+      result.AppendMessageWithFormat("Module hook #%" PRIu64 " added.\n",
+                                     new_hook_sp->GetID());
+    } else {
+      m_module_hook_sp = new_hook_sp;
+      m_interpreter.GetLLDBCommandsFromIOHandler("> ",   // prompt
+                                                 *this); // delegate
+    }
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+
+private:
+  CommandOptions m_options;
+  OptionGroupPythonClassWithDict m_python_class_options;
+  OptionGroupOptions m_all_options;
+  Target::ModuleHookSP m_module_hook_sp;
+};
+
+#pragma mark CommandObjectTargetModuleHookDelete
+
+class CommandObjectTargetModuleHookDelete : public CommandObjectParsed {
+public:
+  CommandObjectTargetModuleHookDelete(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target module-hook delete",
+                            "Delete a module-hook.",
+                            "target module-hook delete [<id>]") {
+    AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
+  }
+
+  ~CommandObjectTargetModuleHookDelete() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    if (command.GetArgumentCount() == 0) {
+      target.RemoveAllModuleHooks();
+      result.SetStatus(eReturnStatusSuccessFinishNoResult);
+      return;
+    }
+
+    for (size_t i = 0; i < command.GetArgumentCount(); i++) {
+      lldb::user_id_t user_id;
+      if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
+        result.AppendErrorWithFormat("invalid module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+      if (!target.RemoveModuleHookByID(user_id)) {
+        result.AppendErrorWithFormat("unknown module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+    }
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+};
+
+#pragma mark CommandObjectTargetModuleHookEnableDisable
+
+class CommandObjectTargetModuleHookEnableDisable : public CommandObjectParsed {
+public:
+  CommandObjectTargetModuleHookEnableDisable(CommandInterpreter &interpreter,
+                                             bool enable, const char *name,
+                                             const char *help,
+                                             const char *syntax)
+      : CommandObjectParsed(interpreter, name, help, syntax), m_enable(enable) {
+    AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
+  }
+
+  ~CommandObjectTargetModuleHookEnableDisable() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    if (command.GetArgumentCount() == 0) {
+      target.SetAllModuleHooksActiveState(m_enable);
+      result.SetStatus(eReturnStatusSuccessFinishNoResult);
+      return;
+    }
+
+    for (size_t i = 0; i < command.GetArgumentCount(); i++) {
+      lldb::user_id_t user_id;
+      if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
+        result.AppendErrorWithFormat("invalid module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+      if (!target.SetModuleHookActiveStateByID(user_id, m_enable)) {
+        result.AppendErrorWithFormat("unknown module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+    }
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+
+private:
+  bool m_enable;
+};
+
+#pragma mark CommandObjectTargetModuleHookList
+
+class CommandObjectTargetModuleHookList : public CommandObjectParsed {
+public:
+  CommandObjectTargetModuleHookList(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target module-hook list",
+                            "List all module-hooks.",
+                            "target module-hook list") {}
+
+  ~CommandObjectTargetModuleHookList() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    size_t num_hooks = target.GetNumModuleHooks();
+    if (num_hooks == 0) {
+      result.GetOutputStream().PutCString("No module hooks.\n");
+    } else {
+      for (size_t i = 0; i < num_hooks; i++) {
+        Target::ModuleHookSP hook_sp = target.GetModuleHookAtIndex(i);
+        if (hook_sp)
+          hook_sp->GetDescription(result.GetOutputStream(),
+                                  eDescriptionLevelFull);
+      }
+    }
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+#pragma mark CommandObjectMultiwordTargetModuleHooks
+
+class CommandObjectMultiwordTargetModuleHooks : public CommandObjectMultiword {
+public:
+  CommandObjectMultiwordTargetModuleHooks(CommandInterpreter &interpreter)
+      : CommandObjectMultiword(
+            interpreter, "target module-hook",
+            "Commands for operating on debugger target module-hooks.",
+            "target module-hook <subcommand> [<subcommand-options>]") {
+    LoadSubCommand("add", CommandObjectSP(new CommandObjectTargetModuleHookAdd(
+                              interpreter)));
+    LoadSubCommand(
+        "delete",
+        CommandObjectSP(new CommandObjectTargetModuleHookDelete(interpreter)));
+    LoadSubCommand(
+        "disable",
+        CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
+            interpreter, false, "target module-hook disable [<id>]",
+            "Disable a module-hook.", "target module-hook disable")));
+    LoadSubCommand(
+        "enable",
+        CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
+            interpreter, true, "target module-hook enable [<id>]",
+            "Enable a module-hook.", "target module-hook enable")));
+    LoadSubCommand(
+        "list",
+        CommandObjectSP(new CommandObjectTargetModuleHookList(interpreter)));
+  }
+
+  ~CommandObjectMultiwordTargetModuleHooks() override = default;
+};
+
 #pragma mark CommandObjectTargetDumpTypesystem
 
 /// Dumps the TypeSystem of the selected Target.
@@ -5625,6 +5950,9 @@ CommandObjectMultiwordTarget::CommandObjectMultiwordTarget(
   LoadSubCommand(
       "stop-hook",
       CommandObjectSP(new CommandObjectMultiwordTargetStopHooks(interpreter)));
+  LoadSubCommand("module-hook",
+                 CommandObjectSP(
+                     new CommandObjectMultiwordTargetModuleHooks(interpreter)));
   LoadSubCommand("modules",
                  CommandObjectSP(new CommandObjectTargetModules(interpreter)));
   LoadSubCommand("symbols",
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index a4d72010d2c4c..8eee059b7c9c0 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1901,6 +1901,17 @@ let Command = "target stop_hook list" in {
         Desc<"Show debugger ${i}nternal stop hooks.">;
 }
 
+let Command = "target module_hook add" in {
+  def target_module_hook_add_one_liner
+      : Option<"one-liner", "o">,
+        Arg<"OneLiner">,
+        Desc<"Add a command for the module hook. Can be specified more than "
+             "once, and commands will be run in the order they appear.">;
+  def target_module_hook_add_on_unload
+      : Option<"on-unload", "u">,
+        Desc<"Also fire the hook when modules are ${u}nloaded.">;
+}
+
 let Command = "thread backtrace" in {
   def thread_backtrace_count : Option<"count", "c">,
                                Group<1>,
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index 303e17a938912..817520bc37271 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -31,6 +31,7 @@ add_lldb_library(lldbPluginScriptInterpreterPython PLUGIN
   Interfaces/ScriptedProcessPythonInterface.cpp
   Interfaces/ScriptedPythonInterface.cpp
   Interfaces/ScriptedStopHookPythonInterface.cpp
+  Interfaces/ScriptedModuleHookPythonInterface.cpp
   Interfaces/ScriptedBreakpointPythonInterface.cpp
   Interfaces/ScriptedThreadPlanPythonInterface.cpp
   Interfaces/ScriptedThreadPythonInterface.cpp
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index 3f6f9cfd4da8d..d7368de2bda67 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -24,6 +24,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedPlatformPythonInterface::Initialize();
   ScriptedProcessPythonInterface::Initialize();
   ScriptedStopHookPythonInterface::Initialize();
+  ScriptedModuleHookPythonInterface::Initialize();
   ScriptedBreakpointPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
   ScriptedFrameProviderPythonInterface::Initialize();
@@ -34,6 +35,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedPlatformPythonInterface::Terminate();
   ScriptedProcessPythonInterface::Terminate();
   ScriptedStopHookPythonInterface::Terminate();
+  ScriptedModuleHookPythonInterface::Terminate();
   ScriptedBreakpointPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
   ScriptedFrameProviderPythonInterface::Terminate();
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 58b19760cb8e1..6866aabb22666 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -16,6 +16,7 @@
 #include "ScriptedBreakpointPythonInterface.h"
 #include "ScriptedFrameProviderPythonInterface.h"
 #include "ScriptedFramePythonInterface.h"
+#include "ScriptedModuleHookPythonInterface.h"
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
new file mode 100644
index 0000000000000..5b2de9a4a27e2
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
@@ -0,0 +1,64 @@
+//===-- ScriptedModuleHookPythonInterface.cpp
+//------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "../lldb-python.h"
+#include "ScriptedModuleHookPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+
+ScriptedModuleHookPythonInterface::ScriptedModuleHookPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedModuleHookInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedModuleHookPythonInterface::CreatePluginObject(
+    llvm::StringRef class_name, lldb::TargetSP target_sp,
+    const StructuredDataImpl &args_sp) {
+  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
+                                                     target_sp, args_sp);
+}
+
+void ScriptedModuleHookPythonInterface::HandleModuleLoaded(
+    lldb::StreamSP &output_sp) {
+  Status error;
+  // We pass only the output stream to Python. The Python class can access
+  // self.target (set during __init__) to query loaded modules if needed.
+  Dispatch("handle_module_loaded", error, output_sp);
+}
+
+void ScriptedModuleHookPythonInterface::HandleModuleUnloaded(
+    lldb::StreamSP &output_sp) {
+  Status error;
+  // Optional method. If the Python class does not implement
+  // handle_module_unloaded, the dispatch will fail silently.
+  Dispatch("handle_module_unloaded", error, output_sp);
+}
+
+void ScriptedModuleHookPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "target module-hook add -P <script-name> [-k key -v value ...]"};
+  const std::vector<llvm::StringRef> api_usages = {};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      llvm::StringRef("Perform actions whenever modules are loaded into the "
+                      "target."),
+      CreateInstance, eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedModuleHookPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
new file mode 100644
index 0000000000000..1a5f4f14c0eab
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
@@ -0,0 +1,52 @@
+//===-- ScriptedModuleHookPythonInterface.h ----------------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDMODULEHOOKPYTHONINTERFACE_H
+#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDMODULEHOOKPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h"
+
+#include "ScriptedPythonInterface.h"
+
+namespace lldb_private {
+class ScriptedModuleHookPythonInterface : public ScriptedModuleHookInterface,
+                                          public ScriptedPythonInterface,
+                                          public PluginInterface {
+public:
+  ScriptedModuleHookPythonInterface(ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
+                     const StructuredDataImpl &args_sp) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>(
+        {{"handle_module_loaded", 1}});
+  }
+
+  void HandleModuleLoaded(lldb::StreamSP &output_sp) override;
+
+  /// Optional: only called if the Python class implements
+  /// handle_module_unloaded. Silently does nothing otherwise.
+  void HandleModuleUnloaded(lldb::StreamSP &output_sp) override;
+
+  static void Initialize();
+
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedModuleHookPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDMODULEHOOKPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 24228a62708fd..1c94c70fa4ccb 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1509,6 +1509,11 @@ ScriptInterpreterPythonImpl::CreateScriptedStopHookInterface() {
   return std::make_shared<ScriptedStopHookPythonInterface>(*this);
 }
 
+ScriptedModuleHookInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedModuleHookInterface() {
+  return std::make_shared<ScriptedModuleHookPythonInterface>(*this);
+}
+
 ScriptedBreakpointInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedBreakpointInterface() {
   return std::make_shared<ScriptedBreakpointPythonInterface>(*this);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index f08b2334f9ccb..59c92b08e127e 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -90,6 +90,9 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   lldb::ScriptedStopHookInterfaceSP CreateScriptedStopHookInterface() override;
 
+  lldb::ScriptedModuleHookInterfaceSP
+  CreateScriptedModuleHookInterface() override;
+
   lldb::ScriptedBreakpointInterfaceSP
   CreateScriptedBreakpointInterface() override;
 
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index f3ee0d91a88d9..c29538ca48cc7 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -38,6 +38,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h"
 #include "lldb/Interpreter/OptionGroupWatchpoint.h"
 #include "lldb/Interpreter/OptionValues.h"
@@ -1873,6 +1874,7 @@ void Target::ModulesDidLoad(ModuleList &module_list) {
     if (m_process_sp) {
       m_process_sp->ModulesDidLoad(module_list);
     }
+    RunModuleHooks(/*is_load=*/true);
     auto data_sp =
         std::make_shared<TargetEventData>(shared_from_this(), module_list);
     BroadcastEvent(eBroadcastBitModulesLoaded, data_sp);
@@ -1932,6 +1934,8 @@ void Target::ModulesDidUnload(ModuleList &module_list, bool delete_locations) {
 
     if (should_flush_type_systems)
       m_scratch_type_system_map.Clear();
+
+    RunModuleHooks(/*is_load=*/false);
   }
 }
 
@@ -4229,6 +4233,271 @@ void Target::StopHookScripted::GetSubclassDescription(
   as_dict->ForEach(print_one_element);
 }
 
+// ModuleHook
+
+Target::ModuleHook::ModuleHook(lldb::TargetSP target_sp, lldb::user_id_t uid)
+    : UserID(uid), m_target_sp(std::move(target_sp)) {}
+
+Target::ModuleHook::ModuleHook(const ModuleHook &rhs)
+    : UserID(rhs.GetID()), m_target_sp(rhs.m_target_sp), m_active(rhs.m_active),
+      m_fire_on_unload(rhs.m_fire_on_unload) {}
+
+void Target::ModuleHook::GetDescription(Stream &s,
+                                        lldb::DescriptionLevel level) const {
+  s.Printf("Hook: %" PRIu64 "\n", GetID());
+  if (level == eDescriptionLevelBrief)
+    return;
+  s.IndentMore();
+  s.Indent();
+  s.Printf("State: %s\n", m_active ? "enabled" : "disabled");
+  if (m_fire_on_unload) {
+    s.Indent();
+    s.PutCString("Fires on: load, unload\n");
+  }
+  GetSubclassDescription(s, level);
+  s.IndentLess();
+}
+
+// ModuleHookCommandLine
+
+void Target::ModuleHookCommandLine::SetActionFromString(
+    const std::string &string) {
+  GetCommands().SplitIntoLines(string);
+}
+
+void Target::ModuleHookCommandLine::SetActionFromStrings(
+    const std::vector<std::string> &strings) {
+  for (const auto &string : strings)
+    GetCommands().AppendString(string.c_str());
+}
+
+void Target::ModuleHookCommandLine::GetSubclassDescription(
+    Stream &s, lldb::DescriptionLevel level) const {
+  if (level == eDescriptionLevelBrief) {
+    if (m_commands.GetSize() == 1)
+      s.PutCString(m_commands.GetStringAtIndex(0));
+    else
+      s.Printf("%" PRIu64 " commands", (uint64_t)m_commands.GetSize());
+    return;
+  }
+
+  s.Indent("Commands: \n");
+  s.IndentMore();
+  for (uint32_t i = 0; i < m_commands.GetSize(); i++) {
+    s.Indent(m_commands.GetStringAtIndex(i));
+    s.PutCString("\n");
+  }
+  s.IndentLess();
+}
+
+void Target::ModuleHookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
+  if (!m_commands.GetSize())
+    return;
+
+  TargetSP target_sp = GetTarget();
+  if (!target_sp)
+    return;
+
+  CommandReturnObject result(false);
+  result.SetImmediateOutputStream(output_sp);
+  result.SetInteractive(false);
+  Debugger &debugger = target_sp->GetDebugger();
+
+  ExecutionContext exe_ctx;
+  if (target_sp->GetProcessSP())
+    exe_ctx.SetContext(target_sp->GetProcessSP());
+  else
+    exe_ctx.SetContext(target_sp, false);
+
+  CommandInterpreterRunOptions options;
+  options.SetStopOnContinue(true);
+  options.SetStopOnError(true);
+  options.SetEchoCommands(false);
+  options.SetPrintResults(true);
+  options.SetPrintErrors(true);
+  options.SetAddToHistory(false);
+
+  bool old_async = debugger.GetAsyncExecution();
+  debugger.SetAsyncExecution(true);
+  debugger.GetCommandInterpreter().HandleCommands(GetCommands(), exe_ctx,
+                                                  options, result);
+  debugger.SetAsyncExecution(old_async);
+}
+
+void Target::ModuleHookCommandLine::HandleModuleUnloaded(StreamSP output_sp) {
+  // Command-based hooks run the same commands on unload as on load.
+  HandleModuleLoaded(output_sp);
+}
+
+// ModuleHookScripted
+
+Status Target::ModuleHookScripted::SetScriptCallback(
+    std::string class_name, StructuredData::ObjectSP extra_args_sp) {
+  ScriptInterpreter *script_interp =
+      GetTarget()->GetDebugger().GetScriptInterpreter();
+  if (!script_interp)
+    return Status::FromErrorString("No script interpreter installed.");
+
+  m_interface_sp = script_interp->CreateScriptedModuleHookInterface();
+  if (!m_interface_sp)
+    return Status::FromErrorStringWithFormat(
+        "ScriptedModuleHook::%s () - ERROR: %s", __FUNCTION__,
+        "Script interpreter couldn't create Scripted Module Hook Interface");
+
+  m_class_name = std::move(class_name);
+  m_extra_args.SetObjectSP(extra_args_sp);
+
+  auto obj_or_err = m_interface_sp->CreatePluginObject(
+      m_class_name, GetTarget(), m_extra_args);
+  if (!obj_or_err)
+    return Status::FromError(obj_or_err.takeError());
+
+  StructuredData::ObjectSP object_sp = *obj_or_err;
+  if (!object_sp || !object_sp->IsValid())
+    return Status::FromErrorStringWithFormat(
+        "ScriptedModuleHook::%s () - ERROR: %s", __FUNCTION__,
+        "Failed to create valid script object");
+
+  return {};
+}
+
+void Target::ModuleHookScripted::HandleModuleLoaded(StreamSP output_sp) {
+  if (!m_interface_sp)
+    return;
+
+  StreamSP stream = std::make_shared<StreamString>();
+  m_interface_sp->HandleModuleLoaded(stream);
+  output_sp->PutCString(
+      reinterpret_cast<StreamString *>(stream.get())->GetData());
+}
+
+void Target::ModuleHookScripted::HandleModuleUnloaded(StreamSP output_sp) {
+  if (!m_interface_sp)
+    return;
+
+  StreamSP stream = std::make_shared<StreamString>();
+  m_interface_sp->HandleModuleUnloaded(stream);
+  output_sp->PutCString(
+      reinterpret_cast<StreamString *>(stream.get())->GetData());
+}
+
+void Target::ModuleHookScripted::GetSubclassDescription(
+    Stream &s, lldb::DescriptionLevel level) const {
+  if (level == eDescriptionLevelBrief) {
+    s.PutCString(m_class_name);
+    return;
+  }
+  s.Indent("Class:");
+  s.Printf("%s\n", m_class_name.c_str());
+
+  if (!m_extra_args.IsValid())
+    return;
+  StructuredData::ObjectSP object_sp = m_extra_args.GetObjectSP();
+  if (!object_sp || !object_sp->IsValid())
+    return;
+
+  StructuredData::Dictionary *as_dict = object_sp->GetAsDictionary();
+  if (!as_dict || !as_dict->IsValid())
+    return;
+
+  uint32_t num_keys = as_dict->GetSize();
+  if (num_keys == 0)
+    return;
+
+  s.Indent("Args:\n");
+  s.IndentMore();
+
+  auto print_one_element = [&s](llvm::StringRef key,
+                                StructuredData::Object *object) {
+    s.Indent();
+    s.Format("{0} : {1}\n", key, object->GetStringValue());
+    return true;
+  };
+
+  as_dict->ForEach(print_one_element);
+  s.IndentLess();
+}
+
+// Module Hook management methods
+
+Target::ModuleHookSP Target::CreateModuleHook(ModuleHook::ModuleHookKind kind) {
+  lldb::user_id_t new_uid = ++m_module_hook_next_id;
+  ModuleHookSP hook_sp;
+  switch (kind) {
+  case ModuleHook::ModuleHookKind::CommandBased:
+    hook_sp.reset(new ModuleHookCommandLine(shared_from_this(), new_uid));
+    break;
+  case ModuleHook::ModuleHookKind::ScriptBased:
+    hook_sp.reset(new ModuleHookScripted(shared_from_this(), new_uid));
+    break;
+  }
+  m_module_hooks[new_uid] = hook_sp;
+  return hook_sp;
+}
+
+void Target::UndoCreateModuleHook(lldb::user_id_t uid) {
+  if (!RemoveModuleHookByID(uid))
+    return;
+  if (uid > 0)
+    --m_module_hook_next_id;
+}
+
+bool Target::RemoveModuleHookByID(lldb::user_id_t uid) {
+  size_t num_removed = m_module_hooks.erase(uid);
+  return (num_removed != 0);
+}
+
+void Target::RemoveAllModuleHooks() { m_module_hooks.clear(); }
+
+Target::ModuleHookSP Target::GetModuleHookByID(lldb::user_id_t uid) {
+  auto iter = m_module_hooks.find(uid);
+  if (iter == m_module_hooks.end())
+    return {};
+  return iter->second;
+}
+
+Target::ModuleHookSP Target::GetModuleHookAtIndex(size_t index) {
+  if (index >= m_module_hooks.size())
+    return {};
+  auto iter = m_module_hooks.begin();
+  std::advance(iter, index);
+  return iter->second;
+}
+
+bool Target::SetModuleHookActiveStateByID(lldb::user_id_t uid,
+                                          bool active_state) {
+  auto iter = m_module_hooks.find(uid);
+  if (iter == m_module_hooks.end())
+    return false;
+  iter->second->SetIsActive(active_state);
+  return true;
+}
+
+void Target::SetAllModuleHooksActiveState(bool active_state) {
+  for (auto &[_, hook] : m_module_hooks)
+    hook->SetIsActive(active_state);
+}
+
+void Target::RunModuleHooks(bool is_load) {
+  if (m_module_hooks.empty())
+    return;
+
+  StreamSP output_sp = m_debugger.GetAsyncOutputStream();
+
+  for (auto &[_, hook_sp] : m_module_hooks) {
+    if (!hook_sp->IsActive())
+      continue;
+    if (!is_load && !hook_sp->GetFireOnUnload())
+      continue;
+    if (is_load)
+      hook_sp->HandleModuleLoaded(output_sp);
+    else
+      hook_sp->HandleModuleUnloaded(output_sp);
+  }
+
+  output_sp->Flush();
+}
+
 static constexpr OptionEnumValueElement g_dynamic_value_types[] = {
     {
         eNoDynamicValues,
diff --git a/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
new file mode 100644
index 0000000000000..51f4f386b2b0a
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
@@ -0,0 +1,19 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_invalid_arg(self):
+        self.expect(
+            "target module-hook delete -1",
+            error=True,
+            startstr='error: invalid module hook id: "-1".',
+        )
+        self.expect(
+            "target module-hook delete abcdfx",
+            error=True,
+            startstr='error: invalid module hook id: "abcdfx".',
+        )
diff --git a/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
new file mode 100644
index 0000000000000..455e555072f8e
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
@@ -0,0 +1,19 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_invalid_arg(self):
+        self.expect(
+            "target module-hook disable -1",
+            error=True,
+            startstr='error: invalid module hook id: "-1".',
+        )
+        self.expect(
+            "target module-hook disable abcdfx",
+            error=True,
+            startstr='error: invalid module hook id: "abcdfx".',
+        )
diff --git a/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
new file mode 100644
index 0000000000000..12f1d66ba03d0
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
@@ -0,0 +1,19 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_invalid_arg(self):
+        self.expect(
+            "target module-hook enable -1",
+            error=True,
+            startstr='error: invalid module hook id: "-1".',
+        )
+        self.expect(
+            "target module-hook enable abcdfx",
+            error=True,
+            startstr='error: invalid module hook id: "abcdfx".',
+        )
diff --git a/lldb/test/Shell/Commands/command-module-hook-fire.test b/lldb/test/Shell/Commands/command-module-hook-fire.test
new file mode 100644
index 0000000000000..84791d91351e1
--- /dev/null
+++ b/lldb/test/Shell/Commands/command-module-hook-fire.test
@@ -0,0 +1,10 @@
+# Test that module hooks fire when modules are loaded.
+#
+# RUN: %clang_host -g %S/Inputs/main.c -o %t
+# RUN: %lldb -b -o 'file %t' \
+# RUN:   -o 'target module-hook add -o "script print(\"HOOK_FIRED\")"' \
+# RUN:   -o 'target modules add %t' \
+# RUN:   2>&1 | FileCheck %s
+
+# CHECK: Module hook #1 added.
+# CHECK: HOOK_FIRED
diff --git a/lldb/test/Shell/Commands/command-module-hook.test b/lldb/test/Shell/Commands/command-module-hook.test
new file mode 100644
index 0000000000000..3cc69a07732f6
--- /dev/null
+++ b/lldb/test/Shell/Commands/command-module-hook.test
@@ -0,0 +1,113 @@
+# Test module hook add/list/delete/enable/disable commands.
+#
+# RUN: %lldb -b -s %s 2>&1 | FileCheck %s
+
+# Adding hooks assigns incrementing IDs.
+target module-hook add -o "script print('hook1')"
+# CHECK: Module hook #1 added.
+target module-hook add -o "script print('hook2')"
+# CHECK: Module hook #2 added.
+target module-hook add -o "script print('hook3')"
+# CHECK: Module hook #3 added.
+
+# List all hooks.
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 1
+# CHECK:   State: enabled
+# CHECK:   Commands:
+# CHECK:       script print('hook1')
+# CHECK: Hook: 2
+# CHECK:   State: enabled
+# CHECK:   Commands:
+# CHECK:       script print('hook2')
+# CHECK: Hook: 3
+# CHECK:   State: enabled
+# CHECK:   Commands:
+# CHECK:       script print('hook3')
+
+# Disable a hook.
+target module-hook disable 2
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 1
+# CHECK:   State: enabled
+# CHECK: Hook: 2
+# CHECK:   State: disabled
+# CHECK: Hook: 3
+# CHECK:   State: enabled
+
+# Re-enable the hook.
+target module-hook enable 2
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 1
+# CHECK:   State: enabled
+# CHECK: Hook: 2
+# CHECK:   State: enabled
+# CHECK: Hook: 3
+# CHECK:   State: enabled
+
+# Delete a hook in the middle.
+target module-hook delete 2
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK:     Hook: 1
+# CHECK-NOT: Hook: 2
+# CHECK:     Hook: 3
+
+# New hook gets a new ID (not reused).
+target module-hook add -o "script print('hook4')"
+# CHECK: Module hook #4 added.
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK:     Hook: 1
+# CHECK-NOT: Hook: 2
+# CHECK:     Hook: 3
+# CHECK:     Hook: 4
+
+# Delete all hooks.
+target module-hook delete
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: No module hooks.
+
+# Add a hook with --on-unload and verify the description.
+target module-hook add -u -o "script print('load+unload')"
+# CHECK: Module hook #5 added.
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 5
+# CHECK:   State: enabled
+# CHECK:   Fires on: load, unload
+# CHECK:   Commands:
+# CHECK:       script print('load+unload')
+
+# Add a hook with multiple one-liner commands.
+target module-hook add -o "script print('first')" -o "script print('second')"
+# CHECK: Module hook #6 added.
+target module-hook list
+# CHECK: Hook: 6
+# CHECK:   State: enabled
+# CHECK-NOT: Fires on:
+# CHECK:   Commands:
+# CHECK:       script print('first')
+# CHECK:       script print('second')
+
+# Disable all hooks.
+target module-hook disable
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 5
+# CHECK:   State: disabled
+# CHECK: Hook: 6
+# CHECK:   State: disabled
+
+# Enable all hooks.
+target module-hook enable
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 5
+# CHECK:   State: enabled
+# CHECK: Hook: 6
+# CHECK:   State: enabled

>From 6a635e28f40cb535d8a3b7b063345eca88a49080 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Mon, 9 Mar 2026 10:53:25 -0700
Subject: [PATCH 2/7] Rename module-hook to avoid ambiguity with modules

---
 lldb/source/Commands/CommandObjectTarget.cpp  | 38 ++++++------
 lldb/source/Commands/Options.td               |  6 +-
 .../delete/TestTargetModuleHookDelete.py      |  4 +-
 .../disable/TestTargetModuleHookDisable.py    |  4 +-
 .../enable/TestTargetModuleHookEnable.py      |  4 +-
 .../Commands/command-module-hook-fire.test    |  2 +-
 .../Shell/Commands/command-module-hook.test   | 62 +++++++++----------
 7 files changed, 60 insertions(+), 60 deletions(-)

diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index b18705e0c6913..d62c227ee05f0 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5335,7 +5335,7 @@ class CommandObjectMultiwordTargetStopHooks : public CommandObjectMultiword {
 
 #pragma mark CommandObjectTargetModuleHookAdd
 
-#define LLDB_OPTIONS_target_module_hook_add
+#define LLDB_OPTIONS_target_modulehook_add
 #include "CommandOptions.inc"
 
 class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
@@ -5347,14 +5347,14 @@ class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
     ~CommandOptions() override = default;
 
     llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
-      return llvm::ArrayRef(g_target_module_hook_add_options);
+      return llvm::ArrayRef(g_target_modulehook_add_options);
     }
 
     Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
                           ExecutionContext *execution_context) override {
       Status error;
       const int short_option =
-          g_target_module_hook_add_options[option_idx].short_option;
+          g_target_modulehook_add_options[option_idx].short_option;
       switch (short_option) {
       case 'o':
         m_use_one_liner = true;
@@ -5382,10 +5382,10 @@ class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
 
   CommandObjectTargetModuleHookAdd(CommandInterpreter &interpreter)
       : CommandObjectParsed(
-            interpreter, "target module-hook add",
+            interpreter, "target modulehook add",
             "Add a hook to be executed whenever modules are loaded into the "
             "target.",
-            "target module-hook add"),
+            "target modulehook add"),
         IOHandlerDelegateMultiline("DONE",
                                    IOHandlerDelegate::Completion::LLDBCommand),
         m_python_class_options("scripted module hook", false, 'P') {
@@ -5393,15 +5393,15 @@ class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
 Command-based module hooks allow running LLDB commands every time modules are
 loaded into the target. For example:
 
-    target module-hook add -o "script print('module loaded')"
+    target modulehook add -o "script print('module loaded')"
 
 Use --on-unload (-u) to also fire the hook when modules are unloaded:
 
-    target module-hook add -u -o "script print('module event')"
+    target modulehook add -u -o "script print('module event')"
 
 Python-based module hooks allow running a Python class:
 
-    target module-hook add -P MyHook
+    target modulehook add -P MyHook
 
 The Python class should implement:
 
@@ -5520,9 +5520,9 @@ called when --on-unload is specified.
 class CommandObjectTargetModuleHookDelete : public CommandObjectParsed {
 public:
   CommandObjectTargetModuleHookDelete(CommandInterpreter &interpreter)
-      : CommandObjectParsed(interpreter, "target module-hook delete",
+      : CommandObjectParsed(interpreter, "target modulehook delete",
                             "Delete a module-hook.",
-                            "target module-hook delete [<id>]") {
+                            "target modulehook delete [<id>]") {
     AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
   }
 
@@ -5602,9 +5602,9 @@ class CommandObjectTargetModuleHookEnableDisable : public CommandObjectParsed {
 class CommandObjectTargetModuleHookList : public CommandObjectParsed {
 public:
   CommandObjectTargetModuleHookList(CommandInterpreter &interpreter)
-      : CommandObjectParsed(interpreter, "target module-hook list",
+      : CommandObjectParsed(interpreter, "target modulehook list",
                             "List all module-hooks.",
-                            "target module-hook list") {}
+                            "target modulehook list") {}
 
   ~CommandObjectTargetModuleHookList() override = default;
 
@@ -5632,9 +5632,9 @@ class CommandObjectMultiwordTargetModuleHooks : public CommandObjectMultiword {
 public:
   CommandObjectMultiwordTargetModuleHooks(CommandInterpreter &interpreter)
       : CommandObjectMultiword(
-            interpreter, "target module-hook",
+            interpreter, "target modulehook",
             "Commands for operating on debugger target module-hooks.",
-            "target module-hook <subcommand> [<subcommand-options>]") {
+            "target modulehook <subcommand> [<subcommand-options>]") {
     LoadSubCommand("add", CommandObjectSP(new CommandObjectTargetModuleHookAdd(
                               interpreter)));
     LoadSubCommand(
@@ -5643,13 +5643,13 @@ class CommandObjectMultiwordTargetModuleHooks : public CommandObjectMultiword {
     LoadSubCommand(
         "disable",
         CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
-            interpreter, false, "target module-hook disable [<id>]",
-            "Disable a module-hook.", "target module-hook disable")));
+            interpreter, false, "target modulehook disable [<id>]",
+            "Disable a module-hook.", "target modulehook disable")));
     LoadSubCommand(
         "enable",
         CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
-            interpreter, true, "target module-hook enable [<id>]",
-            "Enable a module-hook.", "target module-hook enable")));
+            interpreter, true, "target modulehook enable [<id>]",
+            "Enable a module-hook.", "target modulehook enable")));
     LoadSubCommand(
         "list",
         CommandObjectSP(new CommandObjectTargetModuleHookList(interpreter)));
@@ -5950,7 +5950,7 @@ CommandObjectMultiwordTarget::CommandObjectMultiwordTarget(
   LoadSubCommand(
       "stop-hook",
       CommandObjectSP(new CommandObjectMultiwordTargetStopHooks(interpreter)));
-  LoadSubCommand("module-hook",
+  LoadSubCommand("modulehook",
                  CommandObjectSP(
                      new CommandObjectMultiwordTargetModuleHooks(interpreter)));
   LoadSubCommand("modules",
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index 8eee059b7c9c0..50fec369330de 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1901,13 +1901,13 @@ let Command = "target stop_hook list" in {
         Desc<"Show debugger ${i}nternal stop hooks.">;
 }
 
-let Command = "target module_hook add" in {
-  def target_module_hook_add_one_liner
+let Command = "target modulehook add" in {
+  def target_modulehook_add_one_liner
       : Option<"one-liner", "o">,
         Arg<"OneLiner">,
         Desc<"Add a command for the module hook. Can be specified more than "
              "once, and commands will be run in the order they appear.">;
-  def target_module_hook_add_on_unload
+  def target_modulehook_add_on_unload
       : Option<"on-unload", "u">,
         Desc<"Also fire the hook when modules are ${u}nloaded.">;
 }
diff --git a/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
index 51f4f386b2b0a..a071ecd73e883 100644
--- a/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
+++ b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
@@ -8,12 +8,12 @@ class TestCase(TestBase):
     @no_debug_info_test
     def test_invalid_arg(self):
         self.expect(
-            "target module-hook delete -1",
+            "target modulehook delete -1",
             error=True,
             startstr='error: invalid module hook id: "-1".',
         )
         self.expect(
-            "target module-hook delete abcdfx",
+            "target modulehook delete abcdfx",
             error=True,
             startstr='error: invalid module hook id: "abcdfx".',
         )
diff --git a/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
index 455e555072f8e..1541b0006b0c0 100644
--- a/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
+++ b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
@@ -8,12 +8,12 @@ class TestCase(TestBase):
     @no_debug_info_test
     def test_invalid_arg(self):
         self.expect(
-            "target module-hook disable -1",
+            "target modulehook disable -1",
             error=True,
             startstr='error: invalid module hook id: "-1".',
         )
         self.expect(
-            "target module-hook disable abcdfx",
+            "target modulehook disable abcdfx",
             error=True,
             startstr='error: invalid module hook id: "abcdfx".',
         )
diff --git a/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
index 12f1d66ba03d0..dbb16cd10022b 100644
--- a/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
+++ b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
@@ -8,12 +8,12 @@ class TestCase(TestBase):
     @no_debug_info_test
     def test_invalid_arg(self):
         self.expect(
-            "target module-hook enable -1",
+            "target modulehook enable -1",
             error=True,
             startstr='error: invalid module hook id: "-1".',
         )
         self.expect(
-            "target module-hook enable abcdfx",
+            "target modulehook enable abcdfx",
             error=True,
             startstr='error: invalid module hook id: "abcdfx".',
         )
diff --git a/lldb/test/Shell/Commands/command-module-hook-fire.test b/lldb/test/Shell/Commands/command-module-hook-fire.test
index 84791d91351e1..5876ea05fb1fe 100644
--- a/lldb/test/Shell/Commands/command-module-hook-fire.test
+++ b/lldb/test/Shell/Commands/command-module-hook-fire.test
@@ -2,7 +2,7 @@
 #
 # RUN: %clang_host -g %S/Inputs/main.c -o %t
 # RUN: %lldb -b -o 'file %t' \
-# RUN:   -o 'target module-hook add -o "script print(\"HOOK_FIRED\")"' \
+# RUN:   -o 'target modulehook add -o "script print(\"HOOK_FIRED\")"' \
 # RUN:   -o 'target modules add %t' \
 # RUN:   2>&1 | FileCheck %s
 
diff --git a/lldb/test/Shell/Commands/command-module-hook.test b/lldb/test/Shell/Commands/command-module-hook.test
index 3cc69a07732f6..4118a6f18501a 100644
--- a/lldb/test/Shell/Commands/command-module-hook.test
+++ b/lldb/test/Shell/Commands/command-module-hook.test
@@ -3,16 +3,16 @@
 # RUN: %lldb -b -s %s 2>&1 | FileCheck %s
 
 # Adding hooks assigns incrementing IDs.
-target module-hook add -o "script print('hook1')"
+target modulehook add -o "script print('hook1')"
 # CHECK: Module hook #1 added.
-target module-hook add -o "script print('hook2')"
+target modulehook add -o "script print('hook2')"
 # CHECK: Module hook #2 added.
-target module-hook add -o "script print('hook3')"
+target modulehook add -o "script print('hook3')"
 # CHECK: Module hook #3 added.
 
 # List all hooks.
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: Hook: 1
 # CHECK:   State: enabled
 # CHECK:   Commands:
@@ -27,9 +27,9 @@ target module-hook list
 # CHECK:       script print('hook3')
 
 # Disable a hook.
-target module-hook disable 2
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook disable 2
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: Hook: 1
 # CHECK:   State: enabled
 # CHECK: Hook: 2
@@ -38,9 +38,9 @@ target module-hook list
 # CHECK:   State: enabled
 
 # Re-enable the hook.
-target module-hook enable 2
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook enable 2
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: Hook: 1
 # CHECK:   State: enabled
 # CHECK: Hook: 2
@@ -49,34 +49,34 @@ target module-hook list
 # CHECK:   State: enabled
 
 # Delete a hook in the middle.
-target module-hook delete 2
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook delete 2
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK:     Hook: 1
 # CHECK-NOT: Hook: 2
 # CHECK:     Hook: 3
 
 # New hook gets a new ID (not reused).
-target module-hook add -o "script print('hook4')"
+target modulehook add -o "script print('hook4')"
 # CHECK: Module hook #4 added.
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK:     Hook: 1
 # CHECK-NOT: Hook: 2
 # CHECK:     Hook: 3
 # CHECK:     Hook: 4
 
 # Delete all hooks.
-target module-hook delete
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook delete
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: No module hooks.
 
 # Add a hook with --on-unload and verify the description.
-target module-hook add -u -o "script print('load+unload')"
+target modulehook add -u -o "script print('load+unload')"
 # CHECK: Module hook #5 added.
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: Hook: 5
 # CHECK:   State: enabled
 # CHECK:   Fires on: load, unload
@@ -84,9 +84,9 @@ target module-hook list
 # CHECK:       script print('load+unload')
 
 # Add a hook with multiple one-liner commands.
-target module-hook add -o "script print('first')" -o "script print('second')"
+target modulehook add -o "script print('first')" -o "script print('second')"
 # CHECK: Module hook #6 added.
-target module-hook list
+target modulehook list
 # CHECK: Hook: 6
 # CHECK:   State: enabled
 # CHECK-NOT: Fires on:
@@ -95,18 +95,18 @@ target module-hook list
 # CHECK:       script print('second')
 
 # Disable all hooks.
-target module-hook disable
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook disable
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: Hook: 5
 # CHECK:   State: disabled
 # CHECK: Hook: 6
 # CHECK:   State: disabled
 
 # Enable all hooks.
-target module-hook enable
-target module-hook list
-# CHECK: (lldb) target module-hook list
+target modulehook enable
+target modulehook list
+# CHECK: (lldb) target modulehook list
 # CHECK: Hook: 5
 # CHECK:   State: enabled
 # CHECK: Hook: 6

>From d2a0b5317ce81e395d880954b8bcbbe01784f047 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Mon, 9 Mar 2026 12:09:07 -0700
Subject: [PATCH 3/7] fix suggested fixes

---
 .../Interfaces/ScriptedModuleHookInterface.h   |  3 +--
 lldb/include/lldb/Target/Target.h              | 18 +++++++++++-------
 lldb/source/Commands/CommandObjectTarget.cpp   |  2 +-
 .../ScriptedModuleHookPythonInterface.h        |  3 +--
 4 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
index 7a06349eb9161..bbf9531045cde 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
@@ -1,5 +1,4 @@
-//===-- ScriptedModuleHookInterface.h ----------------------------*- C++
-//-*-===//
+//===-- ScriptedModuleHookInterface.h ----------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index bf5582d30921d..39f198c19f608 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1643,8 +1643,9 @@ class Target : public std::enable_shared_from_this<Target>,
 
   // Target Module Hooks
   //
-  // Module hooks fire whenever modules are loaded or unloaded from the
-  // target (via ModulesDidLoad / ModulesDidUnload).
+  // Module hooks fire whenever modules are loaded into the target
+  // (via ModulesDidLoad). Optionally, they can also fire on unload
+  // (via ModulesDidUnload) when the fire_on_unload flag is set.
   class ModuleHook : public UserID {
   public:
     ModuleHook(const ModuleHook &rhs);
@@ -1683,10 +1684,10 @@ class Target : public std::enable_shared_from_this<Target>,
     void SetActionFromString(const std::string &string);
     void SetActionFromStrings(const std::vector<std::string> &strings);
 
-    void HandleModuleLoaded(lldb::StreamSP output) override;
-    void HandleModuleUnloaded(lldb::StreamSP output) override;
     void GetSubclassDescription(Stream &s,
                                 lldb::DescriptionLevel level) const override;
+    void HandleModuleLoaded(lldb::StreamSP output) override;
+    void HandleModuleUnloaded(lldb::StreamSP output) override;
 
   private:
     StringList m_commands;
@@ -1700,15 +1701,15 @@ class Target : public std::enable_shared_from_this<Target>,
   public:
     ~ModuleHookScripted() override = default;
 
+    void GetSubclassDescription(Stream &s,
+                                lldb::DescriptionLevel level) const override;
+
     void HandleModuleLoaded(lldb::StreamSP output) override;
     void HandleModuleUnloaded(lldb::StreamSP output) override;
 
     Status SetScriptCallback(std::string class_name,
                              StructuredData::ObjectSP extra_args_sp);
 
-    void GetSubclassDescription(Stream &s,
-                                lldb::DescriptionLevel level) const override;
-
   private:
     std::string m_class_name;
     StructuredDataImpl m_extra_args;
@@ -1723,6 +1724,9 @@ class Target : public std::enable_shared_from_this<Target>,
 
   ModuleHookSP CreateModuleHook(ModuleHook::ModuleHookKind kind);
 
+  /// Removes the most recently created module hook. Used to roll back a
+  /// hook creation when an error occurs (e.g., invalid script class name
+  /// or empty interactive input).
   void UndoCreateModuleHook(lldb::user_id_t uid);
 
   bool RemoveModuleHookByID(lldb::user_id_t uid);
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index d62c227ee05f0..a83ec3a47693b 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5401,7 +5401,7 @@ Use --on-unload (-u) to also fire the hook when modules are unloaded:
 
 Python-based module hooks allow running a Python class:
 
-    target modulehook add -P MyHook
+    target modulehook add -P mymodule.MyHook
 
 The Python class should implement:
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
index 1a5f4f14c0eab..c63db160e7dc8 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
@@ -1,5 +1,4 @@
-//===-- ScriptedModuleHookPythonInterface.h ----------------------*- C++
-//-*-===//
+//===-- ScriptedModuleHookPythonInterface.h ----------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.

>From fae314ad50e0f7b1687d504f4b1653e2aa6ccb64 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Tue, 10 Mar 2026 10:36:06 -0700
Subject: [PATCH 4/7] New hook logic

---
 .../Interfaces/ScriptedHookInterface.h        |  38 ++
 .../lldb/Interpreter/ScriptInterpreter.h      |   3 +-
 lldb/include/lldb/Target/Target.h             | 134 +++--
 lldb/include/lldb/lldb-forward.h              |   6 +-
 lldb/source/Commands/CommandObjectTarget.cpp  | 557 ++++++++++++++----
 lldb/source/Commands/Options.td               |  75 ++-
 .../ScriptInterpreter/Python/CMakeLists.txt   |   2 +-
 .../ScriptInterpreterPythonInterfaces.cpp     |   4 +-
 .../ScriptInterpreterPythonInterfaces.h       |   2 +-
 .../ScriptedHookPythonInterface.cpp           |  80 +++
 .../Interfaces/ScriptedHookPythonInterface.h  |  50 ++
 .../ScriptedModuleHookPythonInterface.cpp     |   2 +-
 .../Python/ScriptInterpreterPython.cpp        |   6 +-
 .../Python/ScriptInterpreterPythonImpl.h      |   3 +-
 lldb/source/Target/Target.cpp                 | 290 +++++++--
 .../delete/TestTargetModuleHookDelete.py      |   8 +-
 .../disable/TestTargetModuleHookDisable.py    |   8 +-
 .../enable/TestTargetModuleHookEnable.py      |   8 +-
 .../Commands/command-module-hook-fire.test    |   4 +-
 .../Shell/Commands/command-module-hook.test   | 126 ++--
 20 files changed, 1130 insertions(+), 276 deletions(-)
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
new file mode 100644
index 0000000000000..03053ed26879c
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
@@ -0,0 +1,38 @@
+//===-- ScriptedHookInterface.h ---------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H
+
+#include "lldb/lldb-private.h"
+
+#include "ScriptedInterface.h"
+
+namespace lldb_private {
+class ScriptedHookInterface : public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
+                     const StructuredDataImpl &args_sp) = 0;
+
+  /// Called when modules are loaded into the target.
+  virtual void HandleModuleLoaded(lldb::StreamSP &output_sp) {}
+
+  /// Called when modules are unloaded from the target. Optional.
+  virtual void HandleModuleUnloaded(lldb::StreamSP &output_sp) {}
+
+  /// Called when the process stops. Returns "should_stop" -- if false, the
+  /// process will continue. Defaults to true (stop on unimplemented).
+  virtual llvm::Expected<bool> HandleStop(ExecutionContext &exe_ctx,
+                                          lldb::StreamSP &output_sp) {
+    return true;
+  }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDHOOKINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 402af875c7eb5..07e24d7e82b55 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -562,8 +562,7 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
-  virtual lldb::ScriptedModuleHookInterfaceSP
-  CreateScriptedModuleHookInterface() {
+  virtual lldb::ScriptedHookInterfaceSP CreateScriptedHookInterface() {
     return {};
   }
 
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 39f198c19f608..329d9edafebd4 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1641,44 +1641,98 @@ class Target : public std::enable_shared_from_this<Target>,
 
   typedef std::shared_ptr<StopHook> StopHookSP;
 
-  // Target Module Hooks
+  // Target Hooks
   //
-  // Module hooks fire whenever modules are loaded into the target
-  // (via ModulesDidLoad). Optionally, they can also fire on unload
-  // (via ModulesDidUnload) when the fire_on_unload flag is set.
-  class ModuleHook : public UserID {
+  // Hooks fire on lifecycle events. By default, they fire when modules
+  // are loaded into the target (via ModulesDidLoad). They can also fire
+  // on module unload (via ModulesDidUnload) and on process stop (via
+  // RunStopHooks), controlled by the event mask.
+  class Hook : public UserID {
   public:
-    ModuleHook(const ModuleHook &rhs);
-    virtual ~ModuleHook() = default;
+    Hook(const Hook &rhs);
+    virtual ~Hook() = default;
 
-    enum class ModuleHookKind : uint32_t { CommandBased = 0, ScriptBased };
+    enum class HookKind : uint32_t { CommandBased = 0, ScriptBased };
+
+    /// Event mask bits controlling when this hook fires.
+    enum EventMask : uint32_t {
+      kModulesLoaded = (1u << 0),
+      kModulesUnloaded = (1u << 1),
+      kProcessStop = (1u << 2),
+    };
 
     lldb::TargetSP &GetTarget() { return m_target_sp; }
 
     bool IsActive() { return m_active; }
     void SetIsActive(bool is_active) { m_active = is_active; }
 
-    bool GetFireOnUnload() const { return m_fire_on_unload; }
-    void SetFireOnUnload(bool fire) { m_fire_on_unload = fire; }
+    uint32_t GetEventMask() const { return m_event_mask; }
+    void SetEventMask(uint32_t mask) { m_event_mask = mask; }
+    void AddEvent(uint32_t event) { m_event_mask |= event; }
+    void RemoveEvent(uint32_t event) { m_event_mask &= ~event; }
+    bool FiresOn(uint32_t event) const { return m_event_mask & event; }
 
-    void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
-    virtual void GetSubclassDescription(Stream &s,
-                                        lldb::DescriptionLevel level) const = 0;
+    // Stop-hook features (only relevant when kProcessStop is set)
+
+    /// Set the specifier. The hook will own the specifier.
+    void SetSpecifier(SymbolContextSpecifier *specifier);
+    SymbolContextSpecifier *GetSpecifier() { return m_specifier_sp.get(); }
+
+    /// Check if the execution context passes the specifier and thread spec
+    /// filters. Always returns true if no filters are set.
+    bool ExecutionContextPasses(const ExecutionContext &exe_ctx);
+
+    /// Set the thread specifier. The hook will own the thread specifier.
+    void SetThreadSpecifier(ThreadSpec *specifier);
+    ThreadSpec *GetThreadSpecifier() { return m_thread_spec_up.get(); }
+
+    void SetAutoContinue(bool auto_continue) {
+      m_auto_continue = auto_continue;
+    }
+    bool GetAutoContinue() const { return m_auto_continue; }
+
+    void SetRunAtInitialStop(bool at_initial_stop) {
+      m_at_initial_stop = at_initial_stop;
+    }
+    bool GetRunAtInitialStop() const { return m_at_initial_stop; }
+
+    void SetSuppressOutput(bool suppress_output) {
+      m_suppress_output = suppress_output;
+    }
+    bool GetSuppressOutput() const { return m_suppress_output; }
+
+    // Event handler methods
 
     virtual void HandleModuleLoaded(lldb::StreamSP output) = 0;
     virtual void HandleModuleUnloaded(lldb::StreamSP output) = 0;
 
+    /// Called when the process stops. Returns a StopHookResult indicating
+    /// whether the process should remain stopped or continue.
+    virtual StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
+                                                lldb::StreamSP output) = 0;
+
+    void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
+    virtual void GetSubclassDescription(Stream &s,
+                                        lldb::DescriptionLevel level) const = 0;
+
   protected:
     lldb::TargetSP m_target_sp;
     bool m_active = true;
-    bool m_fire_on_unload = false;
+    uint32_t m_event_mask = kModulesLoaded; // Default: fire on load only
+
+    // Stop-hook filter fields (only used when kProcessStop is set)
+    lldb::SymbolContextSpecifierSP m_specifier_sp;
+    std::unique_ptr<ThreadSpec> m_thread_spec_up;
+    bool m_auto_continue = false;
+    bool m_at_initial_stop = true;
+    bool m_suppress_output = false;
 
-    ModuleHook(lldb::TargetSP target_sp, lldb::user_id_t uid);
+    Hook(lldb::TargetSP target_sp, lldb::user_id_t uid);
   };
 
-  class ModuleHookCommandLine : public ModuleHook {
+  class HookCommandLine : public Hook {
   public:
-    ~ModuleHookCommandLine() override = default;
+    ~HookCommandLine() override = default;
 
     StringList &GetCommands() { return m_commands; }
     void SetActionFromString(const std::string &string);
@@ -1688,24 +1742,28 @@ class Target : public std::enable_shared_from_this<Target>,
                                 lldb::DescriptionLevel level) const override;
     void HandleModuleLoaded(lldb::StreamSP output) override;
     void HandleModuleUnloaded(lldb::StreamSP output) override;
+    StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
+                                        lldb::StreamSP output) override;
 
   private:
     StringList m_commands;
 
-    ModuleHookCommandLine(lldb::TargetSP target_sp, lldb::user_id_t uid)
-        : ModuleHook(target_sp, uid) {}
+    HookCommandLine(lldb::TargetSP target_sp, lldb::user_id_t uid)
+        : Hook(target_sp, uid) {}
     friend class Target;
   };
 
-  class ModuleHookScripted : public ModuleHook {
+  class HookScripted : public Hook {
   public:
-    ~ModuleHookScripted() override = default;
+    ~HookScripted() override = default;
 
     void GetSubclassDescription(Stream &s,
                                 lldb::DescriptionLevel level) const override;
 
     void HandleModuleLoaded(lldb::StreamSP output) override;
     void HandleModuleUnloaded(lldb::StreamSP output) override;
+    StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
+                                        lldb::StreamSP output) override;
 
     Status SetScriptCallback(std::string class_name,
                              StructuredData::ObjectSP extra_args_sp);
@@ -1713,35 +1771,35 @@ class Target : public std::enable_shared_from_this<Target>,
   private:
     std::string m_class_name;
     StructuredDataImpl m_extra_args;
-    lldb::ScriptedModuleHookInterfaceSP m_interface_sp;
+    lldb::ScriptedHookInterfaceSP m_interface_sp;
 
-    ModuleHookScripted(lldb::TargetSP target_sp, lldb::user_id_t uid)
-        : ModuleHook(target_sp, uid) {}
+    HookScripted(lldb::TargetSP target_sp, lldb::user_id_t uid)
+        : Hook(target_sp, uid) {}
     friend class Target;
   };
 
-  typedef std::shared_ptr<ModuleHook> ModuleHookSP;
+  typedef std::shared_ptr<Hook> HookSP;
 
-  ModuleHookSP CreateModuleHook(ModuleHook::ModuleHookKind kind);
+  HookSP CreateHook(Hook::HookKind kind);
 
-  /// Removes the most recently created module hook. Used to roll back a
+  /// Removes the most recently created hook. Used to roll back a
   /// hook creation when an error occurs (e.g., invalid script class name
   /// or empty interactive input).
-  void UndoCreateModuleHook(lldb::user_id_t uid);
+  void UndoCreateHook(lldb::user_id_t uid);
 
-  bool RemoveModuleHookByID(lldb::user_id_t uid);
+  bool RemoveHookByID(lldb::user_id_t uid);
 
-  void RemoveAllModuleHooks();
+  void RemoveAllHooks();
 
-  ModuleHookSP GetModuleHookByID(lldb::user_id_t uid);
+  HookSP GetHookByID(lldb::user_id_t uid);
 
-  bool SetModuleHookActiveStateByID(lldb::user_id_t uid, bool active_state);
+  bool SetHookActiveStateByID(lldb::user_id_t uid, bool active_state);
 
-  void SetAllModuleHooksActiveState(bool active_state);
+  void SetAllHooksActiveState(bool active_state);
 
-  size_t GetNumModuleHooks() const { return m_module_hooks.size(); }
+  size_t GetNumHooks() const { return m_hooks.size(); }
 
-  ModuleHookSP GetModuleHookAtIndex(size_t index);
+  HookSP GetHookAtIndex(size_t index);
 
   void RunModuleHooks(bool is_load);
 
@@ -1931,9 +1989,9 @@ class Target : public std::enable_shared_from_this<Target>,
   bool m_suppress_stop_hooks; /// Used to not run stop hooks for expressions
   bool m_is_dummy_target;
 
-  typedef std::map<lldb::user_id_t, ModuleHookSP> ModuleHookCollection;
-  ModuleHookCollection m_module_hooks;
-  lldb::user_id_t m_module_hook_next_id = 0;
+  typedef std::map<lldb::user_id_t, HookSP> HookCollection;
+  HookCollection m_hooks;
+  lldb::user_id_t m_hook_next_id = 0;
   unsigned m_next_persistent_variable_index = 0;
   lldb::user_id_t m_target_unique_id =
       LLDB_INVALID_GLOBALLY_UNIQUE_TARGET_ID; ///< The globally unique ID
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index fcff135236ad0..67888f7d32ed1 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -191,7 +191,7 @@ class ScriptedFrameInterface;
 class ScriptedFrameProviderInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
-class ScriptedModuleHookInterface;
+class ScriptedHookInterface;
 class ScriptedPlatformInterface;
 class ScriptedProcessInterface;
 class ScriptedStopHookInterface;
@@ -426,8 +426,8 @@ typedef std::unique_ptr<lldb_private::ScriptedProcessInterface>
     ScriptedProcessInterfaceUP;
 typedef std::shared_ptr<lldb_private::ScriptedStopHookInterface>
     ScriptedStopHookInterfaceSP;
-typedef std::shared_ptr<lldb_private::ScriptedModuleHookInterface>
-    ScriptedModuleHookInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedHookInterface>
+    ScriptedHookInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedThreadInterface>
     ScriptedThreadInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index a83ec3a47693b..f8fd4ecbc68f2 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5333,13 +5333,13 @@ class CommandObjectMultiwordTargetStopHooks : public CommandObjectMultiword {
   ~CommandObjectMultiwordTargetStopHooks() override = default;
 };
 
-#pragma mark CommandObjectTargetModuleHookAdd
+#pragma mark CommandObjectTargetHookAdd
 
-#define LLDB_OPTIONS_target_modulehook_add
+#define LLDB_OPTIONS_target_hook_add
 #include "CommandOptions.inc"
 
-class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
-                                         public IOHandlerDelegateMultiline {
+class CommandObjectTargetHookAdd : public CommandObjectParsed,
+                                   public IOHandlerDelegateMultiline {
 public:
   class CommandOptions : public OptionGroup {
   public:
@@ -5347,14 +5347,14 @@ class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
     ~CommandOptions() override = default;
 
     llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
-      return llvm::ArrayRef(g_target_modulehook_add_options);
+      return llvm::ArrayRef(g_target_hook_add_options);
     }
 
     Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
                           ExecutionContext *execution_context) override {
       Status error;
       const int short_option =
-          g_target_modulehook_add_options[option_idx].short_option;
+          g_target_hook_add_options[option_idx].short_option;
       switch (short_option) {
       case 'o':
         m_use_one_liner = true;
@@ -5363,6 +5363,9 @@ class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
       case 'u':
         m_on_unload = true;
         break;
+      case 'S':
+        m_on_stop = true;
+        break;
       default:
         llvm_unreachable("unhandled option");
       }
@@ -5373,48 +5376,60 @@ class CommandObjectTargetModuleHookAdd : public CommandObjectParsed,
       m_use_one_liner = false;
       m_one_liner.clear();
       m_on_unload = false;
+      m_on_stop = false;
     }
 
     std::vector<std::string> m_one_liner;
     bool m_use_one_liner = false;
     bool m_on_unload = false;
+    bool m_on_stop = false;
   };
 
-  CommandObjectTargetModuleHookAdd(CommandInterpreter &interpreter)
+  CommandObjectTargetHookAdd(CommandInterpreter &interpreter)
       : CommandObjectParsed(
-            interpreter, "target modulehook add",
-            "Add a hook to be executed whenever modules are loaded into the "
-            "target.",
-            "target modulehook add"),
+            interpreter, "target hook add",
+            "Add a hook to be executed on target lifecycle events.",
+            "target hook add"),
         IOHandlerDelegateMultiline("DONE",
                                    IOHandlerDelegate::Completion::LLDBCommand),
-        m_python_class_options("scripted module hook", false, 'P') {
+        m_python_class_options("scripted hook", false, 'P') {
     SetHelpLong(R"help(
-Command-based module hooks allow running LLDB commands every time modules are
-loaded into the target. For example:
+Hooks fire on target lifecycle events. By default, hooks fire when modules are
+loaded into the target. Use --on-unload (-u) to also fire on module unload, and
+--on-stop (-S) to also fire when the process stops.
 
-    target modulehook add -o "script print('module loaded')"
+Command-based hooks:
 
-Use --on-unload (-u) to also fire the hook when modules are unloaded:
+    target hook add -o "script print('module loaded')"
+    target hook add -u -o "script print('module event')"
+    target hook add -S -o "bt"
+    target hook add -u -S -o "script print('all events')"
 
-    target modulehook add -u -o "script print('module event')"
+Use 'target hook add-filter' to add stop-event filters to an existing hook:
 
-Python-based module hooks allow running a Python class:
+    target hook add -S -o "bt"
+    target hook add-filter -s mylib.so -n main 1
 
-    target modulehook add -P mymodule.MyHook
+Python-based hooks:
+
+    target hook add -P mymodule.MyHook
+    target hook add -u -S -P mymodule.MyHook
 
 The Python class should implement:
 
     class MyHook:
         def __init__(self, target, extra_args, internal_dict):
             self.target = target
-        def handle_module_loaded(self, stream):
+        def handle_module_loaded(self, stream):    # required
             pass
-        def handle_module_unloaded(self, stream):
+        def handle_module_unloaded(self, stream):  # optional
             pass
+        def handle_stop(self, exe_ctx, stream):    # optional, return bool
+            return True  # True = should_stop
 
-handle_module_loaded is required. handle_module_unloaded is optional and only
-called when --on-unload is specified.
+handle_module_loaded is required. handle_module_unloaded is only called when
+--on-unload is specified. handle_stop is only called when --on-stop is
+specified and should return True to stop or False to continue.
 )help");
     m_all_options.Append(&m_python_class_options,
                          LLDB_OPT_SET_1 | LLDB_OPT_SET_2, LLDB_OPT_SET_2);
@@ -5422,7 +5437,7 @@ called when --on-unload is specified.
     m_all_options.Finalize();
   }
 
-  ~CommandObjectTargetModuleHookAdd() override = default;
+  ~CommandObjectTargetHookAdd() override = default;
 
   Options *GetOptions() override { return &m_all_options; }
 
@@ -5433,75 +5448,78 @@ called when --on-unload is specified.
               io_handler.GetOutputStreamFileSP()) {
         LockedStreamFile locked_stream = output_sp->Lock();
         locked_stream.PutCString(
-            "Enter your module hook command(s). Type 'DONE' to end.\n");
+            "Enter your hook command(s). Type 'DONE' to end.\n");
       }
     }
   }
 
   void IOHandlerInputComplete(IOHandler &io_handler,
                               std::string &line) override {
-    if (m_module_hook_sp) {
+    if (m_hook_sp) {
       if (line.empty()) {
         if (lldb::LockableStreamFileSP error_sp =
                 io_handler.GetErrorStreamFileSP()) {
           LockedStreamFile locked_stream = error_sp->Lock();
-          locked_stream.Printf("error: module hook #%" PRIu64
+          locked_stream.Printf("error: hook #%" PRIu64
                                " aborted, no commands.\n",
-                               m_module_hook_sp->GetID());
+                               m_hook_sp->GetID());
         }
-        GetTarget().UndoCreateModuleHook(m_module_hook_sp->GetID());
+        GetTarget().UndoCreateHook(m_hook_sp->GetID());
       } else {
-        auto *hook = static_cast<Target::ModuleHookCommandLine *>(
-            m_module_hook_sp.get());
+        auto *hook = static_cast<Target::HookCommandLine *>(m_hook_sp.get());
         hook->SetActionFromString(line);
         if (lldb::LockableStreamFileSP output_sp =
                 io_handler.GetOutputStreamFileSP()) {
           LockedStreamFile locked_stream = output_sp->Lock();
-          locked_stream.Printf("Module hook #%" PRIu64 " added.\n",
-                               m_module_hook_sp->GetID());
+          locked_stream.Printf("Hook #%" PRIu64 " added.\n",
+                               m_hook_sp->GetID());
         }
       }
-      m_module_hook_sp.reset();
+      m_hook_sp.reset();
     }
     io_handler.SetIsDone(true);
   }
 
   void DoExecute(Args &command, CommandReturnObject &result) override {
-    m_module_hook_sp.reset();
+    m_hook_sp.reset();
     Target &target = GetTarget();
 
-    Target::ModuleHook::ModuleHookKind hook_kind;
+    Target::Hook::HookKind hook_kind;
     if (m_python_class_options.GetName().empty())
-      hook_kind = Target::ModuleHook::ModuleHookKind::CommandBased;
+      hook_kind = Target::Hook::HookKind::CommandBased;
     else
-      hook_kind = Target::ModuleHook::ModuleHookKind::ScriptBased;
+      hook_kind = Target::Hook::HookKind::ScriptBased;
 
-    Target::ModuleHookSP new_hook_sp = target.CreateModuleHook(hook_kind);
+    Target::HookSP new_hook_sp = target.CreateHook(hook_kind);
 
+    // Build event mask.
+    uint32_t event_mask = Target::Hook::kModulesLoaded;
     if (m_options.m_on_unload)
-      new_hook_sp->SetFireOnUnload(true);
+      event_mask |= Target::Hook::kModulesUnloaded;
+    if (m_options.m_on_stop)
+      event_mask |= Target::Hook::kProcessStop;
+    new_hook_sp->SetEventMask(event_mask);
 
     if (m_options.m_use_one_liner) {
-      auto *hook =
-          static_cast<Target::ModuleHookCommandLine *>(new_hook_sp.get());
+      auto *hook = static_cast<Target::HookCommandLine *>(new_hook_sp.get());
       hook->SetActionFromStrings(m_options.m_one_liner);
-      result.AppendMessageWithFormat("Module hook #%" PRIu64 " added.\n",
+      result.AppendMessageWithFormat("Hook #%" PRIu64 " added.\n",
                                      new_hook_sp->GetID());
     } else if (!m_python_class_options.GetName().empty()) {
-      auto *hook = static_cast<Target::ModuleHookScripted *>(new_hook_sp.get());
+      auto *hook = static_cast<Target::HookScripted *>(new_hook_sp.get());
       Status callback_error =
           hook->SetScriptCallback(m_python_class_options.GetName(),
                                   m_python_class_options.GetStructuredData());
       if (callback_error.Fail()) {
-        result.AppendErrorWithFormat("error: couldn't add module hook: %s\n",
+        result.AppendErrorWithFormat("error: couldn't add hook: %s\n",
                                      callback_error.AsCString());
-        target.UndoCreateModuleHook(new_hook_sp->GetID());
+        target.UndoCreateHook(new_hook_sp->GetID());
         return;
       }
-      result.AppendMessageWithFormat("Module hook #%" PRIu64 " added.\n",
+      result.AppendMessageWithFormat("Hook #%" PRIu64 " added.\n",
                                      new_hook_sp->GetID());
     } else {
-      m_module_hook_sp = new_hook_sp;
+      m_hook_sp = new_hook_sp;
       m_interpreter.GetLLDBCommandsFromIOHandler("> ",   // prompt
                                                  *this); // delegate
     }
@@ -5512,27 +5530,295 @@ called when --on-unload is specified.
   CommandOptions m_options;
   OptionGroupPythonClassWithDict m_python_class_options;
   OptionGroupOptions m_all_options;
-  Target::ModuleHookSP m_module_hook_sp;
+  Target::HookSP m_hook_sp;
 };
 
-#pragma mark CommandObjectTargetModuleHookDelete
+#pragma mark CommandObjectTargetHookAddFilter
 
-class CommandObjectTargetModuleHookDelete : public CommandObjectParsed {
+#define LLDB_OPTIONS_target_hook_add_filter
+#include "CommandOptions.inc"
+
+class CommandObjectTargetHookAddFilter : public CommandObjectParsed {
 public:
-  CommandObjectTargetModuleHookDelete(CommandInterpreter &interpreter)
-      : CommandObjectParsed(interpreter, "target modulehook delete",
-                            "Delete a module-hook.",
-                            "target modulehook delete [<id>]") {
+  class CommandOptions : public Options {
+  public:
+    CommandOptions() = default;
+    ~CommandOptions() override = default;
+
+    llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+      return llvm::ArrayRef(g_target_hook_add_filter_options);
+    }
+
+    Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+                          ExecutionContext *execution_context) override {
+      Status error;
+      const int short_option =
+          g_target_hook_add_filter_options[option_idx].short_option;
+      switch (short_option) {
+      case 's':
+        m_module_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'x': {
+        uint32_t thread_index;
+        if (option_arg.getAsInteger(0, thread_index))
+          error = Status::FromErrorStringWithFormat("invalid thread index '%s'",
+                                                    option_arg.str().c_str());
+        else
+          m_thread_index = thread_index;
+        m_thread_specified = true;
+        break;
+      }
+      case 't': {
+        lldb::tid_t thread_id;
+        if (option_arg.getAsInteger(0, thread_id))
+          error = Status::FromErrorStringWithFormat("invalid thread id '%s'",
+                                                    option_arg.str().c_str());
+        else
+          m_thread_id = thread_id;
+        m_thread_specified = true;
+        break;
+      }
+      case 'T':
+        m_thread_name = std::string(option_arg);
+        m_thread_specified = true;
+        break;
+      case 'q':
+        m_queue_name = std::string(option_arg);
+        m_thread_specified = true;
+        break;
+      case 'f':
+        m_file_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'l': {
+        uint32_t line;
+        if (option_arg.getAsInteger(0, line))
+          error = Status::FromErrorStringWithFormat(
+              "invalid start line number '%s'", option_arg.str().c_str());
+        else
+          m_line_start = line;
+        m_sym_ctx_specified = true;
+        break;
+      }
+      case 'e': {
+        uint32_t line;
+        if (option_arg.getAsInteger(0, line))
+          error = Status::FromErrorStringWithFormat(
+              "invalid end line number '%s'", option_arg.str().c_str());
+        else
+          m_line_end = line;
+        m_sym_ctx_specified = true;
+        break;
+      }
+      case 'c':
+        m_class_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'n':
+        m_function_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'G': {
+        bool value, success;
+        value = OptionArgParser::ToBoolean(option_arg, false, &success);
+        if (success)
+          m_auto_continue = value;
+        else
+          error = Status::FromErrorStringWithFormat(
+              "invalid boolean value '%s' passed for -G option",
+              option_arg.str().c_str());
+        break;
+      }
+      case 'I': {
+        bool value, success;
+        value = OptionArgParser::ToBoolean(option_arg, true, &success);
+        if (success)
+          m_at_initial_stop = value;
+        else
+          error = Status::FromErrorStringWithFormat(
+              "invalid boolean value '%s' passed for -I option",
+              option_arg.str().c_str());
+        break;
+      }
+      default:
+        llvm_unreachable("unhandled option");
+      }
+      return error;
+    }
+
+    void OptionParsingStarting(ExecutionContext *execution_context) override {
+      m_sym_ctx_specified = false;
+      m_thread_specified = false;
+      m_module_name.clear();
+      m_file_name.clear();
+      m_class_name.clear();
+      m_function_name.clear();
+      m_line_start = 0;
+      m_line_end = UINT_MAX;
+      m_thread_id = LLDB_INVALID_THREAD_ID;
+      m_thread_index = UINT32_MAX;
+      m_thread_name.clear();
+      m_queue_name.clear();
+      m_auto_continue = false;
+      m_at_initial_stop = true;
+    }
+
+    bool m_sym_ctx_specified = false;
+    bool m_thread_specified = false;
+    std::string m_module_name;
+    std::string m_file_name;
+    std::string m_class_name;
+    std::string m_function_name;
+    uint32_t m_line_start = 0;
+    uint32_t m_line_end = UINT_MAX;
+    lldb::tid_t m_thread_id = LLDB_INVALID_THREAD_ID;
+    uint32_t m_thread_index = UINT32_MAX;
+    std::string m_thread_name;
+    std::string m_queue_name;
+    bool m_auto_continue = false;
+    bool m_at_initial_stop = true;
+  };
+
+  CommandObjectTargetHookAddFilter(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target hook add-filter",
+            "Add stop-event filters to an existing hook.",
+            "target hook add-filter [<filter-options>] <hook-id>") {
+    SetHelpLong(R"help(
+Adds stop-event filters to an existing hook. Filters control when the hook's
+stop handler fires based on the current execution context (module, function,
+source file/line, thread, etc.).
+
+If the hook does not already fire on process stop, the stop event is
+automatically added to its event mask.
+
+Examples:
+
+    target hook add -S -o "bt"
+    target hook add-filter -s mylib.so 1
+
+    target hook add -S -o "bt"
+    target hook add-filter -n main -G true 1
+)help");
+    AddSimpleArgumentList(eArgTypeStopHookID);
+  }
+
+  ~CommandObjectTargetHookAddFilter() override = default;
+
+  Options *GetOptions() override { return &m_options; }
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+
+    if (command.GetArgumentCount() != 1) {
+      result.AppendError("exactly one hook id is required.");
+      return;
+    }
+
+    lldb::user_id_t hook_id;
+    if (!llvm::to_integer(command.GetArgumentAtIndex(0), hook_id)) {
+      result.AppendErrorWithFormat("invalid hook id: \"%s\".\n",
+                                   command.GetArgumentAtIndex(0));
+      return;
+    }
+
+    Target::HookSP hook_sp = target.GetHookByID(hook_id);
+    if (!hook_sp) {
+      result.AppendErrorWithFormat("unknown hook id: \"%" PRIu64 "\".\n",
+                                   hook_id);
+      return;
+    }
+
+    // Automatically add the stop event if not already present.
+    if (!hook_sp->FiresOn(Target::Hook::kProcessStop))
+      hook_sp->AddEvent(Target::Hook::kProcessStop);
+
+    // Set up symbol context specifier if filter options were provided.
+    if (m_options.m_sym_ctx_specified) {
+      auto specifier_up = std::make_unique<SymbolContextSpecifier>(
+          GetDebugger().GetSelectedTarget());
+
+      if (!m_options.m_module_name.empty())
+        specifier_up->AddSpecification(
+            m_options.m_module_name.c_str(),
+            SymbolContextSpecifier::eModuleSpecified);
+
+      if (!m_options.m_class_name.empty())
+        specifier_up->AddSpecification(
+            m_options.m_class_name.c_str(),
+            SymbolContextSpecifier::eClassOrNamespaceSpecified);
+
+      if (!m_options.m_file_name.empty())
+        specifier_up->AddSpecification(m_options.m_file_name.c_str(),
+                                       SymbolContextSpecifier::eFileSpecified);
+
+      if (m_options.m_line_start != 0)
+        specifier_up->AddLineSpecification(
+            m_options.m_line_start,
+            SymbolContextSpecifier::eLineStartSpecified);
+
+      if (m_options.m_line_end != UINT_MAX)
+        specifier_up->AddLineSpecification(
+            m_options.m_line_end, SymbolContextSpecifier::eLineEndSpecified);
+
+      if (!m_options.m_function_name.empty())
+        specifier_up->AddSpecification(
+            m_options.m_function_name.c_str(),
+            SymbolContextSpecifier::eFunctionSpecified);
+
+      hook_sp->SetSpecifier(specifier_up.release());
+    }
+
+    // Set up thread specifier.
+    if (m_options.m_thread_specified) {
+      ThreadSpec *thread_spec = new ThreadSpec();
+
+      if (m_options.m_thread_id != LLDB_INVALID_THREAD_ID)
+        thread_spec->SetTID(m_options.m_thread_id);
+
+      if (m_options.m_thread_index != UINT32_MAX)
+        thread_spec->SetIndex(m_options.m_thread_index);
+
+      if (!m_options.m_thread_name.empty())
+        thread_spec->SetName(m_options.m_thread_name.c_str());
+
+      if (!m_options.m_queue_name.empty())
+        thread_spec->SetQueueName(m_options.m_queue_name.c_str());
+
+      hook_sp->SetThreadSpecifier(thread_spec);
+    }
+
+    hook_sp->SetAutoContinue(m_options.m_auto_continue);
+    hook_sp->SetRunAtInitialStop(m_options.m_at_initial_stop);
+
+    result.AppendMessageWithFormat("Filter added to hook #%" PRIu64 ".\n",
+                                   hook_id);
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+
+private:
+  CommandOptions m_options;
+};
+
+#pragma mark CommandObjectTargetHookDelete
+
+class CommandObjectTargetHookDelete : public CommandObjectParsed {
+public:
+  CommandObjectTargetHookDelete(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target hook delete", "Delete a hook.",
+                            "target hook delete [<id>]") {
     AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
   }
 
-  ~CommandObjectTargetModuleHookDelete() override = default;
+  ~CommandObjectTargetHookDelete() override = default;
 
 protected:
   void DoExecute(Args &command, CommandReturnObject &result) override {
     Target &target = GetTarget();
     if (command.GetArgumentCount() == 0) {
-      target.RemoveAllModuleHooks();
+      target.RemoveAllHooks();
       result.SetStatus(eReturnStatusSuccessFinishNoResult);
       return;
     }
@@ -5540,12 +5826,12 @@ class CommandObjectTargetModuleHookDelete : public CommandObjectParsed {
     for (size_t i = 0; i < command.GetArgumentCount(); i++) {
       lldb::user_id_t user_id;
       if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
-        result.AppendErrorWithFormat("invalid module hook id: \"%s\".\n",
+        result.AppendErrorWithFormat("invalid hook id: \"%s\".\n",
                                      command.GetArgumentAtIndex(i));
         return;
       }
-      if (!target.RemoveModuleHookByID(user_id)) {
-        result.AppendErrorWithFormat("unknown module hook id: \"%s\".\n",
+      if (!target.RemoveHookByID(user_id)) {
+        result.AppendErrorWithFormat("unknown hook id: \"%s\".\n",
                                      command.GetArgumentAtIndex(i));
         return;
       }
@@ -5554,40 +5840,92 @@ class CommandObjectTargetModuleHookDelete : public CommandObjectParsed {
   }
 };
 
-#pragma mark CommandObjectTargetModuleHookEnableDisable
+#pragma mark CommandObjectTargetHookEnableDisable
 
-class CommandObjectTargetModuleHookEnableDisable : public CommandObjectParsed {
+class CommandObjectTargetHookEnableDisable : public CommandObjectParsed {
 public:
-  CommandObjectTargetModuleHookEnableDisable(CommandInterpreter &interpreter,
-                                             bool enable, const char *name,
-                                             const char *help,
-                                             const char *syntax)
+  CommandObjectTargetHookEnableDisable(CommandInterpreter &interpreter,
+                                       bool enable, const char *name,
+                                       const char *help, const char *syntax)
       : CommandObjectParsed(interpreter, name, help, syntax), m_enable(enable) {
     AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
   }
 
-  ~CommandObjectTargetModuleHookEnableDisable() override = default;
+  ~CommandObjectTargetHookEnableDisable() override = default;
 
 protected:
   void DoExecute(Args &command, CommandReturnObject &result) override {
     Target &target = GetTarget();
-    if (command.GetArgumentCount() == 0) {
-      target.SetAllModuleHooksActiveState(m_enable);
+
+    // Check if the first argument is an event type name for per-event
+    // toggling: module-loaded, module-unloaded, stop-hook.
+    uint32_t event_type = 0;
+    size_t id_start_idx = 0;
+
+    if (command.GetArgumentCount() > 0) {
+      llvm::StringRef first_arg = command.GetArgumentAtIndex(0);
+      if (first_arg == "module-loaded") {
+        event_type = Target::Hook::kModulesLoaded;
+        id_start_idx = 1;
+      } else if (first_arg == "module-unloaded") {
+        event_type = Target::Hook::kModulesUnloaded;
+        id_start_idx = 1;
+      } else if (first_arg == "stop-hook") {
+        event_type = Target::Hook::kProcessStop;
+        id_start_idx = 1;
+      }
+    }
+
+    // If no hook IDs given (after possibly consuming event type), apply to all.
+    if (command.GetArgumentCount() == id_start_idx) {
+      if (event_type) {
+        // Per-event toggle on all hooks.
+        size_t num_hooks = target.GetNumHooks();
+        for (size_t i = 0; i < num_hooks; i++) {
+          Target::HookSP hook_sp = target.GetHookAtIndex(i);
+          if (!hook_sp)
+            continue;
+          if (m_enable)
+            hook_sp->AddEvent(event_type);
+          else
+            hook_sp->RemoveEvent(event_type);
+        }
+      } else {
+        // Whole-hook toggle on all hooks.
+        target.SetAllHooksActiveState(m_enable);
+      }
       result.SetStatus(eReturnStatusSuccessFinishNoResult);
       return;
     }
 
-    for (size_t i = 0; i < command.GetArgumentCount(); i++) {
+    // Process hook IDs.
+    for (size_t i = id_start_idx; i < command.GetArgumentCount(); i++) {
       lldb::user_id_t user_id;
       if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
-        result.AppendErrorWithFormat("invalid module hook id: \"%s\".\n",
+        result.AppendErrorWithFormat("invalid hook id: \"%s\".\n",
                                      command.GetArgumentAtIndex(i));
         return;
       }
-      if (!target.SetModuleHookActiveStateByID(user_id, m_enable)) {
-        result.AppendErrorWithFormat("unknown module hook id: \"%s\".\n",
-                                     command.GetArgumentAtIndex(i));
-        return;
+
+      if (event_type) {
+        // Per-event toggle on a specific hook.
+        Target::HookSP hook_sp = target.GetHookByID(user_id);
+        if (!hook_sp) {
+          result.AppendErrorWithFormat("unknown hook id: \"%s\".\n",
+                                       command.GetArgumentAtIndex(i));
+          return;
+        }
+        if (m_enable)
+          hook_sp->AddEvent(event_type);
+        else
+          hook_sp->RemoveEvent(event_type);
+      } else {
+        // Whole-hook toggle.
+        if (!target.SetHookActiveStateByID(user_id, m_enable)) {
+          result.AppendErrorWithFormat("unknown hook id: \"%s\".\n",
+                                       command.GetArgumentAtIndex(i));
+          return;
+        }
       }
     }
     result.SetStatus(eReturnStatusSuccessFinishNoResult);
@@ -5597,26 +5935,25 @@ class CommandObjectTargetModuleHookEnableDisable : public CommandObjectParsed {
   bool m_enable;
 };
 
-#pragma mark CommandObjectTargetModuleHookList
+#pragma mark CommandObjectTargetHookList
 
-class CommandObjectTargetModuleHookList : public CommandObjectParsed {
+class CommandObjectTargetHookList : public CommandObjectParsed {
 public:
-  CommandObjectTargetModuleHookList(CommandInterpreter &interpreter)
-      : CommandObjectParsed(interpreter, "target modulehook list",
-                            "List all module-hooks.",
-                            "target modulehook list") {}
+  CommandObjectTargetHookList(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target hook list", "List all hooks.",
+                            "target hook list") {}
 
-  ~CommandObjectTargetModuleHookList() override = default;
+  ~CommandObjectTargetHookList() override = default;
 
 protected:
   void DoExecute(Args &command, CommandReturnObject &result) override {
     Target &target = GetTarget();
-    size_t num_hooks = target.GetNumModuleHooks();
+    size_t num_hooks = target.GetNumHooks();
     if (num_hooks == 0) {
-      result.GetOutputStream().PutCString("No module hooks.\n");
+      result.GetOutputStream().PutCString("No hooks.\n");
     } else {
       for (size_t i = 0; i < num_hooks; i++) {
-        Target::ModuleHookSP hook_sp = target.GetModuleHookAtIndex(i);
+        Target::HookSP hook_sp = target.GetHookAtIndex(i);
         if (hook_sp)
           hook_sp->GetDescription(result.GetOutputStream(),
                                   eDescriptionLevelFull);
@@ -5626,36 +5963,37 @@ class CommandObjectTargetModuleHookList : public CommandObjectParsed {
   }
 };
 
-#pragma mark CommandObjectMultiwordTargetModuleHooks
+#pragma mark CommandObjectMultiwordTargetHooks
 
-class CommandObjectMultiwordTargetModuleHooks : public CommandObjectMultiword {
+class CommandObjectMultiwordTargetHooks : public CommandObjectMultiword {
 public:
-  CommandObjectMultiwordTargetModuleHooks(CommandInterpreter &interpreter)
+  CommandObjectMultiwordTargetHooks(CommandInterpreter &interpreter)
       : CommandObjectMultiword(
-            interpreter, "target modulehook",
-            "Commands for operating on debugger target module-hooks.",
-            "target modulehook <subcommand> [<subcommand-options>]") {
-    LoadSubCommand("add", CommandObjectSP(new CommandObjectTargetModuleHookAdd(
-                              interpreter)));
-    LoadSubCommand(
-        "delete",
-        CommandObjectSP(new CommandObjectTargetModuleHookDelete(interpreter)));
+            interpreter, "target hook",
+            "Commands for operating on target hooks.",
+            "target hook <subcommand> [<subcommand-options>]") {
     LoadSubCommand(
-        "disable",
-        CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
-            interpreter, false, "target modulehook disable [<id>]",
-            "Disable a module-hook.", "target modulehook disable")));
+        "add", CommandObjectSP(new CommandObjectTargetHookAdd(interpreter)));
     LoadSubCommand(
-        "enable",
-        CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
-            interpreter, true, "target modulehook enable [<id>]",
-            "Enable a module-hook.", "target modulehook enable")));
+        "add-filter",
+        CommandObjectSP(new CommandObjectTargetHookAddFilter(interpreter)));
+    LoadSubCommand("delete", CommandObjectSP(new CommandObjectTargetHookDelete(
+                                 interpreter)));
+    LoadSubCommand("disable",
+                   CommandObjectSP(new CommandObjectTargetHookEnableDisable(
+                       interpreter, false, "target hook disable",
+                       "Disable a hook or a specific event on a hook.",
+                       "target hook disable [<event-type>] [<id> ...]")));
+    LoadSubCommand("enable",
+                   CommandObjectSP(new CommandObjectTargetHookEnableDisable(
+                       interpreter, true, "target hook enable",
+                       "Enable a hook or a specific event on a hook.",
+                       "target hook enable [<event-type>] [<id> ...]")));
     LoadSubCommand(
-        "list",
-        CommandObjectSP(new CommandObjectTargetModuleHookList(interpreter)));
+        "list", CommandObjectSP(new CommandObjectTargetHookList(interpreter)));
   }
 
-  ~CommandObjectMultiwordTargetModuleHooks() override = default;
+  ~CommandObjectMultiwordTargetHooks() override = default;
 };
 
 #pragma mark CommandObjectTargetDumpTypesystem
@@ -5950,9 +6288,8 @@ CommandObjectMultiwordTarget::CommandObjectMultiwordTarget(
   LoadSubCommand(
       "stop-hook",
       CommandObjectSP(new CommandObjectMultiwordTargetStopHooks(interpreter)));
-  LoadSubCommand("modulehook",
-                 CommandObjectSP(
-                     new CommandObjectMultiwordTargetModuleHooks(interpreter)));
+  LoadSubCommand("hook", CommandObjectSP(new CommandObjectMultiwordTargetHooks(
+                             interpreter)));
   LoadSubCommand("modules",
                  CommandObjectSP(new CommandObjectTargetModules(interpreter)));
   LoadSubCommand("symbols",
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index 50fec369330de..12469f392badf 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1901,15 +1901,82 @@ let Command = "target stop_hook list" in {
         Desc<"Show debugger ${i}nternal stop hooks.">;
 }
 
-let Command = "target modulehook add" in {
-  def target_modulehook_add_one_liner
+let Command = "target hook add" in {
+  def target_hook_add_one_liner
       : Option<"one-liner", "o">,
         Arg<"OneLiner">,
-        Desc<"Add a command for the module hook. Can be specified more than "
+        Desc<"Add a command for the hook. Can be specified more than "
              "once, and commands will be run in the order they appear.">;
-  def target_modulehook_add_on_unload
+  def target_hook_add_on_unload
       : Option<"on-unload", "u">,
         Desc<"Also fire the hook when modules are ${u}nloaded.">;
+  def target_hook_add_on_stop
+      : Option<"on-stop", "S">,
+        Desc<"Also fire the hook when the process ${S}tops.">;
+}
+
+let Command = "target hook add_filter" in {
+  def target_hook_add_filter_shlib
+      : Option<"shlib", "s">,
+        Arg<"ShlibName">,
+        Completion<"Module">,
+        Desc<"Set the module within which the stop handler is to be run.">;
+  def target_hook_add_filter_thread_index
+      : Option<"thread-index", "x">,
+        Arg<"ThreadIndex">,
+        Desc<"The stop handler is run only for the thread whose inde${x} "
+             "matches this argument.">;
+  def target_hook_add_filter_thread_id
+      : Option<"thread-id", "t">,
+        Arg<"ThreadID">,
+        Desc<"The stop handler is run only for the ${t}hread whose TID "
+             "matches this argument.">;
+  def target_hook_add_filter_thread_name
+      : Option<"thread-name", "T">,
+        Arg<"ThreadName">,
+        Desc<"The stop handler is run only for the ${T}hread whose thread "
+             "name matches this argument.">;
+  def target_hook_add_filter_queue_name
+      : Option<"queue-name", "q">,
+        Arg<"QueueName">,
+        Desc<"The stop handler is run only for threads in the ${q}ueue whose "
+             "name is given by this argument.">;
+  def target_hook_add_filter_file
+      : Option<"file", "f">,
+        Arg<"Filename">,
+        Desc<"Specify the source ${f}ile within which the stop handler is "
+             "to be run.">,
+        Completion<"SourceFile">;
+  def target_hook_add_filter_start_line
+      : Option<"start-line", "l">,
+        Arg<"LineNum">,
+        Desc<"Set the start of the ${l}ine range for which the stop handler "
+             "is to be run.">;
+  def target_hook_add_filter_end_line
+      : Option<"end-line", "e">,
+        Arg<"LineNum">,
+        Desc<"Set the ${e}nd of the line range for which the stop handler "
+             "is to be run.">;
+  def target_hook_add_filter_classname
+      : Option<"classname", "c">,
+        Arg<"ClassName">,
+        Desc<"Specify the ${c}lass within which the stop handler is to "
+             "be run.">;
+  def target_hook_add_filter_name
+      : Option<"name", "n">,
+        Arg<"FunctionName">,
+        Desc<"Set the function ${n}ame within which the stop handler will "
+             "be run.">,
+        Completion<"Symbol">;
+  def target_hook_add_filter_auto_continue
+      : Option<"auto-continue", "G">,
+        Arg<"Boolean">,
+        Desc<"The hook will auto-continue after running its stop handler.">;
+  def target_hook_add_filter_at_initial_stop
+      : Option<"at-initial-stop", "I">,
+        Arg<"Boolean">,
+        Desc<"Whether the stop handler will trigger when lldb ${I}nitially "
+             "gains control of the process. Defaults to true.">;
 }
 
 let Command = "thread backtrace" in {
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index 817520bc37271..69dd9e669ad9d 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -31,7 +31,7 @@ add_lldb_library(lldbPluginScriptInterpreterPython PLUGIN
   Interfaces/ScriptedProcessPythonInterface.cpp
   Interfaces/ScriptedPythonInterface.cpp
   Interfaces/ScriptedStopHookPythonInterface.cpp
-  Interfaces/ScriptedModuleHookPythonInterface.cpp
+  Interfaces/ScriptedHookPythonInterface.cpp
   Interfaces/ScriptedBreakpointPythonInterface.cpp
   Interfaces/ScriptedThreadPlanPythonInterface.cpp
   Interfaces/ScriptedThreadPythonInterface.cpp
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index d7368de2bda67..9e0ee5f23735f 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -24,7 +24,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedPlatformPythonInterface::Initialize();
   ScriptedProcessPythonInterface::Initialize();
   ScriptedStopHookPythonInterface::Initialize();
-  ScriptedModuleHookPythonInterface::Initialize();
+  ScriptedHookPythonInterface::Initialize();
   ScriptedBreakpointPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
   ScriptedFrameProviderPythonInterface::Initialize();
@@ -35,7 +35,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedPlatformPythonInterface::Terminate();
   ScriptedProcessPythonInterface::Terminate();
   ScriptedStopHookPythonInterface::Terminate();
-  ScriptedModuleHookPythonInterface::Terminate();
+  ScriptedHookPythonInterface::Terminate();
   ScriptedBreakpointPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
   ScriptedFrameProviderPythonInterface::Terminate();
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 6866aabb22666..032e62d7eab9e 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -16,7 +16,7 @@
 #include "ScriptedBreakpointPythonInterface.h"
 #include "ScriptedFrameProviderPythonInterface.h"
 #include "ScriptedFramePythonInterface.h"
-#include "ScriptedModuleHookPythonInterface.h"
+#include "ScriptedHookPythonInterface.h"
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
new file mode 100644
index 0000000000000..0695feacabb1f
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
@@ -0,0 +1,80 @@
+//===-- ScriptedHookPythonInterface.cpp
+//------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "../lldb-python.h"
+#include "ScriptedHookPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+
+ScriptedHookPythonInterface::ScriptedHookPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedHookInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedHookPythonInterface::CreatePluginObject(
+    llvm::StringRef class_name, lldb::TargetSP target_sp,
+    const StructuredDataImpl &args_sp) {
+  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
+                                                     target_sp, args_sp);
+}
+
+void ScriptedHookPythonInterface::HandleModuleLoaded(
+    lldb::StreamSP &output_sp) {
+  Status error;
+  Dispatch("handle_module_loaded", error, output_sp);
+}
+
+void ScriptedHookPythonInterface::HandleModuleUnloaded(
+    lldb::StreamSP &output_sp) {
+  Status error;
+  Dispatch("handle_module_unloaded", error, output_sp);
+}
+
+llvm::Expected<bool>
+ScriptedHookPythonInterface::HandleStop(ExecutionContext &exe_ctx,
+                                        lldb::StreamSP &output_sp) {
+  ExecutionContextRefSP exe_ctx_ref_sp =
+      std::make_shared<ExecutionContextRef>(exe_ctx);
+  Status error;
+  StructuredData::ObjectSP obj =
+      Dispatch("handle_stop", error, exe_ctx_ref_sp, output_sp);
+
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error)) {
+    if (!obj)
+      return true;
+    return error.ToError();
+  }
+
+  return obj->GetBooleanValue();
+}
+
+void ScriptedHookPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "target hook add -P <script-name> [-k key -v value ...]"};
+  const std::vector<llvm::StringRef> api_usages = {};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      llvm::StringRef("Perform actions on target lifecycle events (module "
+                      "load/unload, process stop)."),
+      CreateInstance, eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedHookPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
new file mode 100644
index 0000000000000..8b3f753c75038
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
@@ -0,0 +1,50 @@
+//===-- ScriptedHookPythonInterface.h ----------------------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDHOOKPYTHONINTERFACE_H
+#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDHOOKPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedHookInterface.h"
+
+#include "ScriptedPythonInterface.h"
+
+namespace lldb_private {
+class ScriptedHookPythonInterface : public ScriptedHookInterface,
+                                    public ScriptedPythonInterface,
+                                    public PluginInterface {
+public:
+  ScriptedHookPythonInterface(ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
+                     const StructuredDataImpl &args_sp) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>(
+        {{"handle_module_loaded", 1}});
+  }
+
+  void HandleModuleLoaded(lldb::StreamSP &output_sp) override;
+  void HandleModuleUnloaded(lldb::StreamSP &output_sp) override;
+  llvm::Expected<bool> HandleStop(ExecutionContext &exe_ctx,
+                                  lldb::StreamSP &output_sp) override;
+
+  static void Initialize();
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedHookPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDHOOKPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
index 5b2de9a4a27e2..d0af4f3918951 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
@@ -50,7 +50,7 @@ void ScriptedModuleHookPythonInterface::HandleModuleUnloaded(
 
 void ScriptedModuleHookPythonInterface::Initialize() {
   const std::vector<llvm::StringRef> ci_usages = {
-      "target module-hook add -P <script-name> [-k key -v value ...]"};
+      "target hook add -P <script-name> [-k key -v value ...]"};
   const std::vector<llvm::StringRef> api_usages = {};
   PluginManager::RegisterPlugin(
       GetPluginNameStatic(),
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 1c94c70fa4ccb..5221da03f3483 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1509,9 +1509,9 @@ ScriptInterpreterPythonImpl::CreateScriptedStopHookInterface() {
   return std::make_shared<ScriptedStopHookPythonInterface>(*this);
 }
 
-ScriptedModuleHookInterfaceSP
-ScriptInterpreterPythonImpl::CreateScriptedModuleHookInterface() {
-  return std::make_shared<ScriptedModuleHookPythonInterface>(*this);
+ScriptedHookInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedHookInterface() {
+  return std::make_shared<ScriptedHookPythonInterface>(*this);
 }
 
 ScriptedBreakpointInterfaceSP
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index 59c92b08e127e..863cf27785824 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -90,8 +90,7 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   lldb::ScriptedStopHookInterfaceSP CreateScriptedStopHookInterface() override;
 
-  lldb::ScriptedModuleHookInterfaceSP
-  CreateScriptedModuleHookInterface() override;
+  lldb::ScriptedHookInterfaceSP CreateScriptedHookInterface() override;
 
   lldb::ScriptedBreakpointInterfaceSP
   CreateScriptedBreakpointInterface() override;
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index c29538ca48cc7..ef342f6cfa859 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -38,7 +38,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h"
-#include "lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedHookInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h"
 #include "lldb/Interpreter/OptionGroupWatchpoint.h"
 #include "lldb/Interpreter/OptionValues.h"
@@ -3159,7 +3159,16 @@ bool Target::RunStopHooks(bool at_initial_stop) {
     if (is_active(hook))
       active_hooks.push_back(hook);
   }
-  if (active_hooks.empty())
+
+  // Also collect unified hooks that fire on process stop.
+  std::vector<HookSP> active_unified_hooks;
+  for (auto &[_, hook] : m_hooks) {
+    if (hook->IsActive() && hook->FiresOn(Hook::kProcessStop) &&
+        (!at_initial_stop || hook->GetRunAtInitialStop()))
+      active_unified_hooks.push_back(hook);
+  }
+
+  if (active_hooks.empty() && active_unified_hooks.empty())
     return false;
 
   // Make sure we check that we are not stopped because of us running a user
@@ -3210,6 +3219,8 @@ bool Target::RunStopHooks(bool at_initial_stop) {
 
   size_t num_hooks_with_output = llvm::count_if(
       active_hooks, [](auto h) { return !h->GetSuppressOutput(); });
+  num_hooks_with_output += llvm::count_if(
+      active_unified_hooks, [](auto h) { return !h->GetSuppressOutput(); });
   bool print_hook_header = (num_hooks_with_output > 1);
   bool print_thread_header = (num_exe_ctx > 1);
   bool should_stop = false;
@@ -3273,6 +3284,53 @@ bool Target::RunStopHooks(bool at_initial_stop) {
     }
   }
 
+  // Run unified hooks that fire on process stop.
+  for (auto cur_hook_sp : active_unified_hooks) {
+    bool any_thread_matched = false;
+    for (auto exc_ctx : exc_ctx_with_reasons) {
+      if (!cur_hook_sp->ExecutionContextPasses(exc_ctx))
+        continue;
+
+      bool suppress_output = cur_hook_sp->GetSuppressOutput();
+      if (print_hook_header && !any_thread_matched && !suppress_output) {
+        StreamString s;
+        cur_hook_sp->GetDescription(s, eDescriptionLevelBrief);
+        if (s.GetSize() != 0)
+          output_sp->Printf("\n- Hook %" PRIu64 " (%s)\n", cur_hook_sp->GetID(),
+                            s.GetData());
+        else
+          output_sp->Printf("\n- Hook %" PRIu64 "\n", cur_hook_sp->GetID());
+        any_thread_matched = true;
+      }
+
+      if (print_thread_header && !suppress_output)
+        output_sp->Printf("-- Thread %d\n",
+                          exc_ctx.GetThreadPtr()->GetIndexID());
+
+      auto result = cur_hook_sp->HandleStop(exc_ctx, output_sp);
+      switch (result) {
+      case StopHook::StopHookResult::KeepStopped:
+        if (cur_hook_sp->GetAutoContinue())
+          requested_continue = true;
+        else
+          should_stop = true;
+        break;
+      case StopHook::StopHookResult::RequestContinue:
+        requested_continue = true;
+        break;
+      case StopHook::StopHookResult::NoPreference:
+        break;
+      case StopHook::StopHookResult::AlreadyContinued:
+        output_sp->Printf("\nAborting stop hooks, hook %" PRIu64
+                          " set the program running.\n"
+                          "  Consider using '-G true' to make "
+                          "stop hooks auto-continue.\n",
+                          cur_hook_sp->GetID());
+        return true;
+      }
+    }
+  }
+
   // Resume iff at least one hook requested to continue and no hook asked to
   // stop.
   if (requested_continue && !should_stop) {
@@ -4233,45 +4291,111 @@ void Target::StopHookScripted::GetSubclassDescription(
   as_dict->ForEach(print_one_element);
 }
 
-// ModuleHook
+// Hook
 
-Target::ModuleHook::ModuleHook(lldb::TargetSP target_sp, lldb::user_id_t uid)
+Target::Hook::Hook(lldb::TargetSP target_sp, lldb::user_id_t uid)
     : UserID(uid), m_target_sp(std::move(target_sp)) {}
 
-Target::ModuleHook::ModuleHook(const ModuleHook &rhs)
+Target::Hook::Hook(const Hook &rhs)
     : UserID(rhs.GetID()), m_target_sp(rhs.m_target_sp), m_active(rhs.m_active),
-      m_fire_on_unload(rhs.m_fire_on_unload) {}
+      m_event_mask(rhs.m_event_mask), m_specifier_sp(rhs.m_specifier_sp),
+      m_auto_continue(rhs.m_auto_continue),
+      m_at_initial_stop(rhs.m_at_initial_stop),
+      m_suppress_output(rhs.m_suppress_output) {
+  if (rhs.m_thread_spec_up)
+    m_thread_spec_up = std::make_unique<ThreadSpec>(*rhs.m_thread_spec_up);
+}
+
+void Target::Hook::SetSpecifier(SymbolContextSpecifier *specifier) {
+  m_specifier_sp.reset(specifier);
+}
+
+void Target::Hook::SetThreadSpecifier(ThreadSpec *specifier) {
+  m_thread_spec_up.reset(specifier);
+}
+
+bool Target::Hook::ExecutionContextPasses(const ExecutionContext &exc_ctx) {
+  SymbolContextSpecifier *specifier = GetSpecifier();
+  if (!specifier)
+    return true;
+
+  bool will_run = true;
+  if (exc_ctx.GetFramePtr())
+    will_run = GetSpecifier()->SymbolContextMatches(
+        exc_ctx.GetFramePtr()->GetSymbolContext(eSymbolContextEverything));
+  if (will_run && GetThreadSpecifier() != nullptr)
+    will_run =
+        GetThreadSpecifier()->ThreadPassesBasicTests(exc_ctx.GetThreadRef());
 
-void Target::ModuleHook::GetDescription(Stream &s,
-                                        lldb::DescriptionLevel level) const {
+  return will_run;
+}
+
+void Target::Hook::GetDescription(Stream &s,
+                                  lldb::DescriptionLevel level) const {
   s.Printf("Hook: %" PRIu64 "\n", GetID());
   if (level == eDescriptionLevelBrief)
     return;
   s.IndentMore();
   s.Indent();
   s.Printf("State: %s\n", m_active ? "enabled" : "disabled");
-  if (m_fire_on_unload) {
+
+  // Show which events this hook fires on (only if not the default: load only)
+  if (m_event_mask != kModulesLoaded) {
+    std::string fires_on;
+    if (m_event_mask & kModulesLoaded)
+      fires_on += "load";
+    if (m_event_mask & kModulesUnloaded) {
+      if (!fires_on.empty())
+        fires_on += ", ";
+      fires_on += "unload";
+    }
+    if (m_event_mask & kProcessStop) {
+      if (!fires_on.empty())
+        fires_on += ", ";
+      fires_on += "stop";
+    }
     s.Indent();
-    s.PutCString("Fires on: load, unload\n");
+    s.Printf("Fires on: %s\n", fires_on.c_str());
+  }
+
+  if (m_auto_continue)
+    s.Indent("AutoContinue on\n");
+
+  if (m_specifier_sp) {
+    s.Indent();
+    s.PutCString("Specifier:\n");
+    s.IndentMore();
+    m_specifier_sp->GetDescription(&s, level);
+    s.IndentLess();
+  }
+
+  if (m_thread_spec_up) {
+    StreamString tmp;
+    s.Indent("Thread:\n");
+    m_thread_spec_up->GetDescription(&tmp, level);
+    s.IndentMore();
+    s.Indent(tmp.GetString());
+    s.PutCString("\n");
+    s.IndentLess();
   }
+
   GetSubclassDescription(s, level);
   s.IndentLess();
 }
 
-// ModuleHookCommandLine
+// HookCommandLine
 
-void Target::ModuleHookCommandLine::SetActionFromString(
-    const std::string &string) {
+void Target::HookCommandLine::SetActionFromString(const std::string &string) {
   GetCommands().SplitIntoLines(string);
 }
 
-void Target::ModuleHookCommandLine::SetActionFromStrings(
+void Target::HookCommandLine::SetActionFromStrings(
     const std::vector<std::string> &strings) {
   for (const auto &string : strings)
     GetCommands().AppendString(string.c_str());
 }
 
-void Target::ModuleHookCommandLine::GetSubclassDescription(
+void Target::HookCommandLine::GetSubclassDescription(
     Stream &s, lldb::DescriptionLevel level) const {
   if (level == eDescriptionLevelBrief) {
     if (m_commands.GetSize() == 1)
@@ -4290,7 +4414,7 @@ void Target::ModuleHookCommandLine::GetSubclassDescription(
   s.IndentLess();
 }
 
-void Target::ModuleHookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
+void Target::HookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
   if (!m_commands.GetSize())
     return;
 
@@ -4324,25 +4448,58 @@ void Target::ModuleHookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
   debugger.SetAsyncExecution(old_async);
 }
 
-void Target::ModuleHookCommandLine::HandleModuleUnloaded(StreamSP output_sp) {
+void Target::HookCommandLine::HandleModuleUnloaded(StreamSP output_sp) {
   // Command-based hooks run the same commands on unload as on load.
   HandleModuleLoaded(output_sp);
 }
 
-// ModuleHookScripted
+Target::StopHook::StopHookResult
+Target::HookCommandLine::HandleStop(ExecutionContext &exc_ctx,
+                                    StreamSP output_sp) {
+  assert(exc_ctx.GetTargetPtr() && "Can't call HandleStop on a context "
+                                   "with no target");
+
+  if (!m_commands.GetSize())
+    return StopHook::StopHookResult::KeepStopped;
+
+  CommandReturnObject result(false);
+  result.SetImmediateOutputStream(output_sp);
+  result.SetInteractive(false);
+  Debugger &debugger = exc_ctx.GetTargetPtr()->GetDebugger();
+  CommandInterpreterRunOptions options;
+  options.SetStopOnContinue(true);
+  options.SetStopOnError(true);
+  options.SetEchoCommands(false);
+  options.SetPrintResults(true);
+  options.SetPrintErrors(true);
+  options.SetAddToHistory(false);
+
+  bool old_async = debugger.GetAsyncExecution();
+  debugger.SetAsyncExecution(true);
+  debugger.GetCommandInterpreter().HandleCommands(GetCommands(), exc_ctx,
+                                                  options, result);
+  debugger.SetAsyncExecution(old_async);
+  lldb::ReturnStatus status = result.GetStatus();
+  if (status == eReturnStatusSuccessContinuingNoResult ||
+      status == eReturnStatusSuccessContinuingResult)
+    return StopHook::StopHookResult::AlreadyContinued;
+  return StopHook::StopHookResult::KeepStopped;
+}
+
+// HookScripted
 
-Status Target::ModuleHookScripted::SetScriptCallback(
+Status Target::HookScripted::SetScriptCallback(
     std::string class_name, StructuredData::ObjectSP extra_args_sp) {
   ScriptInterpreter *script_interp =
       GetTarget()->GetDebugger().GetScriptInterpreter();
   if (!script_interp)
     return Status::FromErrorString("No script interpreter installed.");
 
-  m_interface_sp = script_interp->CreateScriptedModuleHookInterface();
+  m_interface_sp = script_interp->CreateScriptedHookInterface();
   if (!m_interface_sp)
     return Status::FromErrorStringWithFormat(
-        "ScriptedModuleHook::%s () - ERROR: %s", __FUNCTION__,
-        "Script interpreter couldn't create Scripted Module Hook Interface");
+        "ScriptedHook::%s () - ERROR: %s", __FUNCTION__,
+        "Script interpreter couldn't create Scripted Hook Interface");
 
   m_class_name = std::move(class_name);
   m_extra_args.SetObjectSP(extra_args_sp);
@@ -4355,13 +4512,13 @@ Status Target::ModuleHookScripted::SetScriptCallback(
   StructuredData::ObjectSP object_sp = *obj_or_err;
   if (!object_sp || !object_sp->IsValid())
     return Status::FromErrorStringWithFormat(
-        "ScriptedModuleHook::%s () - ERROR: %s", __FUNCTION__,
+        "ScriptedHook::%s () - ERROR: %s", __FUNCTION__,
         "Failed to create valid script object");
 
   return {};
 }
 
-void Target::ModuleHookScripted::HandleModuleLoaded(StreamSP output_sp) {
+void Target::HookScripted::HandleModuleLoaded(StreamSP output_sp) {
   if (!m_interface_sp)
     return;
 
@@ -4371,7 +4528,7 @@ void Target::ModuleHookScripted::HandleModuleLoaded(StreamSP output_sp) {
       reinterpret_cast<StreamString *>(stream.get())->GetData());
 }
 
-void Target::ModuleHookScripted::HandleModuleUnloaded(StreamSP output_sp) {
+void Target::HookScripted::HandleModuleUnloaded(StreamSP output_sp) {
   if (!m_interface_sp)
     return;
 
@@ -4381,7 +4538,27 @@ void Target::ModuleHookScripted::HandleModuleUnloaded(StreamSP output_sp) {
       reinterpret_cast<StreamString *>(stream.get())->GetData());
 }
 
-void Target::ModuleHookScripted::GetSubclassDescription(
+Target::StopHook::StopHookResult
+Target::HookScripted::HandleStop(ExecutionContext &exc_ctx,
+                                 StreamSP output_sp) {
+  assert(exc_ctx.GetTargetPtr() && "Can't call HandleStop on a context "
+                                   "with no target");
+
+  if (!m_interface_sp)
+    return StopHook::StopHookResult::KeepStopped;
+
+  lldb::StreamSP stream = std::make_shared<lldb_private::StreamString>();
+  auto should_stop_or_err = m_interface_sp->HandleStop(exc_ctx, stream);
+  output_sp->PutCString(
+      reinterpret_cast<StreamString *>(stream.get())->GetData());
+  if (!should_stop_or_err)
+    return StopHook::StopHookResult::KeepStopped;
+
+  return *should_stop_or_err ? StopHook::StopHookResult::KeepStopped
+                             : StopHook::StopHookResult::RequestContinue;
+}
+
+void Target::HookScripted::GetSubclassDescription(
     Stream &s, lldb::DescriptionLevel level) const {
   if (level == eDescriptionLevelBrief) {
     s.PutCString(m_class_name);
@@ -4418,76 +4595,77 @@ void Target::ModuleHookScripted::GetSubclassDescription(
   s.IndentLess();
 }
 
-// Module Hook management methods
+// Hook management methods
 
-Target::ModuleHookSP Target::CreateModuleHook(ModuleHook::ModuleHookKind kind) {
-  lldb::user_id_t new_uid = ++m_module_hook_next_id;
-  ModuleHookSP hook_sp;
+Target::HookSP Target::CreateHook(Hook::HookKind kind) {
+  lldb::user_id_t new_uid = ++m_hook_next_id;
+  HookSP hook_sp;
   switch (kind) {
-  case ModuleHook::ModuleHookKind::CommandBased:
-    hook_sp.reset(new ModuleHookCommandLine(shared_from_this(), new_uid));
+  case Hook::HookKind::CommandBased:
+    hook_sp.reset(new HookCommandLine(shared_from_this(), new_uid));
     break;
-  case ModuleHook::ModuleHookKind::ScriptBased:
-    hook_sp.reset(new ModuleHookScripted(shared_from_this(), new_uid));
+  case Hook::HookKind::ScriptBased:
+    hook_sp.reset(new HookScripted(shared_from_this(), new_uid));
     break;
   }
-  m_module_hooks[new_uid] = hook_sp;
+  m_hooks[new_uid] = hook_sp;
   return hook_sp;
 }
 
-void Target::UndoCreateModuleHook(lldb::user_id_t uid) {
-  if (!RemoveModuleHookByID(uid))
+void Target::UndoCreateHook(lldb::user_id_t uid) {
+  if (!RemoveHookByID(uid))
     return;
   if (uid > 0)
-    --m_module_hook_next_id;
+    --m_hook_next_id;
 }
 
-bool Target::RemoveModuleHookByID(lldb::user_id_t uid) {
-  size_t num_removed = m_module_hooks.erase(uid);
+bool Target::RemoveHookByID(lldb::user_id_t uid) {
+  size_t num_removed = m_hooks.erase(uid);
   return (num_removed != 0);
 }
 
-void Target::RemoveAllModuleHooks() { m_module_hooks.clear(); }
+void Target::RemoveAllHooks() { m_hooks.clear(); }
 
-Target::ModuleHookSP Target::GetModuleHookByID(lldb::user_id_t uid) {
-  auto iter = m_module_hooks.find(uid);
-  if (iter == m_module_hooks.end())
+Target::HookSP Target::GetHookByID(lldb::user_id_t uid) {
+  auto iter = m_hooks.find(uid);
+  if (iter == m_hooks.end())
     return {};
   return iter->second;
 }
 
-Target::ModuleHookSP Target::GetModuleHookAtIndex(size_t index) {
-  if (index >= m_module_hooks.size())
+Target::HookSP Target::GetHookAtIndex(size_t index) {
+  if (index >= m_hooks.size())
     return {};
-  auto iter = m_module_hooks.begin();
+  auto iter = m_hooks.begin();
   std::advance(iter, index);
   return iter->second;
 }
 
-bool Target::SetModuleHookActiveStateByID(lldb::user_id_t uid,
-                                          bool active_state) {
-  auto iter = m_module_hooks.find(uid);
-  if (iter == m_module_hooks.end())
+bool Target::SetHookActiveStateByID(lldb::user_id_t uid, bool active_state) {
+  auto iter = m_hooks.find(uid);
+  if (iter == m_hooks.end())
     return false;
   iter->second->SetIsActive(active_state);
   return true;
 }
 
-void Target::SetAllModuleHooksActiveState(bool active_state) {
-  for (auto &[_, hook] : m_module_hooks)
+void Target::SetAllHooksActiveState(bool active_state) {
+  for (auto &[_, hook] : m_hooks)
     hook->SetIsActive(active_state);
 }
 
 void Target::RunModuleHooks(bool is_load) {
-  if (m_module_hooks.empty())
+  if (m_hooks.empty())
     return;
 
+  uint32_t event = is_load ? Hook::kModulesLoaded : Hook::kModulesUnloaded;
+
   StreamSP output_sp = m_debugger.GetAsyncOutputStream();
 
-  for (auto &[_, hook_sp] : m_module_hooks) {
+  for (auto &[_, hook_sp] : m_hooks) {
     if (!hook_sp->IsActive())
       continue;
-    if (!is_load && !hook_sp->GetFireOnUnload())
+    if (!hook_sp->FiresOn(event))
       continue;
     if (is_load)
       hook_sp->HandleModuleLoaded(output_sp);
diff --git a/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
index a071ecd73e883..28ac8293cca03 100644
--- a/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
+++ b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
@@ -8,12 +8,12 @@ class TestCase(TestBase):
     @no_debug_info_test
     def test_invalid_arg(self):
         self.expect(
-            "target modulehook delete -1",
+            "target hook delete -1",
             error=True,
-            startstr='error: invalid module hook id: "-1".',
+            startstr='error: invalid hook id: "-1".',
         )
         self.expect(
-            "target modulehook delete abcdfx",
+            "target hook delete abcdfx",
             error=True,
-            startstr='error: invalid module hook id: "abcdfx".',
+            startstr='error: invalid hook id: "abcdfx".',
         )
diff --git a/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
index 1541b0006b0c0..3a77a1da5f8e0 100644
--- a/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
+++ b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
@@ -8,12 +8,12 @@ class TestCase(TestBase):
     @no_debug_info_test
     def test_invalid_arg(self):
         self.expect(
-            "target modulehook disable -1",
+            "target hook disable -1",
             error=True,
-            startstr='error: invalid module hook id: "-1".',
+            startstr='error: invalid hook id: "-1".',
         )
         self.expect(
-            "target modulehook disable abcdfx",
+            "target hook disable abcdfx",
             error=True,
-            startstr='error: invalid module hook id: "abcdfx".',
+            startstr='error: invalid hook id: "abcdfx".',
         )
diff --git a/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
index dbb16cd10022b..8ab024b88c2d3 100644
--- a/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
+++ b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
@@ -8,12 +8,12 @@ class TestCase(TestBase):
     @no_debug_info_test
     def test_invalid_arg(self):
         self.expect(
-            "target modulehook enable -1",
+            "target hook enable -1",
             error=True,
-            startstr='error: invalid module hook id: "-1".',
+            startstr='error: invalid hook id: "-1".',
         )
         self.expect(
-            "target modulehook enable abcdfx",
+            "target hook enable abcdfx",
             error=True,
-            startstr='error: invalid module hook id: "abcdfx".',
+            startstr='error: invalid hook id: "abcdfx".',
         )
diff --git a/lldb/test/Shell/Commands/command-module-hook-fire.test b/lldb/test/Shell/Commands/command-module-hook-fire.test
index 5876ea05fb1fe..28ccd853bc803 100644
--- a/lldb/test/Shell/Commands/command-module-hook-fire.test
+++ b/lldb/test/Shell/Commands/command-module-hook-fire.test
@@ -2,9 +2,9 @@
 #
 # RUN: %clang_host -g %S/Inputs/main.c -o %t
 # RUN: %lldb -b -o 'file %t' \
-# RUN:   -o 'target modulehook add -o "script print(\"HOOK_FIRED\")"' \
+# RUN:   -o 'target hook add -o "script print(\"HOOK_FIRED\")"' \
 # RUN:   -o 'target modules add %t' \
 # RUN:   2>&1 | FileCheck %s
 
-# CHECK: Module hook #1 added.
+# CHECK: Hook #1 added.
 # CHECK: HOOK_FIRED
diff --git a/lldb/test/Shell/Commands/command-module-hook.test b/lldb/test/Shell/Commands/command-module-hook.test
index 4118a6f18501a..58676881d902a 100644
--- a/lldb/test/Shell/Commands/command-module-hook.test
+++ b/lldb/test/Shell/Commands/command-module-hook.test
@@ -1,18 +1,18 @@
-# Test module hook add/list/delete/enable/disable commands.
+# Test hook add/list/delete/enable/disable/add-filter commands.
 #
 # RUN: %lldb -b -s %s 2>&1 | FileCheck %s
 
 # Adding hooks assigns incrementing IDs.
-target modulehook add -o "script print('hook1')"
-# CHECK: Module hook #1 added.
-target modulehook add -o "script print('hook2')"
-# CHECK: Module hook #2 added.
-target modulehook add -o "script print('hook3')"
-# CHECK: Module hook #3 added.
+target hook add -o "script print('hook1')"
+# CHECK: Hook #1 added.
+target hook add -o "script print('hook2')"
+# CHECK: Hook #2 added.
+target hook add -o "script print('hook3')"
+# CHECK: Hook #3 added.
 
 # List all hooks.
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK: Hook: 1
 # CHECK:   State: enabled
 # CHECK:   Commands:
@@ -27,9 +27,9 @@ target modulehook list
 # CHECK:       script print('hook3')
 
 # Disable a hook.
-target modulehook disable 2
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook disable 2
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK: Hook: 1
 # CHECK:   State: enabled
 # CHECK: Hook: 2
@@ -38,9 +38,9 @@ target modulehook list
 # CHECK:   State: enabled
 
 # Re-enable the hook.
-target modulehook enable 2
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook enable 2
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK: Hook: 1
 # CHECK:   State: enabled
 # CHECK: Hook: 2
@@ -49,34 +49,34 @@ target modulehook list
 # CHECK:   State: enabled
 
 # Delete a hook in the middle.
-target modulehook delete 2
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook delete 2
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK:     Hook: 1
 # CHECK-NOT: Hook: 2
 # CHECK:     Hook: 3
 
 # New hook gets a new ID (not reused).
-target modulehook add -o "script print('hook4')"
-# CHECK: Module hook #4 added.
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook add -o "script print('hook4')"
+# CHECK: Hook #4 added.
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK:     Hook: 1
 # CHECK-NOT: Hook: 2
 # CHECK:     Hook: 3
 # CHECK:     Hook: 4
 
 # Delete all hooks.
-target modulehook delete
-target modulehook list
-# CHECK: (lldb) target modulehook list
-# CHECK: No module hooks.
+target hook delete
+target hook list
+# CHECK: (lldb) target hook list
+# CHECK: No hooks.
 
 # Add a hook with --on-unload and verify the description.
-target modulehook add -u -o "script print('load+unload')"
-# CHECK: Module hook #5 added.
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook add -u -o "script print('load+unload')"
+# CHECK: Hook #5 added.
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK: Hook: 5
 # CHECK:   State: enabled
 # CHECK:   Fires on: load, unload
@@ -84,9 +84,9 @@ target modulehook list
 # CHECK:       script print('load+unload')
 
 # Add a hook with multiple one-liner commands.
-target modulehook add -o "script print('first')" -o "script print('second')"
-# CHECK: Module hook #6 added.
-target modulehook list
+target hook add -o "script print('first')" -o "script print('second')"
+# CHECK: Hook #6 added.
+target hook list
 # CHECK: Hook: 6
 # CHECK:   State: enabled
 # CHECK-NOT: Fires on:
@@ -95,19 +95,67 @@ target modulehook list
 # CHECK:       script print('second')
 
 # Disable all hooks.
-target modulehook disable
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook disable
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK: Hook: 5
 # CHECK:   State: disabled
 # CHECK: Hook: 6
 # CHECK:   State: disabled
 
 # Enable all hooks.
-target modulehook enable
-target modulehook list
-# CHECK: (lldb) target modulehook list
+target hook enable
+target hook list
+# CHECK: (lldb) target hook list
 # CHECK: Hook: 5
 # CHECK:   State: enabled
 # CHECK: Hook: 6
 # CHECK:   State: enabled
+
+# Add a hook with --on-stop.
+target hook delete
+target hook add -S -o "bt"
+# CHECK: Hook #7 added.
+target hook list
+# CHECK: (lldb) target hook list
+# CHECK: Hook: 7
+# CHECK:   State: enabled
+# CHECK:   Fires on: load, stop
+# CHECK:   Commands:
+# CHECK:       bt
+
+# Add a hook with both --on-unload and --on-stop.
+target hook add -u -S -o "script print('all events')"
+# CHECK: Hook #8 added.
+target hook list
+# CHECK: Hook: 8
+# CHECK:   State: enabled
+# CHECK:   Fires on: load, unload, stop
+
+# Add a filter to hook #7 using add-filter.
+target hook add-filter -s mylib.so 7
+# CHECK: Filter added to hook #7.
+target hook list
+# CHECK: (lldb) target hook list
+# CHECK: Hook: 7
+# CHECK:   State: enabled
+# CHECK:   Fires on: load, stop
+# CHECK:   Specifier:
+# CHECK:     Module: mylib.so
+
+# Per-event disable: remove stop from hook #7.
+target hook disable stop-hook 7
+target hook list
+# CHECK: (lldb) target hook list
+# CHECK: Hook: 7
+# CHECK:   State: enabled
+# CHECK-NOT: Fires on: load, stop
+# CHECK:   Specifier:
+
+# Per-event enable: re-add stop to hook #7.
+target hook enable stop-hook 7
+target hook list
+# CHECK: (lldb) target hook list
+# CHECK: Hook: 7
+# CHECK:   State: enabled
+# CHECK:   Fires on: load, stop

>From 9d0181755560dfab1706d3235c5ae818a521c323 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Tue, 10 Mar 2026 10:44:52 -0700
Subject: [PATCH 5/7] fix format

---
 .../Python/Interfaces/ScriptedHookPythonInterface.cpp          | 3 +--
 .../Python/Interfaces/ScriptedHookPythonInterface.h            | 3 +--
 .../Python/Interfaces/ScriptedModuleHookPythonInterface.cpp    | 3 +--
 3 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
index 0695feacabb1f..db217ba0414da 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
@@ -1,5 +1,4 @@
-//===-- ScriptedHookPythonInterface.cpp
-//------------------------------------===//
+//===-- ScriptedHookPythonInterface.cpp ------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
index 8b3f753c75038..d0d00d8e9e803 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
@@ -1,5 +1,4 @@
-//===-- ScriptedHookPythonInterface.h ----------------------------*- C++
-//-*-===//
+//===-- ScriptedHookPythonInterface.h ----------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
index d0af4f3918951..f10e78b806c76 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
@@ -1,5 +1,4 @@
-//===-- ScriptedModuleHookPythonInterface.cpp
-//------------------------------===//
+//===-- ScriptedModuleHookPythonInterface.cpp ------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.

>From efd53140a91164b53db72a080eec2c9ca26d73f2 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Tue, 10 Mar 2026 11:05:38 -0700
Subject: [PATCH 6/7] Fixed format and small bug where we didn't copy m_hooks
 and m_hook_next_id

---
 .../include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h | 2 +-
 .../lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h   | 2 +-
 .../Python/Interfaces/ScriptedHookPythonInterface.cpp           | 2 +-
 .../Python/Interfaces/ScriptedHookPythonInterface.h             | 2 +-
 .../Python/Interfaces/ScriptedModuleHookPythonInterface.cpp     | 2 +-
 .../Python/Interfaces/ScriptedModuleHookPythonInterface.h       | 2 +-
 lldb/source/Target/Target.cpp                                   | 2 ++
 7 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
index 03053ed26879c..1ea76e2ab875f 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
@@ -26,7 +26,7 @@ class ScriptedHookInterface : public ScriptedInterface {
   /// Called when modules are unloaded from the target. Optional.
   virtual void HandleModuleUnloaded(lldb::StreamSP &output_sp) {}
 
-  /// Called when the process stops. Returns "should_stop" -- if false, the
+  /// Called when the process stops. Returns "should_stop" if false, the
   /// process will continue. Defaults to true (stop on unimplemented).
   virtual llvm::Expected<bool> HandleStop(ExecutionContext &exe_ctx,
                                           lldb::StreamSP &output_sp) {
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
index bbf9531045cde..e5a95384cac56 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
@@ -1,4 +1,4 @@
-//===-- ScriptedModuleHookInterface.h ----------------------------*- C++ -*-===//
+//===-- ScriptedModuleHookInterface.h ---------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
index db217ba0414da..78ca14c99bb46 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.cpp
@@ -1,4 +1,4 @@
-//===-- ScriptedHookPythonInterface.cpp ------------------------------------===//
+//===-- ScriptedHookPythonInterface.cpp -----------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
index d0d00d8e9e803..9e8b8f7605992 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedHookPythonInterface.h
@@ -1,4 +1,4 @@
-//===-- ScriptedHookPythonInterface.h ----------------------------*- C++ -*-===//
+//===-- ScriptedHookPythonInterface.h ---------------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
index f10e78b806c76..1147aaf6cbd5c 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
@@ -1,4 +1,4 @@
-//===-- ScriptedModuleHookPythonInterface.cpp ------------------------------===//
+//===-- ScriptedModuleHookPythonInterface.cpp -----------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
index c63db160e7dc8..3fdb392352b3b 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
@@ -1,4 +1,4 @@
-//===-- ScriptedModuleHookPythonInterface.h ----------------------*- C++ -*-===//
+//===-- ScriptedModuleHookPythonInterface.h ---------------------*- C++ -*-===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index ef342f6cfa859..61c05dcef4b2f 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -222,6 +222,8 @@ void Target::PrimeFromDummyTarget(Target &target) {
   m_stop_hooks = target.m_stop_hooks;
   m_stop_hook_next_id = target.m_stop_hook_next_id;
   m_internal_stop_hooks = target.m_internal_stop_hooks;
+  m_hooks = target.m_hooks;
+  m_hook_next_id = target.m_hook_next_id;
 
   for (const auto &breakpoint_sp : target.m_breakpoint_list.Breakpoints()) {
     if (breakpoint_sp->IsInternal())

>From ace96e909351420389a4a0ae082cbfb6fe75f51e Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Thu, 12 Mar 2026 10:37:51 -0700
Subject: [PATCH 7/7] Fixed recent comments

---
 lldb/include/lldb/Target/Target.h            | 35 ++++++----
 lldb/source/Commands/CommandObjectTarget.cpp |  2 +-
 lldb/source/Target/Target.cpp                | 68 ++++++++++++--------
 3 files changed, 63 insertions(+), 42 deletions(-)

diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 329d9edafebd4..889eb5360d029 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1655,6 +1655,9 @@ class Target : public std::enable_shared_from_this<Target>,
     enum class HookKind : uint32_t { CommandBased = 0, ScriptBased };
 
     /// Event mask bits controlling when this hook fires.
+    // FIXME: Add more event types as needed, e.g.:
+    //  kProcessExited (fires when the process exits).
+    //  kProcessDetached (fires when the debugger detaches).
     enum EventMask : uint32_t {
       kModulesLoaded = (1u << 0),
       kModulesUnloaded = (1u << 1),
@@ -1672,17 +1675,19 @@ class Target : public std::enable_shared_from_this<Target>,
     void RemoveEvent(uint32_t event) { m_event_mask &= ~event; }
     bool FiresOn(uint32_t event) const { return m_event_mask & event; }
 
-    // Stop-hook features (only relevant when kProcessStop is set)
+    // Filter fields
 
-    /// Set the specifier. The hook will own the specifier.
-    void SetSpecifier(SymbolContextSpecifier *specifier);
-    SymbolContextSpecifier *GetSpecifier() { return m_specifier_sp.get(); }
+    /// Set the symbol context specifier. The hook takes ownership.
+    void SetSCSpecifier(SymbolContextSpecifier *specifier);
+    SymbolContextSpecifier *GetSCSpecifier() {
+      return m_sc_specifier_sp.get();
+    }
 
     /// Check if the execution context passes the specifier and thread spec
     /// filters. Always returns true if no filters are set.
     bool ExecutionContextPasses(const ExecutionContext &exe_ctx);
 
-    /// Set the thread specifier. The hook will own the thread specifier.
+    /// Set the thread specifier. The hook takes ownership.
     void SetThreadSpecifier(ThreadSpec *specifier);
     ThreadSpec *GetThreadSpecifier() { return m_thread_spec_up.get(); }
 
@@ -1711,17 +1716,15 @@ class Target : public std::enable_shared_from_this<Target>,
     virtual StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
                                                 lldb::StreamSP output) = 0;
 
-    void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
-    virtual void GetSubclassDescription(Stream &s,
-                                        lldb::DescriptionLevel level) const = 0;
+    virtual void GetDescription(Stream &s,
+                                lldb::DescriptionLevel level) const;
 
   protected:
     lldb::TargetSP m_target_sp;
     bool m_active = true;
     uint32_t m_event_mask = kModulesLoaded; // Default: fire on load only
 
-    // Stop-hook filter fields (only used when kProcessStop is set)
-    lldb::SymbolContextSpecifierSP m_specifier_sp;
+    lldb::SymbolContextSpecifierSP m_sc_specifier_sp;
     std::unique_ptr<ThreadSpec> m_thread_spec_up;
     bool m_auto_continue = false;
     bool m_at_initial_stop = true;
@@ -1735,11 +1738,15 @@ class Target : public std::enable_shared_from_this<Target>,
     ~HookCommandLine() override = default;
 
     StringList &GetCommands() { return m_commands; }
+
+    /// Populate the command list by splitting a single string on newlines.
     void SetActionFromString(const std::string &string);
+
+    /// Populate the command list from a vector of individual command strings.
     void SetActionFromStrings(const std::vector<std::string> &strings);
 
-    void GetSubclassDescription(Stream &s,
-                                lldb::DescriptionLevel level) const override;
+    void GetDescription(Stream &s,
+                        lldb::DescriptionLevel level) const override;
     void HandleModuleLoaded(lldb::StreamSP output) override;
     void HandleModuleUnloaded(lldb::StreamSP output) override;
     StopHook::StopHookResult HandleStop(ExecutionContext &exe_ctx,
@@ -1757,8 +1764,8 @@ class Target : public std::enable_shared_from_this<Target>,
   public:
     ~HookScripted() override = default;
 
-    void GetSubclassDescription(Stream &s,
-                                lldb::DescriptionLevel level) const override;
+    void GetDescription(Stream &s,
+                        lldb::DescriptionLevel level) const override;
 
     void HandleModuleLoaded(lldb::StreamSP output) override;
     void HandleModuleUnloaded(lldb::StreamSP output) override;
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index f8fd4ecbc68f2..2adfc65f5a95e 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5768,7 +5768,7 @@ automatically added to its event mask.
             m_options.m_function_name.c_str(),
             SymbolContextSpecifier::eFunctionSpecified);
 
-      hook_sp->SetSpecifier(specifier_up.release());
+      hook_sp->SetSCSpecifier(specifier_up.release());
     }
 
     // Set up thread specifier.
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 61c05dcef4b2f..12a44ad51511d 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -4300,7 +4300,8 @@ Target::Hook::Hook(lldb::TargetSP target_sp, lldb::user_id_t uid)
 
 Target::Hook::Hook(const Hook &rhs)
     : UserID(rhs.GetID()), m_target_sp(rhs.m_target_sp), m_active(rhs.m_active),
-      m_event_mask(rhs.m_event_mask), m_specifier_sp(rhs.m_specifier_sp),
+      m_event_mask(rhs.m_event_mask),
+      m_sc_specifier_sp(rhs.m_sc_specifier_sp),
       m_auto_continue(rhs.m_auto_continue),
       m_at_initial_stop(rhs.m_at_initial_stop),
       m_suppress_output(rhs.m_suppress_output) {
@@ -4308,8 +4309,8 @@ Target::Hook::Hook(const Hook &rhs)
     m_thread_spec_up = std::make_unique<ThreadSpec>(*rhs.m_thread_spec_up);
 }
 
-void Target::Hook::SetSpecifier(SymbolContextSpecifier *specifier) {
-  m_specifier_sp.reset(specifier);
+void Target::Hook::SetSCSpecifier(SymbolContextSpecifier *specifier) {
+  m_sc_specifier_sp.reset(specifier);
 }
 
 void Target::Hook::SetThreadSpecifier(ThreadSpec *specifier) {
@@ -4317,13 +4318,13 @@ void Target::Hook::SetThreadSpecifier(ThreadSpec *specifier) {
 }
 
 bool Target::Hook::ExecutionContextPasses(const ExecutionContext &exc_ctx) {
-  SymbolContextSpecifier *specifier = GetSpecifier();
+  SymbolContextSpecifier *specifier = GetSCSpecifier();
   if (!specifier)
     return true;
 
   bool will_run = true;
   if (exc_ctx.GetFramePtr())
-    will_run = GetSpecifier()->SymbolContextMatches(
+    will_run = specifier->SymbolContextMatches(
         exc_ctx.GetFramePtr()->GetSymbolContext(eSymbolContextEverything));
   if (will_run && GetThreadSpecifier() != nullptr)
     will_run =
@@ -4341,7 +4342,6 @@ void Target::Hook::GetDescription(Stream &s,
   s.Indent();
   s.Printf("State: %s\n", m_active ? "enabled" : "disabled");
 
-  // Show which events this hook fires on (only if not the default: load only)
   if (m_event_mask != kModulesLoaded) {
     std::string fires_on;
     if (m_event_mask & kModulesLoaded)
@@ -4363,11 +4363,11 @@ void Target::Hook::GetDescription(Stream &s,
   if (m_auto_continue)
     s.Indent("AutoContinue on\n");
 
-  if (m_specifier_sp) {
+  if (m_sc_specifier_sp) {
     s.Indent();
     s.PutCString("Specifier:\n");
     s.IndentMore();
-    m_specifier_sp->GetDescription(&s, level);
+    m_sc_specifier_sp->GetDescription(&s, level);
     s.IndentLess();
   }
 
@@ -4381,24 +4381,12 @@ void Target::Hook::GetDescription(Stream &s,
     s.IndentLess();
   }
 
-  GetSubclassDescription(s, level);
   s.IndentLess();
 }
 
-// HookCommandLine
-
-void Target::HookCommandLine::SetActionFromString(const std::string &string) {
-  GetCommands().SplitIntoLines(string);
-}
-
-void Target::HookCommandLine::SetActionFromStrings(
-    const std::vector<std::string> &strings) {
-  for (const auto &string : strings)
-    GetCommands().AppendString(string.c_str());
-}
-
-void Target::HookCommandLine::GetSubclassDescription(
+void Target::HookCommandLine::GetDescription(
     Stream &s, lldb::DescriptionLevel level) const {
+  Hook::GetDescription(s, level);
   if (level == eDescriptionLevelBrief) {
     if (m_commands.GetSize() == 1)
       s.PutCString(m_commands.GetStringAtIndex(0));
@@ -4407,6 +4395,7 @@ void Target::HookCommandLine::GetSubclassDescription(
     return;
   }
 
+  s.IndentMore();
   s.Indent("Commands: \n");
   s.IndentMore();
   for (uint32_t i = 0; i < m_commands.GetSize(); i++) {
@@ -4414,6 +4403,19 @@ void Target::HookCommandLine::GetSubclassDescription(
     s.PutCString("\n");
   }
   s.IndentLess();
+  s.IndentLess();
+}
+
+// HookCommandLine
+
+void Target::HookCommandLine::SetActionFromString(const std::string &string) {
+  GetCommands().SplitIntoLines(string);
+}
+
+void Target::HookCommandLine::SetActionFromStrings(
+    const std::vector<std::string> &strings) {
+  for (const auto &string : strings)
+    GetCommands().AppendString(string.c_str());
 }
 
 void Target::HookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
@@ -4560,28 +4562,39 @@ Target::HookScripted::HandleStop(ExecutionContext &exc_ctx,
                              : StopHook::StopHookResult::RequestContinue;
 }
 
-void Target::HookScripted::GetSubclassDescription(
+void Target::HookScripted::GetDescription(
     Stream &s, lldb::DescriptionLevel level) const {
+  Hook::GetDescription(s, level);
   if (level == eDescriptionLevelBrief) {
     s.PutCString(m_class_name);
     return;
   }
+
+  s.IndentMore();
   s.Indent("Class:");
   s.Printf("%s\n", m_class_name.c_str());
 
-  if (!m_extra_args.IsValid())
+  if (!m_extra_args.IsValid()) {
+    s.IndentLess();
     return;
+  }
   StructuredData::ObjectSP object_sp = m_extra_args.GetObjectSP();
-  if (!object_sp || !object_sp->IsValid())
+  if (!object_sp || !object_sp->IsValid()) {
+    s.IndentLess();
     return;
+  }
 
   StructuredData::Dictionary *as_dict = object_sp->GetAsDictionary();
-  if (!as_dict || !as_dict->IsValid())
+  if (!as_dict || !as_dict->IsValid()) {
+    s.IndentLess();
     return;
+  }
 
   uint32_t num_keys = as_dict->GetSize();
-  if (num_keys == 0)
+  if (num_keys == 0) {
+    s.IndentLess();
     return;
+  }
 
   s.Indent("Args:\n");
   s.IndentMore();
@@ -4595,6 +4608,7 @@ void Target::HookScripted::GetSubclassDescription(
 
   as_dict->ForEach(print_one_element);
   s.IndentLess();
+  s.IndentLess();
 }
 
 // Hook management methods



More information about the lldb-commits mailing list