NousResearch/hermes-agent · error · RuntimeError

iron-proxy log {log_path} has unexpected owner uid={st.st_ui

Error message

iron-proxy log {log_path} has unexpected owner uid={st.st_uid}; refusing to write.

What it means

After opening the iron-proxy log file (and fchmod'ing it to 0600), start_proxy fstat()s the fd and verifies st_uid matches the current effective uid, mirroring the pidfile ownership check. If the file on disk is owned by another uid (e.g. a pre-existing file created by root or another user, or a planted file at the log path), it raises rather than letting the proxy write into a file the current user does not own. This is a defensive check against symlink/file-planting attacks on HERMES_HOME paths.

Source

Thrown at agent/proxy_sources/iron_proxy.py:1856

        log_open_flags |= os.O_NOFOLLOW
    try:
        log_fd = os.open(str(log_path), log_open_flags, 0o600)
    except OSError as exc:
        # ELOOP from a planted symlink — refuse with a clear error.
        raise RuntimeError(
            f"Refusing to write iron-proxy log {log_path}: {exc}.  "
            "Remove that path manually and retry."
        ) from exc
    try:
        os.fchmod(log_fd, 0o600)  # tighten if file pre-existed
    except OSError:
        pass
    # Verify ownership — same st_uid check the pidfile uses.
    try:
        st = os.fstat(log_fd)
        if hasattr(os, "getuid") and st.st_uid != os.getuid():
            os.close(log_fd)
            raise RuntimeError(
                f"iron-proxy log {log_path} has unexpected owner "
                f"uid={st.st_uid}; refusing to write."
            )
    except AttributeError:
        pass  # Windows

    try:
        # Use the fd directly via the dup mechanism; Popen will dup() it
        # into the child so we can close ours unconditionally below.
        # NOTE: on Windows ``start_new_session`` is invalid; we don't
        # support Windows for the proxy (the binary itself doesn't ship)
        # but the kwarg is POSIX-only and silently ignored on Win.
        popen_kwargs: Dict = dict(
            env=env,
            stdin=subprocess.DEVNULL,
            stdout=log_fd,
            stderr=subprocess.STDOUT,
        )

View on GitHub (pinned to c896c09c42)

Solutions

  1. Remove or reown the offending log file: find the path from the error message and `rm <log_path>` (or `chown $(id -u) <log_path>`), then retry `hermes egress start`
  2. If the whole state dir is mis-owned, fix it once: `chown -R $(id -u):$(id -g) <proxy-state-dir>`
  3. Never run the egress proxy under sudo/root; if you did, clean up all files it created in the state dir
  4. Verify no symlink exists at the log path (`ls -l <log_path>`) — a planted symlink is treated the same way

Example fix

# before: log file owned by root after a sudo run
# RuntimeError: iron-proxy log ... unexpected owner uid=0
sudo rm /home/user/.hermes/iron-proxy/proxy.log
hermes egress start

# after: start succeeds, log recreated with current uid
Defensive patterns

Strategy: validation

Validate before calling

import os, stat
from pathlib import Path

def log_path_safe(p: Path) -> bool:
    if p.is_symlink():
        return False
    try:
        st = p.stat()
    except FileNotFoundError:
        return os.stat(p.parent).st_uid == os.getuid()  # parent owned by us
    return st.st_uid == os.getuid() and not stat.S_ISLNK(st.st_mode)

# before start_proxy(): assert log_path_safe(log_path) else remove/fix it

Type guard

def is_owned_log(p: Path) -> bool:
    try:
        st = p.lstat()
    except FileNotFoundError:
        return True
    return st.st_uid == os.getuid() and not (st.st_mode & 0o170000) == 0o120000

Try / catch

try:
    start_proxy(...)
except RuntimeError as e:
    if "unexpected owner" in str(e):
        log_path = Path(str(e).split()[3])  # remove/reown, then retry once
        os.remove(log_path)
        start_proxy(...)

Prevention

When it happens

Trigger: Calling start_proxy() when the log path (under the iron-proxy state dir) already exists and is owned by a different uid — e.g. a previous root-run of `hermes egress start`, restoring a state dir with sudo/cp -a, or a hostile pre-created file. Only fires on POSIX (guarded by hasattr(os, 'getuid')).

Common situations: Running `sudo hermes egress start` once and then running as the normal user; copying ~/.hermes between users with ownership preserved; shared machines where another account wrote to the same HERMES_HOME; CI containers that pre-create dirs as a different uid.

Related errors


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