{"record":{"id":"9bc355798c9712b3","repo":"NousResearch/hermes-agent","slug":"refusing-to-write-iron-proxy-log-log-path-exc","errorCode":null,"errorMessage":"Refusing to write iron-proxy log {log_path}: {exc}.  Remove that path manually and retry.","messagePattern":"Refusing to write iron-proxy log (.+?): (.+?)\\.  Remove that path manually and retry\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/proxy_sources/iron_proxy.py","lineNumber":1843,"sourceCode":"    log_path = _proxy_state_dir() / \"iron-proxy.log\"\n    # Keep ownership of the fd tight: open with explicit 0o600 so the\n    # log doesn't get world-readable under a slack umask, then close it\n    # immediately after Popen (the child has its own dup).  Without the\n    # close-on-success path, every restart leaked one fd in the Hermes\n    # process.\n    #\n    # O_NOFOLLOW (defence-in-depth, same threat model as the pidfile\n    # path): a same-uid attacker who plants ``iron-proxy.log`` as a\n    # symlink to e.g. ``~/.ssh/authorized_keys`` would otherwise cause\n    # every restart to append daemon diagnostics to that file.\n    log_open_flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND\n    if hasattr(os, \"O_NOFOLLOW\"):\n        log_open_flags |= os.O_NOFOLLOW\n    try:\n        log_fd = os.open(str(log_path), log_open_flags, 0o600)\n    except OSError as exc:\n        # ELOOP from a planted symlink — refuse with a clear error.\n        raise RuntimeError(\n            f\"Refusing to write iron-proxy log {log_path}: {exc}.  \"\n            \"Remove that path manually and retry.\"\n        ) from exc\n    try:\n        os.fchmod(log_fd, 0o600)  # tighten if file pre-existed\n    except OSError:\n        pass\n    # Verify ownership — same st_uid check the pidfile uses.\n    try:\n        st = os.fstat(log_fd)\n        if hasattr(os, \"getuid\") and st.st_uid != os.getuid():\n            os.close(log_fd)\n            raise RuntimeError(\n                f\"iron-proxy log {log_path} has unexpected owner \"\n                f\"uid={st.st_uid}; refusing to write.\"\n            )\n    except AttributeError:\n        pass  # Windows","sourceCodeStart":1825,"sourceCodeEnd":1861,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/proxy_sources/iron_proxy.py#L1825-L1861","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import os\nfrom pathlib import Path\n\ndef log_path_safe(path: Path) -> bool:\n    if path.is_symlink():\n        return False\n    if path.exists():\n        return path.stat().st_uid == os.geteuid()\n    return os.access(path.parent, os.W_OK)","typeGuard":null,"tryCatchPattern":"try:\n    start_proxy()\nexcept RuntimeError as e:\n    if \"Refusing to write iron-proxy log\" in str(e):\n        # symlink or foreign-owned log — remove the named path and retry\n        raise","preventionTips":["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."],"tags":["security","filesystem","permissions","symlink","iron-proxy"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}