diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2c6eb74..6ec25b8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -98,6 +98,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added `occupied-scroll = true` option to bspwm module.
   Allows scrolling only through occupied desktops only.
   ([`#2427`](https://github.com/polybar/polybar/issues/2427))
+- `custom/ipc`: `send` action to send arbitrary strings to be displayed in the module.
+  ([`#2455`](https://github.com/polybar/polybar/issues/2455))
 
 ### Changed
 - Slight changes to the value ranges the different ramp levels are responsible
diff --git a/doc/user/actions.rst b/doc/user/actions.rst
index 95eb5e05..cc6145d9 100644
--- a/doc/user/actions.rst
+++ b/doc/user/actions.rst
@@ -258,6 +258,13 @@ custom/menu
            The data has the form ``N-M`` and the action will execute the command
            in ``menu-N-M-exec``.
 
+custom/ipc
+^^^^^^^^^^
+
+:``send``: *(Has Data)* Replace the contents of the module with the data passed in this action.
+
+  .. versionadded:: 3.6.0
+
 Deprecated Action Names
 -----------------------
 
diff --git a/include/modules/ipc.hpp b/include/modules/ipc.hpp
index d0097cc1..5a5a1100 100644
--- a/include/modules/ipc.hpp
+++ b/include/modules/ipc.hpp
@@ -34,6 +34,11 @@ namespace modules {
 
     static constexpr auto TYPE = "custom/ipc";
 
+    static constexpr auto EVENT_SEND = "send";
+
+   protected:
+    void action_send(const string& data);
+
    private:
     static constexpr const char* TAG_OUTPUT{"<output>"};
     vector<unique_ptr<hook>> m_hooks;
diff --git a/src/modules/ipc.cpp b/src/modules/ipc.cpp
index 2e6b9948..9aea8ca5 100644
--- a/src/modules/ipc.cpp
+++ b/src/modules/ipc.cpp
@@ -13,15 +13,15 @@ namespace modules {
    * create formatting tags
    */
   ipc_module::ipc_module(const bar_settings& bar, string name_) : static_module<ipc_module>(bar, move(name_)) {
+    m_router->register_action_with_data(EVENT_SEND, &ipc_module::action_send);
+
     size_t index = 0;
 
-    for (auto&& command : m_conf.get_list<string>(name(), "hook")) {
+    for (auto&& command : m_conf.get_list<string>(name(), "hook", {})) {
       m_hooks.emplace_back(std::make_unique<hook>(hook{name() + to_string(++index), command}));
     }
 
-    if (m_hooks.empty()) {
-      throw module_error("No hooks defined");
-    }
+    m_log.info("%s: Loaded %d hooks", name(), m_hooks.size());
 
     if ((m_initial = m_conf.get(name(), "initial", 0_z)) && m_initial > m_hooks.size()) {
       throw module_error("Initial hook out of bounds (defined: " + to_string(m_hooks.size()) + ")");
@@ -125,6 +125,11 @@ namespace modules {
       broadcast();
     }
   }
+
+  void ipc_module::action_send(const string& data) {
+    m_output = data;
+    broadcast();
+  }
 }  // namespace modules
 
 POLYBAR_NS_END