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
- Inspect and remove the offending path: `ls -l <pidfile>` then `rm <pidfile>` (follow no symlinks: use `rm` on the link itself)
- Fix state-dir permissions if the OSError text is 'Permission denied' (chown/chmod the parent dir)
- 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
- Treat any symlink at the pidfile path as hostile — remove the link, never write through it
- Lock down the proxy state dir (0700, your uid) so others can't plant files
- Alert on this error in monitoring: it can indicate a planted path, not just cruft
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
- iron-proxy log {log_path} has unexpected owner uid={st.st_ui
- pidfile {pidfile} has unexpected owner uid={st.st_uid}
- Unsafe SSH control dir: ${controlDir} is a symlink.
- Unsafe SSH control dir: ${controlDir} is not a directory.
- Unsafe SSH control dir: ${controlDir} is owned by uid ${st.u
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/ad13b9a95a8c8430.
Report an issue: GitHub.