NousResearch/hermes-agent · error · anyhow::Error

could not move existing app aside at {} (leaving it in place

Error message

could not move existing app aside at {} (leaving it in place): {err}

What it means

First phase of the atomic bundle swap in swap_in_new_bundle: renaming the existing target .app to <target>.hermes-update-old failed, so the updater removes the staged tmp copy and leaves the original app untouched. The invariant is that a failed update must never brick the install — bail before touching the original rather than risk deleting the running app with no replacement.

Source

Thrown at apps/bootstrap-installer/src-tauri/src/update.rs:1084

    Ok(target_app.to_path_buf())
}

/// Move a freshly-staged bundle (`tmp`) into place at `target`, parking any
/// existing bundle at `old` so the move can succeed (macOS `rename` won't
/// overwrite a non-empty directory).
///
/// Invariant: on ANY failure path, `target` is left pointing at a working
/// bundle — either the original (rolled back from `old`) or untouched — and we
/// never delete the running app with no replacement in place. The staged `tmp`
/// copy is cleaned up on failure.
async fn swap_in_new_bundle(tmp: &Path, target: &Path, old: &Path) -> Result<()> {
    let moved_old = if target.exists() {
        if let Err(err) = tokio::fs::rename(target, old).await {
            // Could not move the existing app aside. Leave it untouched and
            // bail — a failed update must not brick the install.
            remove_dir_if_exists(tmp).await;
            return Err(anyhow!(
                "could not move existing app aside at {} (leaving it in place): {err}",
                target.display()
            ));
        }
        true
    } else {
        false
    };
    if let Err(err) = tokio::fs::rename(tmp, target).await {
        // Restore the original app from the backup so the user keeps a working
        // install, and clean up the staged copy.
        if moved_old {
            let _ = tokio::fs::rename(old, target).await;
        }
        remove_dir_if_exists(tmp).await;
        return Err(anyhow!("installing updated app at {}: {err}", target.display()));
    }
    remove_dir_if_exists(old).await;

View on GitHub (pinned to c896c09c42)

Solutions

  1. Quit Hermes completely (check Activity Monitor) and retry the update.
  2. Remove leftover <app>.hermes-update-old / -new siblings from the previous failed attempt.
  3. Fix permissions on the target's parent directory (chown / reinstall into user-writable location).
  4. If /Applications requires admin, run the updater with the necessary privileges or move the app to ~/Applications.
Defensive patterns

Strategy: validation

Validate before calling

// Pre-swap checks: old app not running, no stale leftovers, writable parent.
fn swap_precheck(target: &std::path::Path) -> Result<(), String> {
    let old = std::path::PathBuf::from(format!("{}.hermes-update-old", target.display()));
    let tmp = std::path::PathBuf::from(format!("{}.hermes-update-new", target.display()));
    if old.exists() { return Err(format!("stale leftover {} from a failed update — remove it first", old.display())); }
    if tmp.exists() { return Err(format!("stale leftover {} — remove it first", tmp.display())); }
    if target.parent().map(|p| p.metadata().is_ok_and(|m| m.permissions().readonly())).unwrap_or(true) {
        return Err("target parent directory is read-only".into());
    }
    Ok(())
}

Try / catch

// The function already implements the correct pattern: on failed move-aside, delete tmp and
// leave the original untouched — never continue into a state where the target is deleted.
match tokio::fs::rename(target, old).await {
    Ok(()) => { /* proceed to rename tmp -> target */ }
    Err(err) => {
        remove_dir_if_exists(tmp).await; // staged copy cleaned
        return Err(anyhow!("could not move existing app aside at {} (leaving it in place): {err}", target.display()));
    }
}

Prevention

When it happens

Trigger: The target .app is busy — macOS refuses to rename a bundle that is running or has files held open; permission denied on /Applications (app installed without user-writable perms); the .hermes-update-old leftover from a prior crashed update already exists as a file where a directory rename is required.

Common situations: The old Hermes.app is still running when the swap starts (stage-1 race); user-installed into /Applications under an admin context so the rename needs elevation; leftover stale .hermes-update-old path from an earlier failed run.

Related errors


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