hermes - 💡(How to fix) Fix fix: atomic_replace() fails with EXDEV when HERMES_HOME is a cross-filesystem symlink [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

atomic_replace() in utils.py uses os.replace() to atomically move a temp file onto the target path. However, when ~/.hermes/ is a symlink pointing to a different filesystem (e.g., /mnt/data/hermes-data/), os.replace() raises OSError: [Errno 18] Invalid cross-device link because the temp file (created on rootfs) and the target (on a different mount) are on different filesystems.

Error Message

python3 -c " import tempfile, os tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.tmp') tmp.write(b'test') tmp.close() os.replace(tmp.name, os.path.expanduser('~/.hermes/.env'))

OSError: [Errno 18] Invalid cross-device link

"

Root Cause

https://github.com/NousResearch/hermes-agent/blob/main/utils.py#L77-L86

def atomic_replace(tmp_path, target):
    real_path = os.path.realpath(target_str) if os.path.islink(target_str) else target_str
    os.replace(str(tmp_path), real_path)  # ← fails with EXDEV on cross-fs
    return real_path

os.replace() (backed by rename() syscall) cannot move files across mount points. The fix should fall back to shutil.move() when errno == 18 (EXDEV).

Fix Action

Fixed

Code Example

# Setup: HERMES_HOME symlink to a different filesystem
ln -sf /mnt/data/hermes-data ~/.hermes
hermes config set model.default test  # Fails with EXDEV

---

python3 -c "
import tempfile, os
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.tmp')
tmp.write(b'test')
tmp.close()
os.replace(tmp.name, os.path.expanduser('~/.hermes/.env'))
# OSError: [Errno 18] Invalid cross-device link
"

---

def atomic_replace(tmp_path, target):
    real_path = os.path.realpath(target_str) if os.path.islink(target_str) else target_str
    os.replace(str(tmp_path), real_path)  # ← fails with EXDEV on cross-fs
    return real_path

---

def atomic_replace(tmp_path, target):
    real_path = os.path.realpath(target_str) if os.path.islink(target_str) else target_str
    try:
        os.replace(str(tmp_path), real_path)
    except OSError as e:
        if e.errno == 18:  # EXDEV: cross-device link
            import shutil
            shutil.move(str(tmp_path), real_path)
        else:
            raise
    return real_path
RAW_BUFFERClick to expand / collapse

title: "fix: atomic_replace() fails with EXDEV when HERMES_HOME is a cross-filesystem symlink" labels: ["bug", "gateway"]

Description

atomic_replace() in utils.py uses os.replace() to atomically move a temp file onto the target path. However, when ~/.hermes/ is a symlink pointing to a different filesystem (e.g., /mnt/data/hermes-data/), os.replace() raises OSError: [Errno 18] Invalid cross-device link because the temp file (created on rootfs) and the target (on a different mount) are on different filesystems.

Impact

  • hermes model and hermes config commands crash with OSError: [Errno 18]
  • Gateway fails to persist dedup state (feishu_seen_message_ids.json) silently
  • Channel directory state is lost on gateway restart
  • Users with ~/.hermes -> /mnt/data/... symlinks cannot use the agent at all

Reproduction

# Setup: HERMES_HOME symlink to a different filesystem
ln -sf /mnt/data/hermes-data ~/.hermes
hermes config set model.default test  # Fails with EXDEV

Or in ~/.hermes/hermes-agent:

python3 -c "
import tempfile, os
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.tmp')
tmp.write(b'test')
tmp.close()
os.replace(tmp.name, os.path.expanduser('~/.hermes/.env'))
# OSError: [Errno 18] Invalid cross-device link
"

Root Cause

https://github.com/NousResearch/hermes-agent/blob/main/utils.py#L77-L86

def atomic_replace(tmp_path, target):
    real_path = os.path.realpath(target_str) if os.path.islink(target_str) else target_str
    os.replace(str(tmp_path), real_path)  # ← fails with EXDEV on cross-fs
    return real_path

os.replace() (backed by rename() syscall) cannot move files across mount points. The fix should fall back to shutil.move() when errno == 18 (EXDEV).

Proposed Fix

def atomic_replace(tmp_path, target):
    real_path = os.path.realpath(target_str) if os.path.islink(target_str) else target_str
    try:
        os.replace(str(tmp_path), real_path)
    except OSError as e:
        if e.errno == 18:  # EXDEV: cross-device link
            import shutil
            shutil.move(str(tmp_path), real_path)
        else:
            raise
    return real_path

Related

The same cross-filesystem issue affects save_env_value() / remove_env_value() in hermes_cli/config.py, where tempfile.NamedTemporaryFile creates temp files in the default tmpdir (rootfs) while .resolve() isn't called on get_env_path(). Adding .resolve() to env_path = get_env_path() calls covers this case.

Environment

  • Hermes Agent v0.14.0+ (also present in v2026.5.28)
  • Linux with ~/.hermes symlinked to a separate mount point

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING