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
- Install Hermes with the LaunchAgent gateway on macOS (or any host where the dashboard runs inside the gateway process).
- From the web dashboard, click "Enable webhooks" (Settings → Webhooks), OR complete Telegram QR onboarding and click Apply.
- 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.
Bug Report — Dashboard auto-restart silently fails because subprocess inherits
_HERMES_GATEWAY=1Summary
The web dashboard's two auto-restart paths (
POST /api/webhooks/enableand the Telegram QR onboarding apply endpoint) silently fail because the subprocess they spawn inherits the_HERMES_GATEWAY=1env var from the dashboard process. The CLI's self-restart loop guard athermes_cli/gateway.py:6347then 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_actionjust 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
~/.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:…and the subprocess exits with code 1. The gateway does NOT restart, but the dashboard returns
{"ok": true, "restart_started": true, ...}because_spawn_gateway_restartonly checks that the spawn succeeded, not that the restart actually completed.Root cause
_spawn_hermes_actioninhermes_cli/web_server.py(around line 2361) builds the subprocess env fromos.environ:The dashboard process is itself a child of the gateway (started as
dashboard --no-open --host 127.0.0.1 --port 0fromgateway.run), soos.environalready contains_HERMES_GATEWAY=1(set atgateway/run.py:1278). The spread copies it through, and the spawnedhermes gateway restartsubprocess then trips the guard athermes_cli/gateway.py:6347:The proper restart watcher in
gateway/run.pyalready 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 restartrefuses to run as a self-restart loop guard and the gateway stays stopped."_spawn_hermes_actionis 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:with:
Additionally,
_spawn_gateway_restartshould 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:
Those work fine because they don't inherit
_HERMES_GATEWAY=1.Related
hermes updatewhen 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.hermes gateway restartfrom inside a tool call) is already guarded bytools/terminal_tool.py:2116and the cron-create guard inhermes_cli/cron.py:259.