{"record":{"id":"ecc28ca6eabda8db","repo":"NousResearch/hermes-agent","slug":"refusing-to-install-update-into-non-app-path","errorCode":null,"errorMessage":"refusing to install update into non-app path: {}","messagePattern":"refusing to install update into non-app path: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"apps/bootstrap-installer/src-tauri/src/update.rs","lineNumber":991,"sourceCode":"    while let Some(arg) = iter.next() {\n        if arg == name {\n            return iter.next();\n        }\n        if let Some(value) = arg.strip_prefix(&format!(\"{name}=\")) {\n            return Some(value.to_string());\n        }\n    }\n    None\n}\n\n#[cfg(target_os = \"macos\")]\nasync fn install_macos_app_update(\n    app: &AppHandle,\n    install_root: &Path,\n    target_app: &Path,\n) -> Result<PathBuf> {\n    if target_app.extension().and_then(|e| e.to_str()) != Some(\"app\") {\n        return Err(anyhow!(\n            \"refusing to install update into non-app path: {}\",\n            target_app.display()\n        ));\n    }\n\n    let rebuilt_app = crate::bootstrap::resolve_hermes_desktop_app(install_root).ok_or_else(|| {\n        anyhow!(\n            \"desktop rebuild succeeded but no Hermes.app was found under {}\",\n            install_root.join(\"apps\").join(\"desktop\").join(\"release\").display()\n        )\n    })?;\n\n    let same = match (rebuilt_app.canonicalize(), target_app.canonicalize()) {\n        (Ok(a), Ok(b)) => a == b,\n        _ => rebuilt_app == target_app,\n    };\n    if same {\n        emit_log(","sourceCodeStart":973,"sourceCodeEnd":1009,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/bootstrap-installer/src-tauri/src/update.rs#L973-L1009","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nhermes-setup --update --target-app /Applications\n\n# after\nhermes-setup --update --target-app /Applications/Hermes.app","handlingStrategy":"validation","validationCode":"fn valid_target_app(p: &std::path::Path) -> bool {\n    p.extension().and_then(|e| e.to_str()) == Some(\"app\")\n        && p.file_name().map(|n| n != \"app\").unwrap_or(false) // not a bare directory named 'app'\n}\n\nif !valid_target_app(&target_app) {\n    eprintln!(\"--target-app must be a .app bundle path, got {}\", target_app.display());\n}","typeGuard":"fn is_app_bundle(p: &std::path::Path) -> bool {\n    p.extension().and_then(|e| e.to_str()) == Some(\"app\")\n        && p.join(\"Contents\").join(\"MacOS\").exists()\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["macos","validation","update","bootstrap-installer"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}