Skip to content

fix: handle EXDEV cross-device errors in atomic_replace - #33508

Closed
OmarB97 wants to merge 3 commits into
NousResearch:mainfrom
OmarB97:fix/atomic-replace-cross-device-exdev
Closed

fix: handle EXDEV cross-device errors in atomic_replace#33508
OmarB97 wants to merge 3 commits into
NousResearch:mainfrom
OmarB97:fix/atomic-replace-cross-device-exdev

Conversation

@OmarB97

@OmarB97 OmarB97 commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

When config files (e.g. ~/.hermes/config.yaml) are symlinked to a different filesystem — common on WSL where dotfiles live under /mnt/c/os.replace() fails with EXDEV (errno 18: Invalid cross-device link).

This causes every hermes config set command to crash with:

OSError: [Errno 18] Invalid cross-device link: '/home/user/.hermes/.config_xxx.tmp' -> '/mnt/c/Users/.../dotfiles/hermes/config.yaml'

Fix

Catch EXDEV in atomic_replace and fall back to shutil.copy2 + os.unlink. copy2 preserves metadata (permissions, timestamps) so the result is equivalent. Non-EXDEV errors (EACCES, etc.) still propagate normally.

This affects all atomic write paths (atomic_yaml_write, atomic_json_write, atomic_roundtrip_yaml_update) since they all route through atomic_replace.

Test Plan

  • 4 new regression tests (all passing):
    • Direct EXDEV fallback on plain file
    • EXDEV fallback preserving symlinks
    • Non-EXDEV errors are not swallowed
    • End-to-end atomic_yaml_write EXDEV recovery
  • All 12 existing symlink tests still pass

Context

WSL users who symlink their dotfiles repo to ~/.hermes/config.yaml hitting a Windows mount path under /mnt/c/. The temp file is created in the WSL filesystem (~/.hermes/), but os.replace's underlying rename() syscall can't cross filesystem boundaries.

Fixes #34252 — credit to @ccwssy for the root-cause analysis of the cross-device rename failure.

When config files (e.g. ~/.hermes/config.yaml) are symlinked to a
different filesystem — common on WSL where dotfiles live under /mnt/c/
— os.replace() fails with EXDEV (errno 18: Invalid cross-device link).

Fall back to shutil.copy2 + os.unlink when EXDEV is raised. copy2
preserves metadata (permissions, timestamps) so the result is
equivalent. Non-EXDEV errors (EACCES, etc.) still propagate.

This affects all atomic write paths (atomic_yaml_write,
atomic_json_write, atomic_roundtrip_yaml_update) since they all
route through atomic_replace.

Added 4 regression tests:
- Direct EXDEV fallback on plain file
- EXDEV fallback preserving symlinks
- Non-EXDEV errors are not swallowed
- End-to-end atomic_yaml_write EXDEV recovery
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists area/config Config system, migrations, profiles labels May 27, 2026
Omar Baradei and others added 2 commits June 15, 2026 18:41
…cross-device-exdev

# Conflicts:
#	tests/test_atomic_replace_symlinks.py
#	utils.py
@OmarB97
OmarB97 force-pushed the fix/atomic-replace-cross-device-exdev branch from 439f60a to ad7b67e Compare June 16, 2026 01:46
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the EXDEV investigation and regression coverage.

Automated hermes-sweeper review found this is already implemented on current main:

  • utils.py:114-135 catches EXDEV (and EBUSY) and performs copy, metadata preservation, fsync, and temp-file cleanup while retaining the resolved symlink target.
  • tests/test_atomic_replace_symlinks.py:245-349 already covers the mocked EXDEV path, symlink preservation, non-EXDEV propagation, and a real cross-filesystem case.
  • This landed in bf8effad0 (fix(utils): copy fallback for atomic replace across devices, fix(utils): EXDEV/EBUSY copy fallback in atomic_replace (port from gemini-cli#21541) #43852), included in v2026.6.19.

The linked #34252/#34262 discussion concerns the same root cause. Closing as superseded by current main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: atomic_replace() fails with EXDEV when HERMES_HOME is a cross-filesystem symlink

3 participants