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

running ditto: {e}

Error message

running ditto: {e}

What it means

macOS-only failure in the bundle-copy step: tokio could not even spawn `/usr/bin/ditto` to copy the rebuilt .app into the staged <target>.hermes-update-new path. This is a spawn/OS-level error (ditto missing, fork failure), distinct from ditto running and exiting non-zero, which is error 555.

Source

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

            target_app.display()
        ),
    );

    if let Some(parent) = target_app.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }
    let tmp = PathBuf::from(format!("{}.hermes-update-new", target_app.display()));
    let old = PathBuf::from(format!("{}.hermes-update-old", target_app.display()));
    remove_dir_if_exists(&tmp).await;
    remove_dir_if_exists(&old).await;

    let ditto = Command::new("/usr/bin/ditto")
        .arg(&rebuilt_app)
        .arg(&tmp)
        .current_dir(crate::paths::hermes_home())
        .status()
        .await
        .map_err(|e| anyhow!("running ditto: {e}"))?;
    if !ditto.success() {
        return Err(anyhow!(
            "ditto failed while copying updated app into {}",
            tmp.display()
        ));
    }

    // Atomic-as-possible swap with rollback. Extracted so the invariant
    // (target is never left deleted-with-no-replacement) can be unit-tested
    // without ditto / a real .app bundle.
    swap_in_new_bundle(&tmp, target_app, &old).await?;

    let _ = Command::new("/usr/bin/xattr")
        .arg("-dr")
        .arg("com.apple.quarantine")
        .arg(target_app)
        .current_dir(crate::paths::hermes_home())
        .status()

View on GitHub (pinned to c896c09c42)

Solutions

  1. Run `/usr/bin/ditto --help` in Terminal to confirm ditto exists and executes on that machine.
  2. Free system resources / raise ulimits and retry.
  3. If MDM/security software blocks it, exempt the updater or perform the update manually per the docs.
  4. Check the error text after the colon — it names the exact OS errno.
Defensive patterns

Strategy: validation

Validate before calling

fn ditto_available() -> bool {
    std::path::Path::new("/usr/bin/ditto").exists()
        && std::process::Command::new("/usr/bin/ditto").arg("-h").output().is_ok()
}

if !ditto_available() {
    eprintln!("/usr/bin/ditto unavailable — cannot stage the app update on this system.");
}

Try / catch

match Command::new("/usr/bin/ditto").arg(&rebuilt_app).arg(&tmp).status().await {
    Ok(st) if st.success() => { /* proceed to swap */ }
    Ok(st) => Err(anyhow!("ditto failed while copying updated app into {}", tmp.display())),
    Err(e) => Err(anyhow!("running ditto: {e} — is /usr/bin/ditto present and executable?")),
}

Prevention

When it happens

Trigger: /usr/bin/ditto absent on a gutted/locked-down macOS (rare, SIP-modified or non-standard system); process/resource limits hit (fork: resource temporarily unavailable); a security tool blocking execution of the child from the updater.

Common situations: Heavily hardened or MDM-locked Macs restricting child process execution; extremely low file descriptors/memory during the update; running the updater inside a restricted sandbox container where /usr/bin is not visible.

Related errors


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