e88c9f0ef2
`hermes import` wrote every zip member with `open(target, "wb")` followed by `dst.write(src.read())`, at both restore sites in `run_import`. Opening for write truncates the user's existing file to zero *before* any replacement bytes exist, so a Ctrl-C, an ENOSPC, a corrupt zip member, or a crash leaves `config.yaml`, `.env`, or an external provider config (e.g. `~/.honcho/config.json`) empty with nothing behind it — during the disaster-recovery path the user is running precisely because they already lost something. The `_external/` branch writes outside HERMES_HOME, into third-party configs under the user's home, so the blast radius is not confined to Hermes state. Both sites now stage the member into the target's own directory, fsync it, and publish with `utils.atomic_replace`, so the target only ever moves from its old contents to the complete new contents. `atomic_replace` rather than a bare `os.replace`: it resolves a symlinked target first, so deployments that link `config.yaml` into a dotfiles repo keep the link instead of having it silently swapped for a regular file (#16743), and it falls back to copy/fsync/unlink on EXDEV/EBUSY for cross-device and bind-mount installs. Members stream through `shutil.copyfileobj` instead of being read whole into memory. The temp file is removed on any failure so a partial import leaves no residue, and permission bits are carried across the replace so mkstemp's 0600 does not silently tighten restored files. This extends the module's own established idiom — `backup.py` already publishes atomically via `os.replace` in `_atomic_output_path` and in the snapshot writer — into the one path that still overwrote user files in place.