NousResearch/hermes-agent · error · RuntimeError

could not reach the management API at {host}:{port} ({exc}).

Error message

could not reach the management API at {host}:{port} ({exc}).  If the daemon was started before reload support, run `hermes egress restart` once.

What it means

POST to http://<host>:<port>/v1/reload failed at the transport layer (URLError/OSError): connection refused, timeout, or reset. The comment in the code names the classic cause — a daemon started from a pre-management config is alive (pid checks pass) but has no listener on the management port. The message tells the user to do the one-time restart.

Source

Thrown at agent/proxy_sources/iron_proxy.py:988

            pass
        if exc.code == 422:
            raise RuntimeError(
                f"iron-proxy rejected the new config (validation failed; "
                f"the running ruleset is unchanged): {body}"
            ) from exc
        if exc.code == 401:
            raise RuntimeError(
                "management API rejected our key (401).  The running "
                "daemon was started with a different management.token — "
                "run `hermes egress restart`."
            ) from exc
        raise RuntimeError(
            f"management reload failed (HTTP {exc.code}): {body}"
        ) from exc
    except (urllib.error.URLError, OSError) as exc:
        # A daemon started from a pre-management config is alive but has
        # no listener on the management port.
        raise RuntimeError(
            f"could not reach the management API at {host}:{port} ({exc}).  "
            "If the daemon was started before reload support, run "
            "`hermes egress restart` once."
        ) from exc


def _default_http_listen(tunnel_port: int) -> List[str]:
    """Build the single host:port bind the proxy should listen on.

    iron-proxy v0.39 supports exactly ONE ``proxy.http_listen`` bind per
    daemon process, so this returns a one-element list and the choice of
    host matters:

    * **Linux:** bind the docker bridge gateway (``172.17.0.1`` by
      default).  Sandboxes reach the proxy via
      ``host.docker.internal:host-gateway``, which Docker resolves to
      exactly this bridge gateway IP on Linux — a loopback-only bind is
      unreachable from inside containers there.  The bridge IP is still

View on GitHub (pinned to c896c09c42)

Solutions

  1. Run `hermes egress restart` once — this both upgrades the daemon to a management-capable config and re-binds the listener.
  2. If it persists, check the pid actually belongs to iron-proxy (ps -p <pid>) and that the management host:port in proxy.yaml is loopback-reachable (ss -ltnp).
  3. Verify daemon health from iron-proxy.log for hangs.
Defensive patterns

Strategy: try-catch

Validate before calling

import socket

def mgmt_listener_reachable(host: str, port: int, timeout: float = 2.0) -> bool:
    try:
        with socket.create_connection((host, port), timeout=timeout):
            return True
    except OSError:
        return False

Try / catch

try:
    reload_proxy()
except RuntimeError as e:
    if "could not reach the management API" in str(e):
        # pre-management daemon or dead listener — one restart fixes both
        raise

Prevention

When it happens

Trigger: reload_proxy() when nothing accepts the connection on the management host:port — daemon predates management support, daemon hung/dead but pid reused by another process (so _pid_alive passes), or the management listener binds a different address than the config says.

Common situations: Post-upgrade first reload on a long-running daemon; host reboot where the pid was recycled; firewall/nftables rules on loopback exotic setups blocking the port.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/a4f6c4b971dff275. Report an issue: GitHub.