hermes - 💡(How to fix) Fix concurrency: TOCTOU race in get_plugin_manager() can orphan discovered plugins [3 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

hermes_cli/plugins.py:get_plugin_manager() has a time-of-check/time-of-use (TOCTOU) race that can leave _plugin_manager pointing to an undiscovered (empty) PluginManager instance.

Root Cause

_plugin_manager: Optional[PluginManager] = None

def get_plugin_manager() -> PluginManager:
    global _plugin_manager
    if _plugin_manager is None:        # ← both threads pass this check
        _plugin_manager = PluginManager()  # ← both create a new instance
    return _plugin_manager

Two threads calling get_plugin_manager() concurrently can each see _plugin_manager is None, both create a fresh PluginManager(), and race to write the global. The second write wins; the first manager — including any hooks or plugins discovered on it — is orphaned.

Fix Action

Fixed

Code Example

_plugin_manager: Optional[PluginManager] = None

def get_plugin_manager() -> PluginManager:
    global _plugin_manager
    if _plugin_manager is None:        # ← both threads pass this check
        _plugin_manager = PluginManager()  # ← both create a new instance
    return _plugin_manager

---

_plugin_manager: Optional[PluginManager] = None
_plugin_manager_lock = threading.Lock()

def get_plugin_manager() -> PluginManager:
    global _plugin_manager
    if _plugin_manager is None:
        with _plugin_manager_lock:
            if _plugin_manager is None:
                _plugin_manager = PluginManager()
    return _plugin_manager
RAW_BUFFERClick to expand / collapse

Summary

hermes_cli/plugins.py:get_plugin_manager() has a time-of-check/time-of-use (TOCTOU) race that can leave _plugin_manager pointing to an undiscovered (empty) PluginManager instance.

Root Cause

_plugin_manager: Optional[PluginManager] = None

def get_plugin_manager() -> PluginManager:
    global _plugin_manager
    if _plugin_manager is None:        # ← both threads pass this check
        _plugin_manager = PluginManager()  # ← both create a new instance
    return _plugin_manager

Two threads calling get_plugin_manager() concurrently can each see _plugin_manager is None, both create a fresh PluginManager(), and race to write the global. The second write wins; the first manager — including any hooks or plugins discovered on it — is orphaned.

Impact Scenario

  1. Thread A and Thread B both call get_plugin_manager() → both see None
  2. Thread A creates Manager A and writes _plugin_manager = A
  3. Thread B creates Manager B and overwrites _plugin_manager = B
  4. Thread A calls discover_and_load() on Manager A → registers hooks/tools in Manager A
  5. _plugin_manager now points to Manager B (no discovery done yet)
  6. Thread C calls get_plugin_manager() → gets Manager B (empty)
  7. invoke_hook() on Manager B fires no hooks — platform plugins, memory plugins, etc. are silently missing

This is reachable in any gateway deployment where multiple agent threads start up concurrently and each calls discover_plugins() / get_plugin_manager().

Files

  • hermes_cli/plugins.pyget_plugin_manager() (line ~1288)

Expected Fix

Double-checked locking with a module-level threading.Lock():

_plugin_manager: Optional[PluginManager] = None
_plugin_manager_lock = threading.Lock()

def get_plugin_manager() -> PluginManager:
    global _plugin_manager
    if _plugin_manager is None:
        with _plugin_manager_lock:
            if _plugin_manager is None:
                _plugin_manager = PluginManager()
    return _plugin_manager

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING