NousResearch/hermes-agent · error · RuntimeError

Refusing to write iron-proxy log {log_path}: {exc}. Remove

Error message

Refusing to write iron-proxy log {log_path}: {exc}.  Remove that path manually and retry.

What it means

start_proxy() opens the daemon log with O_WRONLY|O_CREAT|O_APPEND|O_NOFOLLOW and 0600 — the O_NOFOLLOW specifically blocks a same-uid attacker from planting iron-proxy.log as a symlink to a sensitive file (the comment gives ~/.ssh/authorized_keys as the example). Any OSError on that open (ELOOP from a symlink, EACCES on a foreign-owned or wrong-perm file) makes startup refuse rather than write through the attack path.

Source

Thrown at agent/proxy_sources/iron_proxy.py:1843

    log_path = _proxy_state_dir() / "iron-proxy.log"
    # Keep ownership of the fd tight: open with explicit 0o600 so the
    # log doesn't get world-readable under a slack umask, then close it
    # immediately after Popen (the child has its own dup).  Without the
    # close-on-success path, every restart leaked one fd in the Hermes
    # process.
    #
    # O_NOFOLLOW (defence-in-depth, same threat model as the pidfile
    # path): a same-uid attacker who plants ``iron-proxy.log`` as a
    # symlink to e.g. ``~/.ssh/authorized_keys`` would otherwise cause
    # every restart to append daemon diagnostics to that file.
    log_open_flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
    if hasattr(os, "O_NOFOLLOW"):
        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

View on GitHub (pinned to c896c09c42)

Solutions

  1. Inspect `ls -la <log_path>`; if it is a symlink or foreign-owned, remove it (`rm <log_path>`) and retry — the daemon recreates it with safe permissions.
  2. If you want logs elsewhere, configure the daemon's logging target properly instead of symlinking the file.
  3. Keep one owning identity for all `hermes egress` invocations on a machine.
Defensive patterns

Strategy: validation

Validate before calling

import os
from pathlib import Path

def log_path_safe(path: Path) -> bool:
    if path.is_symlink():
        return False
    if path.exists():
        return path.stat().st_uid == os.geteuid()
    return os.access(path.parent, os.W_OK)

Try / catch

try:
    start_proxy()
except RuntimeError as e:
    if "Refusing to write iron-proxy log" in str(e):
        # symlink or foreign-owned log — remove the named path and retry
        raise

Prevention

When it happens

Trigger: start_proxy() / `hermes egress start` when iron-proxy.log in the proxy state dir is a symlink (planted or accidental), owned by another user, or has permissions the current user can't open for append; also when the state dir itself is unwritable.

Common situations: Mixed sudo/non-sudo runs leaving a root-owned log; a symlinked log from a naive attempt to put logs elsewhere; security tooling flagging and locking the file; genuinely hostile same-uid environments (the threat this guards).

Related errors


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