NousResearch/hermes-agent · error · anyhow::Error
refusing to install update into non-app path: {}
Error message
refusing to install update into non-app path: {} What it means
macOS-only guard in install_macos_app_update: the --target-app path passed to the updater does not end in .app, so copying the rebuilt bundle over it is refused. This prevents the updater from dittoing an application bundle on top of an arbitrary directory (e.g. /Applications itself or a user folder), which would corrupt the target.
Source
Thrown at apps/bootstrap-installer/src-tauri/src/update.rs:991
while let Some(arg) = iter.next() {
if arg == name {
return iter.next();
}
if let Some(value) = arg.strip_prefix(&format!("{name}=")) {
return Some(value.to_string());
}
}
None
}
#[cfg(target_os = "macos")]
async fn install_macos_app_update(
app: &AppHandle,
install_root: &Path,
target_app: &Path,
) -> Result<PathBuf> {
if target_app.extension().and_then(|e| e.to_str()) != Some("app") {
return Err(anyhow!(
"refusing to install update into non-app path: {}",
target_app.display()
));
}
let rebuilt_app = crate::bootstrap::resolve_hermes_desktop_app(install_root).ok_or_else(|| {
anyhow!(
"desktop rebuild succeeded but no Hermes.app was found under {}",
install_root.join("apps").join("desktop").join("release").display()
)
})?;
let same = match (rebuilt_app.canonicalize(), target_app.canonicalize()) {
(Ok(a), Ok(b)) => a == b,
_ => rebuilt_app == target_app,
};
if same {
emit_log(View on GitHub (pinned to c896c09c42)
Solutions
- Pass the full path to the .app bundle, e.g. --target-app /Applications/Hermes.app.
- Check the updater command line: only macOS parses target-app args, so verify argument order after any new flags.
- If you really want a non-bundle destination, that flow is unsupported — install the bundle and symlink instead.
Example fix
# before hermes-setup --update --target-app /Applications # after hermes-setup --update --target-app /Applications/Hermes.app
Defensive patterns
Strategy: validation
Validate before calling
fn valid_target_app(p: &std::path::Path) -> bool {
p.extension().and_then(|e| e.to_str()) == Some("app")
&& p.file_name().map(|n| n != "app").unwrap_or(false) // not a bare directory named 'app'
}
if !valid_target_app(&target_app) {
eprintln!("--target-app must be a .app bundle path, got {}", target_app.display());
} Type guard
fn is_app_bundle(p: &std::path::Path) -> bool {
p.extension().and_then(|e| e.to_str()) == Some("app")
&& p.join("Contents").join("MacOS").exists()
} Prevention
- Always pass the full bundle path (/Applications/Hermes.app), never the containing folder.
- Validate CLI args in wrapper scripts before invoking the updater.
- Keep argument order stable when adding new updater flags.
When it happens
Trigger: Invoking the updater with a --target-app argument that is a directory, .dmg, or path with a trailing component not named *.app; a wrapper script passing an install location instead of a bundle path; target_app_from_args parsing an unexpected flag as the path.
Common situations: A custom launchd/scripted update flow passes /Applications instead of /Applications/Hermes.app; a typo in the argument; argument order swapped so a branch name lands in the target-app position.
Related errors
- desktop rebuild succeeded but no Hermes.app was found under
- running ditto: {e}
- ditto failed while copying updated app into {}
- could not move existing app aside at {} (leaving it in place
- installing updated app at {}: {err}
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/ecc28ca6eabda8db.
Report an issue: GitHub.