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 # WindowsView on GitHub (pinned to c896c09c42)
Solutions
- 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.
- If you want logs elsewhere, configure the daemon's logging target properly instead of symlinking the file.
- 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
- Never replace the daemon log with a symlink; configure log destinations properly.
- Keep one owning user for all egress daemon invocations.
- If this fires unexpectedly, investigate for same-uid tampering — the refusal is a security control, not an inconvenience.
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
- Refusing to start: could not pre-create audit log {audit_pat
- Unsafe SSH control dir: ${controlDir} is a symlink.
- Unsafe SSH control dir: ${controlDir} is owned by uid ${st.u
- iron-proxy log {log_path} has unexpected owner uid={st.st_ui
- unsafe token directory
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/9bc355798c9712b3.
Report an issue: GitHub.