{"record":{"id":"e183ee6b86bf21eb","repo":"NousResearch/hermes-agent","slug":"iron-proxy-log-log-path-has-unexpected-owner-uid","errorCode":null,"errorMessage":"iron-proxy log {log_path} has unexpected owner uid={st.st_uid}; refusing to write.","messagePattern":"iron-proxy log (.+?) has unexpected owner uid=(.+?); refusing to write\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/proxy_sources/iron_proxy.py","lineNumber":1856,"sourceCode":"        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\n\n    try:\n        # Use the fd directly via the dup mechanism; Popen will dup() it\n        # into the child so we can close ours unconditionally below.\n        # NOTE: on Windows ``start_new_session`` is invalid; we don't\n        # support Windows for the proxy (the binary itself doesn't ship)\n        # but the kwarg is POSIX-only and silently ignored on Win.\n        popen_kwargs: Dict = dict(\n            env=env,\n            stdin=subprocess.DEVNULL,\n            stdout=log_fd,\n            stderr=subprocess.STDOUT,\n        )","sourceCodeStart":1838,"sourceCodeEnd":1874,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/proxy_sources/iron_proxy.py#L1838-L1874","documentation":"After opening the iron-proxy log file (and fchmod'ing it to 0600), start_proxy fstat()s the fd and verifies st_uid matches the current effective uid, mirroring the pidfile ownership check. If the file on disk is owned by another uid (e.g. a pre-existing file created by root or another user, or a planted file at the log path), it raises rather than letting the proxy write into a file the current user does not own. This is a defensive check against symlink/file-planting attacks on HERMES_HOME paths.","triggerScenarios":"Calling start_proxy() when the log path (under the iron-proxy state dir) already exists and is owned by a different uid — e.g. a previous root-run of `hermes egress start`, restoring a state dir with sudo/cp -a, or a hostile pre-created file. Only fires on POSIX (guarded by hasattr(os, 'getuid')).","commonSituations":"Running `sudo hermes egress start` once and then running as the normal user; copying ~/.hermes between users with ownership preserved; shared machines where another account wrote to the same HERMES_HOME; CI containers that pre-create dirs as a different uid.","solutions":["Remove or reown the offending log file: find the path from the error message and `rm <log_path>` (or `chown $(id -u) <log_path>`), then retry `hermes egress start`","If the whole state dir is mis-owned, fix it once: `chown -R $(id -u):$(id -g) <proxy-state-dir>`","Never run the egress proxy under sudo/root; if you did, clean up all files it created in the state dir","Verify no symlink exists at the log path (`ls -l <log_path>`) — a planted symlink is treated the same way"],"exampleFix":"# before: log file owned by root after a sudo run\n# RuntimeError: iron-proxy log ... unexpected owner uid=0\nsudo rm /home/user/.hermes/iron-proxy/proxy.log\nhermes egress start\n\n# after: start succeeds, log recreated with current uid","handlingStrategy":"validation","validationCode":"import os, stat\nfrom pathlib import Path\n\ndef log_path_safe(p: Path) -> bool:\n    if p.is_symlink():\n        return False\n    try:\n        st = p.stat()\n    except FileNotFoundError:\n        return os.stat(p.parent).st_uid == os.getuid()  # parent owned by us\n    return st.st_uid == os.getuid() and not stat.S_ISLNK(st.st_mode)\n\n# before start_proxy(): assert log_path_safe(log_path) else remove/fix it","typeGuard":"def is_owned_log(p: Path) -> bool:\n    try:\n        st = p.lstat()\n    except FileNotFoundError:\n        return True\n    return st.st_uid == os.getuid() and not (st.st_mode & 0o170000) == 0o120000","tryCatchPattern":"try:\n    start_proxy(...)\nexcept RuntimeError as e:\n    if \"unexpected owner\" in str(e):\n        log_path = Path(str(e).split()[3])  # remove/reown, then retry once\n        os.remove(log_path)\n        start_proxy(...)","preventionTips":["Never run `hermes egress start` under sudo — mixed-uid file creation is the top cause","After restoring or copying a HERMES_HOME, run `chown -R $(id -u)` over it","Pre-flight: lstat the log path before start and require uid match / no symlink"],"tags":["security","filesystem","permissions","egress-proxy"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}