hermes - 💡(How to fix) Fix [Bug]: gateway command approval prompts route to wrong Discord thread — stale os.environ session key leaks across concurrent gateway sessions [1 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…

Root Cause

In gateway/run.py (line 14650 as of commit ff5e19333), every gateway message handler writes the current session key to the process-global os.environ:

os.environ["HERMES_SESSION_KEY"] = session_key or ""

And tools/approval.py get_current_session_key() function falls back to os.environ when contextvars are unavailable:

def get_current_session_key(default: str = "default") -> str:
    # 1. approval-specific contextvars (set by gateway before agent.run)
    # 2. session_context contextvars (set by _set_session_env)
    # 3. os.environ fallback (CLI, cron, tests)
    ...
    return get_session_env("HERMES_SESSION_KEY", default)

And gateway/session_context.py get_session_env() falls back to os.environ when the contextvar is _UNSET (never set in the current task):

def get_session_env(name: str, default: str = "") -> str:
    var = _VAR_MAP.get(name)
    if var is not None:
        value = var.get()
        if value is not _UNSET:
            return value
    return os.environ.get(name, default)

Race condition scenario:

  1. Thread A begins processing → _set_session_env() sets contextvars → os.environ["HERMES_SESSION_KEY"] = "session-A"
  2. Thread B begins processing → os.environ["HERMES_SESSION_KEY"] = "session-B"overwrites Thread A's value
  3. Thread A's tool thread (from run_in_executor / ThreadPoolExecutor) encounters a dangerous command
  4. get_current_session_key() → contextvar is _UNSET in the tool thread → falls back to os.environ → gets "session-B" instead of "session-A"
  5. Approval prompt sent to Thread B's notification callback → embed lands in Thread B

Fix Action

Fixed

Code Example

os.environ["HERMES_SESSION_KEY"] = session_key or ""

---

def get_current_session_key(default: str = "default") -> str:
    # 1. approval-specific contextvars (set by gateway before agent.run)
    # 2. session_context contextvars (set by _set_session_env)
    # 3. os.environ fallback (CLI, cron, tests)
    ...
    return get_session_env("HERMES_SESSION_KEY", default)

---

def get_session_env(name: str, default: str = "") -> str:
    var = _VAR_MAP.get(name)
    if var is not None:
        value = var.get()
        if value is not _UNSET:
            return value
    return os.environ.get(name, default)
RAW_BUFFERClick to expand / collapse

Bug Description

When running Hermes via the Discord gateway, "Command Approval Required" prompts for terminal() commands matching DANGEROUS_PATTERNS are routed to the wrong Discord thread. The user may be working in Thread A but sees approval prompts meant for Thread B (or vice versa).

This has been observed to happen repeatedly in multi-thread usage.

Root Cause

In gateway/run.py (line 14650 as of commit ff5e19333), every gateway message handler writes the current session key to the process-global os.environ:

os.environ["HERMES_SESSION_KEY"] = session_key or ""

And tools/approval.py get_current_session_key() function falls back to os.environ when contextvars are unavailable:

def get_current_session_key(default: str = "default") -> str:
    # 1. approval-specific contextvars (set by gateway before agent.run)
    # 2. session_context contextvars (set by _set_session_env)
    # 3. os.environ fallback (CLI, cron, tests)
    ...
    return get_session_env("HERMES_SESSION_KEY", default)

And gateway/session_context.py get_session_env() falls back to os.environ when the contextvar is _UNSET (never set in the current task):

def get_session_env(name: str, default: str = "") -> str:
    var = _VAR_MAP.get(name)
    if var is not None:
        value = var.get()
        if value is not _UNSET:
            return value
    return os.environ.get(name, default)

Race condition scenario:

  1. Thread A begins processing → _set_session_env() sets contextvars → os.environ["HERMES_SESSION_KEY"] = "session-A"
  2. Thread B begins processing → os.environ["HERMES_SESSION_KEY"] = "session-B"overwrites Thread A's value
  3. Thread A's tool thread (from run_in_executor / ThreadPoolExecutor) encounters a dangerous command
  4. get_current_session_key() → contextvar is _UNSET in the tool thread → falls back to os.environ → gets "session-B" instead of "session-A"
  5. Approval prompt sent to Thread B's notification callback → embed lands in Thread B

Expected Behavior

Approval prompts should always be delivered to the originating thread, regardless of concurrent gateway sessions.

Proposed Fix

The os.environ["HERMES_SESSION_KEY"] = session_key or "" write at gateway/run.py:14650 should be removed. The contextvar system (gateway/session_context.py) already provides concurrency-safe session state isolation. The comment on that line says "Keep os.environ as fallback for CLI/cron" but this code path only runs inside GatewayRunner._run_agent(), which is exclusively used by the gateway — CLI and cron use separate code paths.

The approval contextvar _approval_session_key is set at gateway/run.py:15169-15170 via set_current_session_key(), and the notification callback _approval_notify_sync captures the correct chat_id + thread_id in its closure — so removing the os.environ write preserves correct routing.

Environment

  • Hermes Agent @ ff5e19333
  • Discord gateway
  • Multi-thread usage

Related

  • Issue #10304 (partial: session context cleanup leaking stale env values, fixed in b3b88a279)

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