Skip to content

[Bug]: Dashboard auto-restart silently fails — _spawn_hermes_action inherits _HERMES_GATEWAY=1 from dashboard process #52470

Description

@nightwlkr

Bug Report — Dashboard auto-restart silently fails because subprocess inherits _HERMES_GATEWAY=1

Summary

The web dashboard's two auto-restart paths (POST /api/webhooks/enable and the Telegram QR onboarding apply endpoint) silently fail because the subprocess they spawn inherits the _HERMES_GATEWAY=1 env var from the dashboard process. The CLI's self-restart loop guard at hermes_cli/gateway.py:6347 then refuses to run the restart and the subprocess exits with code 1 — without the dashboard noticing.

This is the same loop-guard logic that gateway/run.py (lines 5159 and 5189) already handles correctly when spawning its proper restart watcher. The dashboard's _spawn_hermes_action just never got the equivalent fix.

Affected versions

Hermes Agent v0.17.0 (2026.6.19), upstream commit d6269da. Likely affects every version since the dashboard auto-restart paths were added.

Reproduction

  1. Install Hermes with the LaunchAgent gateway on macOS (or any host where the dashboard runs inside the gateway process).
  2. From the web dashboard, click "Enable webhooks" (Settings → Webhooks), OR complete Telegram QR onboarding and click Apply.
  3. Watch ~/.hermes/logs/gateway-restart.log.

Expected: The log shows === gateway-restart started ... === followed by → Stopping gateway (PID N) — draining in-flight runs ... and ✓ Service restarted. The gateway restarts.

Actual: The log shows === gateway-restart started ... === followed by:

✗ Refusing to restart the gateway from inside the gateway process.
This command was blocked to prevent restart loops.
Use `hermes gateway restart` from a shell outside the running gateway.

…and the subprocess exits with code 1. The gateway does NOT restart, but the dashboard returns {"ok": true, "restart_started": true, ...} because _spawn_gateway_restart only checks that the spawn succeeded, not that the restart actually completed.

Root cause

_spawn_hermes_action in hermes_cli/web_server.py (around line 2361) builds the subprocess env from os.environ:

"env": {**os.environ, "HERMES_NONINTERACTIVE": "1"},

The dashboard process is itself a child of the gateway (started as dashboard --no-open --host 127.0.0.1 --port 0 from gateway.run), so os.environ already contains _HERMES_GATEWAY=1 (set at gateway/run.py:1278). The spread copies it through, and the spawned hermes gateway restart subprocess then trips the guard at hermes_cli/gateway.py:6347:

if os.getenv("_HERMES_GATEWAY") == "1":
    print_error(
        "Refusing to restart the gateway from inside the gateway process. ..."
    )
    sys.exit(1)

The proper restart watcher in gateway/run.py already strips the marker before spawning (watcher_env.pop("_HERMES_GATEWAY", None) at lines 5159 and 5189), with a comment explaining exactly this gotcha: "If it inherits the gateway marker, hermes gateway restart refuses to run as a self-restart loop guard and the gateway stays stopped."

_spawn_hermes_action is missing that strip, so all dashboard-triggered lifecycle actions (gateway start/stop/restart, hermes update, mcp install, etc.) silently fail when launched from inside the gateway.

Proposed fix

In hermes_cli/web_server.py, replace the env construction in _spawn_hermes_action:

# Before
"env": {**os.environ, "HERMES_NONINTERACTIVE": "1"},

with:

# Strip the gateway marker before spawning any detached action. The
# dashboard runs inside the gateway process, so os.environ contains
# _HERMES_GATEWAY=1 — without stripping it, lifecycle subcommands
# (gateway stop/restart, hermes update) trip the self-restart loop
# guard in hermes_cli/gateway.py and silently exit with code 1.
# Compare gateway/run.py:5159 and gateway/run.py:5189, which already
# strip the marker when spawning the proper restart watcher.
child_env = {k: v for k, v in os.environ.items() if k != "_HERMES_GATEWAY"}
child_env["HERMES_NONINTERACTIVE"] = "1"

# ...

"env": child_env,

Additionally, _spawn_gateway_restart should consider polling the spawned subprocess's exit code (or capturing stderr) so the dashboard can surface a real failure to the user instead of returning {"restart_started": true} when the restart silently bailed. That's a UX nit, the env-strip is the actual fix.

Workaround (until fixed)

Run lifecycle commands from a shell outside the dashboard / running gateway:

hermes gateway restart
hermes update

Those work fine because they don't inherit _HERMES_GATEWAY=1.

Related

  • The same pattern almost certainly affects hermes update when invoked from the dashboard's "Update now" button (POST /api/hermes/update_spawn_hermes_action(["update"], "hermes-update")). I haven't verified end-to-end, but the fix above covers it.
  • The agent-side analog (an agent calling hermes gateway restart from inside a tool call) is already guarded by tools/terminal_tool.py:2116 and the cron-create guard in hermes_cli/cron.py:259.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/cliCLI entry point, hermes_cli/, setup wizardcomp/dashboardWeb dashboard / control panel UI (dashboard/, landing)type/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions