diff --git a/crates/native/src/install.rs b/crates/native/src/install.rs
--- a/crates/native/src/install.rs
+++ b/crates/native/src/install.rs
@@ -73,6 +73,9 @@ pub fn install_at(
     let mut commit = commit;
     #[cfg(not(windows))]
     let _ = commit;
+    if let Some(message) = existing.and_then(|installed| managed_elsewhere(app, installed, false)) {
+        return Err(message);
+    }
     #[allow(unused_variables)]
     let destination = destination(app, kind, existing, previous, job.install_locations)?;
     if let Some(inst) = existing
@@ -330,6 +333,11 @@ fn published_checksum(release: &Release, asset: &Asset, http: &Http) -> Result<O
 /// package, but updated by the user's AUR helper, which builds as the user and can't run behind
 /// the password prompt.
 pub fn managed_elsewhere(app: &CraftApp, installed: &Installed, uninstall: bool) -> Option<String> {
+    // Applications-folder links and launcher records do not transfer ownership of a Nix output.
+    #[cfg(unix)]
+    if crate::nix::is_managed(Path::new(&installed.location)) {
+        return Some(format!("{} was installed outside the launcher. Update or remove it the way it was installed.", app.name));
+    }
     match installed.source {
         InstallSource::Pacman if !uninstall => {
             let pkg = installed.package.clone().unwrap_or_else(|| app.id.to_string());
@@ -1404,6 +1412,36 @@ pub mod windows {
 mod tests {
     use super::*;
 
+    #[cfg(unix)]
+    #[test]
+    fn nix_store_copies_refuse_reinstall_and_removal_before_starting_work() {
+        let app = CraftApp::by_id("photocraft").unwrap();
+        let dir = tempfile::tempdir().unwrap();
+        let paths = Paths::under(dir.path());
+        let http = Http::new().unwrap();
+        let stop = Stop::default();
+        let mut phases = Vec::new();
+        for source in [InstallSource::MacApp, InstallSource::Launcher, InstallSource::Binary] {
+            let installed =
+                Installed { version: None, source, location: "/nix/store/hash-photocraft/Applications/PhotoCraft.app".into(), package: None };
+            let message = managed_elsewhere(app, &installed, false).unwrap();
+            assert_eq!(managed_elsewhere(app, &installed, true).unwrap(), message);
+            let mut phase = |phase| phases.push(phase);
+            let locations = InstallLocations::default();
+            let mut job = Job { http: &http, paths: &paths, stop: &stop, phase: &mut phase, install_locations: &locations, replace: false };
+            // The deliberately empty release and asset would fail validation if any download
+            // work ran. Ownership must take precedence over either validation or installation.
+            let result = install(app, &Release::default(), &Asset::default(), PackageKind::Dmg, Some(&installed), None, &mut job);
+            assert_eq!(result.unwrap_err(), message);
+            let result = install_at(app, &Release::default(), &Asset::default(), PackageKind::Dmg, Some(&installed), None, &mut job, None);
+            assert_eq!(result.unwrap_err(), message);
+            assert_eq!(uninstall(app, &installed, None, &paths, &stop, &mut phase).unwrap_err(), message);
+            assert_eq!(installed.source, source);
+        }
+        assert!(phases.is_empty());
+        assert!(!paths.downloads().exists());
+    }
+
     #[test]
     fn selected_folders_must_exist_and_leave_no_probe_files() {
         let dir = tempfile::tempdir().unwrap();
diff --git a/crates/native/src/lib.rs b/crates/native/src/lib.rs
--- a/crates/native/src/lib.rs
+++ b/crates/native/src/lib.rs
@@ -15,6 +15,8 @@ pub mod http;
 pub mod install;
 pub mod login;
 pub mod manifest;
+#[cfg(unix)]
+mod nix;
 pub mod packages;
 pub mod paths;
 #[cfg(windows)]
diff --git a/crates/native/src/nix.rs b/crates/native/src/nix.rs
new file mode 100644
--- /dev/null
+++ b/crates/native/src/nix.rs
@@ -0,0 +1,86 @@
+//! Ownership of outputs in Nix's conventional immutable store.
+//!
+//! This does not change application discovery, PATH precedence, or package-manager metadata.
+//! Custom store roots cannot be inferred from a path and are not covered here.
+
+use std::path::Path;
+
+pub(crate) fn is_managed(path: &Path) -> bool {
+    resolved_in_store(path, Path::new("/nix/store"))
+}
+
+fn resolved_in_store(path: &Path, store: &Path) -> bool {
+    if !path.is_absolute() {
+        return false;
+    }
+    // Resolve profile and Applications-folder links before checking ownership. Keep direct
+    // store paths protected even when the output has disappeared since the detection pass.
+    let resolved = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
+    output_path(&resolved, store)
+}
+
+fn output_path(path: &Path, store: &Path) -> bool {
+    path.strip_prefix(store).is_ok_and(|relative| {
+        let mut components = relative.components();
+        components.next().is_some_and(|first| matches!(first, std::path::Component::Normal(_)))
+            && components.all(|component| matches!(component, std::path::Component::Normal(_)))
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn store_outputs_and_their_contents_are_managed() {
+        for path in
+            ["/nix/store/hash-photocraft", "/nix/store/hash-photocraft/bin/photocraft", "/nix/store/hash-photocraft/Applications/PhotoCraft.app"]
+        {
+            assert!(is_managed(Path::new(path)), "{path}");
+        }
+        for path in [
+            "/nix/store",
+            "/nix/storefront/hash/bin/app",
+            "/tmp/nix/store/hash/bin/app",
+            "/nix/store/missing/../../tmp/app",
+            "ai.storyteller.photocraft",
+            "nix/store/hash/bin/app",
+        ] {
+            assert!(!is_managed(Path::new(path)), "{path}");
+        }
+    }
+
+    #[test]
+    fn profile_and_bundle_links_keep_external_ownership() {
+        use std::os::unix::fs::{PermissionsExt, symlink};
+
+        let root = tempfile::tempdir().unwrap();
+        let root = root.path().canonicalize().unwrap();
+        let store = root.join("store");
+        let bundle = store.join("hash-photocraft/Applications/PhotoCraft.app");
+        let executable = bundle.join("Contents/MacOS/PhotoCraft");
+        std::fs::create_dir_all(executable.parent().unwrap()).unwrap();
+        std::fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
+        std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap();
+        let profile = root.join("photocraft");
+        let applications = root.join("PhotoCraft.app");
+        symlink(&executable, &profile).unwrap();
+        symlink(&bundle, &applications).unwrap();
+        assert!(resolved_in_store(&profile, &store));
+        assert!(resolved_in_store(&applications, &store));
+        assert!(resolved_in_store(&executable, &store));
+        assert!(!resolved_in_store(&root, &store));
+        let manual = root.join("Manual.app");
+        std::fs::create_dir(&manual).unwrap();
+        let manual_link = root.join("Manual link.app");
+        symlink(&manual, &manual_link).unwrap();
+        assert!(!resolved_in_store(&manual_link, &store));
+        let external_executable = root.join("external-app");
+        std::fs::write(&external_executable, b"#!/bin/sh\nexit 0\n").unwrap();
+        let external_link = root.join("external-link");
+        symlink(&external_executable, &external_link).unwrap();
+        assert!(!resolved_in_store(&external_link, &store));
+        assert!(profile.is_file());
+        assert!(applications.is_dir());
+    }
+}
diff --git a/crates/native/src/self_update.rs b/crates/native/src/self_update.rs
--- a/crates/native/src/self_update.rs
+++ b/crates/native/src/self_update.rs
@@ -89,6 +89,10 @@ fn marker(paths: &Paths) -> PathBuf {
 pub fn current() -> Option<Install> {
     let exe = std::env::current_exe().ok()?;
     let exe = exe.canonicalize().unwrap_or(exe);
+    #[cfg(unix)]
+    if crate::nix::is_managed(&exe) {
+        return None;
+    }
     detect(&exe)
 }
 
