NousResearch/hermes-agent · error · RuntimeError

Refusing to start: could not pre-create audit log {audit_pat

Error message

Refusing to start: could not pre-create audit log {audit_path} with restrictive permissions ({exc}).  Move or chmod any existing file at that path and retry.

What it means

Before launching the daemon, start_proxy() pre-creates the audit log with O_WRONLY|O_CREAT|O_APPEND (+O_NOFOLLOW where available) and force-chmods it 0600, so the audit trail is never world-readable and never follows a planted symlink. If the os.open/fchmod fails (wrong permissions on an existing file, symlink attack ELOOP, unwritable directory, SELinux denial), startup is refused rather than proceeding with a weakened audit trail.

Source

Thrown at agent/proxy_sources/iron_proxy.py:1352

    file is non-load-bearing until the version bump — but the qualified
    message keeps operators from wiring monitoring to a path that can't
    exist.
    """

    try:
        # Use os.open + O_CREAT to avoid races on the chmod.
        open_flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
        if hasattr(os, "O_NOFOLLOW"):
            open_flags |= os.O_NOFOLLOW
        fd = os.open(str(audit_path), open_flags, 0o600)
        try:
            # Tighten perms even if the file already existed under a
            # slacker umask.
            os.fchmod(fd, 0o600)
        finally:
            os.close(fd)
    except OSError as exc:
        raise RuntimeError(
            f"Refusing to start: could not pre-create audit log "
            f"{audit_path} with restrictive permissions ({exc}).  "
            f"Move or chmod any existing file at that path and retry."
        ) from exc


def write_proxy_config(config: Dict) -> Path:
    """Serialize the config dict to ``<hermes_home>/proxy/proxy.yaml``.

    Uses ``yaml.safe_dump`` so we never emit Python tags.
    """

    try:
        import yaml  # PyYAML is already a Hermes dep
    except ImportError as exc:
        raise RuntimeError(
            "PyYAML is required to write the iron-proxy config but is not "
            "installed."

View on GitHub (pinned to c896c09c42)

Solutions

  1. Inspect the path in the message: `ls -l <audit_path>` — if it's a symlink or foreign-owned file, remove or chown/chmod it (chmod 600) and retry.
  2. Ensure the parent proxy state directory is owned by the user running Hermes.
  3. Don't run `hermes egress start` under mixed sudo/non-sudo invocations; pick one identity.
Defensive patterns

Strategy: validation

Validate before calling

import os
from pathlib import Path

def audit_log_writable(path: Path) -> bool:
    if path.is_symlink():
        return False
    if path.exists():
        st = path.stat()
        return os.geteuid() == st.st_uid and (st.st_mode & 0o600 == 0o600)
    return os.access(path.parent, os.W_OK)

Try / catch

try:
    start_proxy()
except RuntimeError as e:
    if "audit log" in str(e):
        # fix perms/ownership of the named path, then retry
        raise

Prevention

When it happens

Trigger: start_proxy() / `hermes egress start` when the audit log path exists with wrong ownership/perms (e.g. created earlier by root, or 0644 with a sticky context), is a symlink (O_NOFOLLOW → ELOOP), or its parent dir is not writable by the current user.

Common situations: Running the daemon once under sudo then again as the normal user; restored backups that lost ownership; security modules (SELinux) denying the open; a same-uid attacker planting a symlink.

Related errors


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