NousResearch/hermes-agent · error · RuntimeError

Refusing to write pidfile {pidfile}: {exc}. Remove that pat

Error message

Refusing to write pidfile {pidfile}: {exc}.  Remove that path manually and retry.

What it means

os.open of the pidfile path failed with an OSError other than FileExistsError — most notably ELOOP, which is what the kernel returns when O_NOFOLLOW hits a symlink at the pidfile path (the flag and message exist specifically to defeat a symlink plant). The raise refuses to write and tells the operator to remove the path manually.

Source

Thrown at agent/proxy_sources/iron_proxy.py:2042

        # the previous _pid_alive check raced (rare; another start in
        # flight), OR a stale pidfile survived a crash.  Discriminate
        # and retry once with O_TRUNC if stale.
        existing_pid = _read_pid()
        if existing_pid and _pid_alive(existing_pid):
            raise RuntimeError(
                f"Another iron-proxy start appears to be in progress "
                f"(pidfile {pidfile} -> pid {existing_pid}).  "
                f"Run `hermes egress stop` if that proxy is stuck."
            )
        # Stale — unlink and retry.
        try:
            pidfile.unlink()
        except FileNotFoundError:
            pass
        fd = os.open(str(pidfile), open_flags, 0o600)
    except OSError as exc:
        # ELOOP from a planted symlink at the pidfile path.
        raise RuntimeError(
            f"Refusing to write pidfile {pidfile}: {exc}.  "
            "Remove that path manually and retry."
        ) from exc

    try:
        # Ownership check — same st_uid pattern the log file uses.
        try:
            st = os.fstat(fd)
            if hasattr(os, "getuid") and st.st_uid != os.getuid():
                raise RuntimeError(
                    f"pidfile {pidfile} has unexpected owner uid={st.st_uid}"
                )
        except AttributeError:
            pass  # Windows
        os.write(fd, str(pid).encode("utf-8"))
    finally:
        os.close(fd)

View on GitHub (pinned to c896c09c42)

Solutions

  1. Inspect and remove the offending path: `ls -l <pidfile>` then `rm <pidfile>` (follow no symlinks: use `rm` on the link itself)
  2. Fix state-dir permissions if the OSError text is 'Permission denied' (chown/chmod the parent dir)
  3. Retry `hermes egress start`

Example fix

# before: pidfile path is a symlink
ls -l ~/.hermes/iron-proxy/proxy.pid  # -> /etc/some/target
rm ~/.hermes/iron-proxy/proxy.pid
hermes egress start

# after: pidfile created as a regular 0600 file via O_EXCL|O_NOFOLLOW
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def pidfile_path_ok(p: Path) -> bool:
    if p.is_symlink():
        return False
    if p.exists():
        return p.owner() == __import__('getpass').getuser()  # or uid compare
    return p.parent.is_dir()

# before start: if not pidfile_path_ok(pidfile): inspect/remove it

Type guard

def is_regular_owned(p: Path) -> bool:
    import os
    try:
        st = p.lstat()
    except FileNotFoundError:
        return True
    import stat as S
    return S.S_ISREG(st.st_mode) and st.st_uid == os.getuid()

Try / catch

try:
    start_proxy(...)
except RuntimeError as e:
    if 'Refusing to write pidfile' in str(e):
        pidfile.unlink(missing_ok=True)  # removes symlink itself, no follow
        start_proxy(...)

Prevention

When it happens

Trigger: A symlink (or hard link to a sensitive file / path with too many symlink hops) exists where the pidfile should be created; also other open() failures like EACCES on a restrictive state dir.

Common situations: An attacker or misguided script symlinked the pidfile path at /etc/passwd-style targets; a leftover symlink from an experiment; HERMES_HOME/state dir permissions changed so open(O_CREAT) fails with EACCES.

Related errors


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