镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

macOS FMA uninstalls: move trashed files as the user, not root (1/11) - #57086

Merged
allenhouchins merged 1 commit into
mainfrom
allenhouchins/fma-trash-as-user-1
Nov 1, 2026
Merged

allenhouchins merged 1 commit into
mainfrom
allenhouchins/fma-trash-as-user-1

Conversation

@allenhouchins

@allenhouchins allenhouchins commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Part 1 of 11, split from #56846. That PR changes 992 files, and the auto-approval only covers PRs under 100 files. The 11 PRs touch disjoint files, so they merge in any order without conflicts, and together they match #56846's tree exactly. Merge this one first so the generator change lands before, or with, the rest of the outputs.

Other parts:

Summary

Hardening for the trash() helper in generated macOS FMA uninstall scripts. Fleet runs these scripts as root. trash() moved the cask's zap paths into the console user's ~/.Trash with a root mv -f. Those paths and ~/.Trash both belong to that user, so root shouldn't be the one writing there.

  • Paths in the user's home (~/...) are moved to the Trash as the user, with sudo -u "$logged_in_user" mv -f. If the user's move fails, the script logs Failed to move <path> to the Trash. and root doesn't take over.
  • Other paths are deleted as root by a new remove_system_path helper instead of being moved into the user's Trash, such as /Library/LaunchDaemons/*.plist, /Library/PrivilegedHelperTools/* and /Users/Shared/*. The helper already deleted these at the login window (macOS FMA uninstalls: stop when the app won't quit, and work when no one is logged in #56457).
  • A symlinked parent folder can't redirect a root delete. rm -rf doesn't follow a symlink at the end of a path, but it does follow one in a parent folder. For example, a user who creates /Users/Shared/TechSmith as a symlink first could aim Camtasia's /Users/Shared/TechSmith/Camtasia delete at a root-only folder. remove_system_path uses cd -P to enter the real parent folder, checks that it's the folder the path names, and deletes ./<name> from there, so a symlink swapped in after the check has no effect. Only top-level symlinks such as /tmp and /etc are followed, because users can't write to /. A path that fails the check is left in place and logged as Skipped <path>: a folder in its path is a symlink.
  • At the login window, ~ paths are still skipped, and cleanup stays best-effort: trash() never fails the uninstall.

Behavior change: system paths listed in a cask's zap trash: are deleted, not moved to the user's Trash. A system path with a symlink in its parent folders below the top level is skipped.

A shell without Full Disk Access can still move files into ~/.Trash by name (only listing it is blocked), so the move as the user doesn't depend on fleetd's TCC grants.

Outputs

Across all 11 parts, 974 macOS outputs change. Each changes only its uninstall script and uninstall_script_ref. Version, URL, sha256, install script and queries are byte-identical to main.

This part has the generator, its tests, the 16 custom uninstall scripts, and 73 outputs:

  • 16 custom-script outputs: each embeds its updated input script. The custom scripts carry a copy of trash(), now the new one plus remove_system_path(), including Citrix Workspace's, added on main in Citrix Workspace FMA: remove the runtime-copied updater on uninstall #57081. TestCustomUninstallScriptsUseCurrentTrash keeps both identical to the generator.
  • 20 more frozen outputs: the ingest job never rewrites these, so they ship here. (WezTerm is both custom and frozen.)
  • 37 generated outputs, 010-editor through alcove.

Parts 2–11 have the other 901 generated outputs. The outputs were built and cross-checked against cmd/maintained-apps regeneration in #56846.

Because these outputs change, the darwin validator runs on each part's apps. Expect some failures from apps that had already drifted upstream. The Check Script Diff job will fail here: its comment is over GitHub's 65,536-character limit at this size. It isn't a required check.

Checklist for submitter

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

TestUninstallScriptTrash (renamed from TestUninstallScriptTrashWithoutGUIUser) runs generated trash calls in bash with the user's home under a temp dir. A bare mv is stubbed to fail, so the test fails if root ever moves a file. It checks that:

  • home paths really land in ~/.Trash through sudo -u
  • system paths are deleted
  • a system path under a symlinked parent folder, given directly or through a glob, is skipped, and the files the symlink points to stay in place
  • a failed user move is reported and nothing is removed as root

The test fails against the old helper, and also fails with the symlink check in remove_system_path turned off.

Also checked:

  • go test ./cmd/maintained-apps/... ./ee/maintained-apps/... passes on this branch alone, and on each of parts 2–11 alone
  • every custom script passes bash -n
  • run directly on macOS, remove_system_path deletes a normal path, skips one under a symlinked folder, removes a symlink at the end of the path without touching its target, and follows /tmp

Not yet run as root by fleetd on a real Mac.

AI

AI: Claude Code (claude-opus-5-5)

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/010-editor/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 849c40bd -> 58d6202b ===

--- /tmp/old.V9xREF	2026-10-31 18:26:44.744546262 +0000
+++ /tmp/new.bB44G1	2026-10-31 18:26:44.744546262 +0000
@@ -57,6 +57,25 @@
   sudo rmdir "$dir" 2>/dev/null || true
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -64,19 +83,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -95,11 +113,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -113,9 +131,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/1password/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 91ae9d61 -> 4a4bdb86 ===

--- /tmp/old.zzLl5P	2026-10-31 18:26:44.778546315 +0000
+++ /tmp/new.Keb24Y	2026-10-31 18:26:44.779546317 +0000
@@ -143,6 +143,25 @@
   done
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -150,19 +169,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -181,11 +199,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -199,9 +217,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/4k-slideshow-maker/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 6df6a6df -> fbde5bd7 ===

--- /tmp/old.pEX1Nb	2026-10-31 18:26:44.811546367 +0000
+++ /tmp/new.SiaMtU	2026-10-31 18:26:44.811546367 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/4k-stogram/darwin.json

=== Install Script (no changes) ===
=== Uninstall // fd83e2a6 -> 28cd97a1 ===

--- /tmp/old.cQF8jR	2026-10-31 18:26:44.844546419 +0000
+++ /tmp/new.UEcPfg	2026-10-31 18:26:44.844546419 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/4k-video-downloader/darwin.json

=== Install Script (no changes) ===
=== Uninstall // e95017d9 -> dba3da33 ===

--- /tmp/old.saeEtG	2026-10-31 18:26:44.876546470 +0000
+++ /tmp/new.j4Shsh	2026-10-31 18:26:44.876546470 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/4k-video-to-mp3/darwin.json

=== Install Script (no changes) ===
=== Uninstall // fe3bd02b -> 2f98ebd2 ===

--- /tmp/old.SbnGIC	2026-10-31 18:26:44.908546520 +0000
+++ /tmp/new.cWnpDV	2026-10-31 18:26:44.908546520 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/4k-youtube-to-mp3/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 04a9d533 -> 0c249725 ===

--- /tmp/old.mx9AGH	2026-10-31 18:26:44.944546577 +0000
+++ /tmp/new.mHg6sO	2026-10-31 18:26:44.944546577 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/8x8-work/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 53727df6 -> 0922e23f ===

--- /tmp/old.gltlXJ	2026-10-31 18:26:44.981546635 +0000
+++ /tmp/new.TSePh6	2026-10-31 18:26:44.981546635 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/a-better-finder-rename/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 4ccd271d -> 71b38f2a ===

--- /tmp/old.hRRN5g	2026-10-31 18:26:45.013546686 +0000
+++ /tmp/new.fOXmIT	2026-10-31 18:26:45.013546686 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/ableton-live-suite/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 0f6ca42d -> b933edb1 ===

--- /tmp/old.kfmdmg	2026-10-31 18:26:45.045546736 +0000
+++ /tmp/new.NgbCxb	2026-10-31 18:26:45.045546736 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/acorn/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 83ec2d07 -> 6b6899e1 ===

--- /tmp/old.GytcBb	2026-10-31 18:26:45.076546785 +0000
+++ /tmp/new.LBKpL0	2026-10-31 18:26:45.077546787 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/activedock/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 06db0cc2 -> cda958ce ===

--- /tmp/old.vRtbXH	2026-10-31 18:26:45.110546839 +0000
+++ /tmp/new.mbweUP	2026-10-31 18:26:45.110546839 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/activitywatch/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 35bc8a92 -> 26e5747b ===

--- /tmp/old.c8yNcR	2026-10-31 18:26:45.143546891 +0000
+++ /tmp/new.0Gy89G	2026-10-31 18:26:45.143546891 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/actual/darwin.json

=== Install Script (no changes) ===
=== Uninstall // cb140292 -> 34618eb1 ===

--- /tmp/old.tQL3LG	2026-10-31 18:26:45.175546941 +0000
+++ /tmp/new.1Lv4So	2026-10-31 18:26:45.175546941 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/adguard/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 7779731c -> 81490d6a ===

--- /tmp/old.MGF4Da	2026-10-31 18:26:45.208546993 +0000
+++ /tmp/new.QhIKZn	2026-10-31 18:26:45.208546993 +0000
@@ -218,6 +218,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -225,19 +244,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -256,11 +274,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -274,9 +292,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/adlock/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 77a66f0c -> 59f03c9f ===

--- /tmp/old.M1b7S6	2026-10-31 18:26:45.241547045 +0000
+++ /tmp/new.Ct85TY	2026-10-31 18:26:45.241547045 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/adobe-acrobat-pro/darwin.json

=== Install Script (no changes) ===
=== Uninstall // b050ed0e -> 1e117bdc ===

--- /tmp/old.7lBevS	2026-10-31 18:26:45.274547097 +0000
+++ /tmp/new.WRvSpi	2026-10-31 18:26:45.274547097 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/adobe-acrobat-reader/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 7179c3a1 -> aab0372b ===

--- /tmp/old.vKiX0N	2026-10-31 18:26:45.310547154 +0000
+++ /tmp/new.nDiHsP	2026-10-31 18:26:45.310547154 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/adobe-digital-editions/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 48744f71 -> 03cbb2b0 ===

--- /tmp/old.wYuuaj	2026-10-31 18:26:45.343547206 +0000
+++ /tmp/new.zxQNUX	2026-10-31 18:26:45.343547206 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/adobe-dng-converter/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 74580991 -> cc65eb0e ===

--- /tmp/old.KToNEm	2026-10-31 18:26:45.375547257 +0000
+++ /tmp/new.CB9Xmp	2026-10-31 18:26:45.375547257 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/advanced-renamer/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 29568fa2 -> 825d73bd ===

--- /tmp/old.l0tsSX	2026-10-31 18:26:45.407547307 +0000
+++ /tmp/new.b3fp8d	2026-10-31 18:26:45.407547307 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity-designer/darwin.json

=== Install Script (no changes) ===
=== Uninstall // d2c02c71 -> 8db4838c ===

--- /tmp/old.z3eHIs	2026-10-31 18:26:45.439547357 +0000
+++ /tmp/new.YEX8t7	2026-10-31 18:26:45.439547357 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity-designer@1/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 02df3487 -> e85c80af ===

--- /tmp/old.FqYH7g	2026-10-31 18:26:45.474547413 +0000
+++ /tmp/new.dlp63B	2026-10-31 18:26:45.474547413 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity-photo/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 642ed154 -> c9d151f9 ===

--- /tmp/old.ggL0ky	2026-10-31 18:26:45.506547463 +0000
+++ /tmp/new.i8fNFu	2026-10-31 18:26:45.506547463 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity-photo@1/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 7adb91a2 -> 0a468663 ===

--- /tmp/old.Pmxa1x	2026-10-31 18:26:45.539547515 +0000
+++ /tmp/new.ZL41tB	2026-10-31 18:26:45.539547515 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity-publisher/darwin.json

=== Install Script (no changes) ===
=== Uninstall // d26ee6ad -> a3bbcbb0 ===

--- /tmp/old.ymE1sl	2026-10-31 18:26:45.576547573 +0000
+++ /tmp/new.TaGfYK	2026-10-31 18:26:45.576547573 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity-publisher@1/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 3ce50edd -> 11ffabee ===

--- /tmp/old.C2kFy1	2026-10-31 18:26:45.608547624 +0000
+++ /tmp/new.qZIwcY	2026-10-31 18:26:45.608547624 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/affinity/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 778320cb -> 1fff133d ===

--- /tmp/old.kE784z	2026-10-31 18:26:45.640547674 +0000
+++ /tmp/new.0eHdQk	2026-10-31 18:26:45.640547674 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airbuddy/darwin.json

=== Install Script (no changes) ===
=== Uninstall // c4abe31c -> a753c19a ===

--- /tmp/old.XFUbyE	2026-10-31 18:26:45.671547723 +0000
+++ /tmp/new.CrOBqp	2026-10-31 18:26:45.671547723 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/aircall/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ca14cb0a -> 03acc2bb ===

--- /tmp/old.nut9pC	2026-10-31 18:26:45.703547774 +0000
+++ /tmp/new.7qTfoH	2026-10-31 18:26:45.703547774 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airdroid/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 121f3035 -> 9f70c2c3 ===

--- /tmp/old.AnFPTM	2026-10-31 18:26:45.735547824 +0000
+++ /tmp/new.lsk0lY	2026-10-31 18:26:45.735547824 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airparrot/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ed9bc6f3 -> 21104ec0 ===

--- /tmp/old.VBFBda	2026-10-31 18:26:45.767547875 +0000
+++ /tmp/new.CyLEuH	2026-10-31 18:26:45.767547875 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airserver/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 5f07d184 -> 44ba3f40 ===

--- /tmp/old.29rUAE	2026-10-31 18:26:45.803547931 +0000
+++ /tmp/new.k2GzTD	2026-10-31 18:26:45.803547931 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airtable/darwin.json

=== Install Script (no changes) ===
=== Uninstall // cfbe4ea7 -> 09c3fe3c ===

--- /tmp/old.p3jYia	2026-10-31 18:26:45.835547982 +0000
+++ /tmp/new.u9VVaH	2026-10-31 18:26:45.836547983 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airtame/darwin.json

=== Install Script (no changes) ===
=== Uninstall // e261b627 -> 7f3a4b16 ===

--- /tmp/old.qXy3LF	2026-10-31 18:26:45.868548034 +0000
+++ /tmp/new.ERsF86	2026-10-31 18:26:45.868548034 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/airy/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 1bdd53bf -> bd142597 ===

--- /tmp/old.bOjKG3	2026-10-31 18:26:45.900548084 +0000
+++ /tmp/new.HkCJu4	2026-10-31 18:26:45.900548084 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/akiflow/darwin.json

=== Install Script (no changes) ===
=== Uninstall // c7ccf1d0 -> d6e318c3 ===

--- /tmp/old.9LDWIF	2026-10-31 18:26:45.932548135 +0000
+++ /tmp/new.PJtEKY	2026-10-31 18:26:45.933548136 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/alcove/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 27aeb336 -> 560258cc ===

--- /tmp/old.G5HI7q	2026-10-31 18:26:45.996548236 +0000
+++ /tmp/new.I3Q5R1	2026-10-31 18:26:45.996548236 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/avast-secure-browser/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 767278d7 -> ecd7cd9e ===

--- /tmp/old.T6hq39	2026-10-31 18:26:46.033548294 +0000
+++ /tmp/new.PDDEKV	2026-10-31 18:26:46.033548294 +0000
@@ -123,6 +123,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -130,19 +149,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -161,11 +179,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -179,9 +197,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/camtasia/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 20e151f4 -> df5ce3c4 ===

--- /tmp/old.gGicQm	2026-10-31 18:26:46.067548348 +0000
+++ /tmp/new.4T2hXf	2026-10-31 18:26:46.067548348 +0000
@@ -5,6 +5,25 @@
 LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }')
 # functions
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -12,19 +31,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -43,11 +61,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -61,9 +79,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/cisco-jabber/darwin.json

=== Install Script (no changes) ===
=== Uninstall // fb3996c9 -> b6c38756 ===

--- /tmp/old.Dw3GEr	2026-10-31 18:26:46.102548403 +0000
+++ /tmp/new.gUNgY3	2026-10-31 18:26:46.102548403 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/citrix-workspace/darwin.json

=== Install Script (no changes) ===
=== Uninstall // b077b2e9 -> 610684e2 ===

--- /tmp/old.MBRpn5	2026-10-31 18:26:46.134548453 +0000
+++ /tmp/new.2Dd7YB	2026-10-31 18:26:46.135548455 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/cleanmymac/darwin.json

=== Install Script (no changes) ===
=== Uninstall // a2214fc2 -> 3bce6742 ===

--- /tmp/old.IFZBlb	2026-10-31 18:26:46.167548505 +0000
+++ /tmp/new.zie91X	2026-10-31 18:26:46.167548505 +0000
@@ -140,6 +140,25 @@
   done
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -147,19 +166,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -178,11 +196,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -196,9 +214,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/comet/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 1cbb381b -> 072a8cee ===

--- /tmp/old.UiQ5OZ	2026-10-31 18:26:46.200548558 +0000
+++ /tmp/new.wUEbx5	2026-10-31 18:26:46.200548558 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/crashplan/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 98dc4b5b -> b6ef5882 ===

--- /tmp/old.twJ7DZ	2026-10-31 18:26:46.233548610 +0000
+++ /tmp/new.EsdZeQ	2026-10-31 18:26:46.234548611 +0000
@@ -245,6 +245,25 @@
   sleep 3
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -252,19 +271,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -283,11 +301,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -301,9 +319,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/cycling74-max/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ddb0bd80 -> 81adddbe ===

--- /tmp/old.tbOXRM	2026-10-31 18:26:46.267548663 +0000
+++ /tmp/new.Ufx2bB	2026-10-31 18:26:46.267548663 +0000
@@ -5,6 +5,25 @@
 LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }')
 # functions
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -12,19 +31,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -43,11 +61,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -61,9 +79,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/downie/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 8d3773ec -> 4a9b4cc6 ===

--- /tmp/old.mZ5ggC	2026-10-31 18:26:46.300548715 +0000
+++ /tmp/new.2RCZuN	2026-10-31 18:26:46.300548715 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/dymo-connect/darwin.json

=== Install Script (no changes) ===
=== Uninstall // cfe50ddd -> aa3d5a72 ===

--- /tmp/old.jJrJVo	2026-10-31 18:26:46.334548769 +0000
+++ /tmp/new.D9oAC8	2026-10-31 18:26:46.334548769 +0000
@@ -245,6 +245,25 @@
   sleep 3
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -252,19 +271,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -283,11 +301,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -301,9 +319,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/evernote/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 0e526451 -> 7f9aa1a9 ===

--- /tmp/old.1iUNZK	2026-10-31 18:26:46.368548822 +0000
+++ /tmp/new.zKKGRV	2026-10-31 18:26:46.369548824 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/forklift/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 3e061d0c -> 2cb443a5 ===

--- /tmp/old.4UCtL5	2026-10-31 18:26:46.401548874 +0000
+++ /tmp/new.5KIsnF	2026-10-31 18:26:46.401548874 +0000
@@ -140,6 +140,25 @@
   done
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -147,19 +166,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -178,11 +196,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -196,9 +214,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/freefilesync/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ee1d08f3 -> 3d6bcfb1 ===

--- /tmp/old.OywCkv	2026-10-31 18:26:46.434548926 +0000
+++ /tmp/new.d0NjJf	2026-10-31 18:26:46.434548926 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/google-gemini/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 605aa85c -> fce44a81 ===

--- /tmp/old.OfWTrY	2026-10-31 18:26:46.468548980 +0000
+++ /tmp/new.BTadSD	2026-10-31 18:26:46.468548980 +0000
@@ -207,6 +207,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -214,19 +233,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -245,11 +263,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -263,9 +281,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/gpg-suite/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 4c789744 -> f4179907 ===

--- /tmp/old.kxshhi	2026-10-31 18:26:46.501549032 +0000
+++ /tmp/new.z7SrbW	2026-10-31 18:26:46.502549034 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/homebrew-app/darwin.json

=== Install Script (no changes) ===
=== Uninstall // fd995ba0 -> 353138cb ===

--- /tmp/old.qQfyGM	2026-10-31 18:26:46.536549087 +0000
+++ /tmp/new.q1RK0H	2026-10-31 18:26:46.536549087 +0000
@@ -5,6 +5,25 @@
 LOGGED_IN_USER=$(scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ { print $3 }')
 # functions
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -12,19 +31,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -43,11 +61,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -61,9 +79,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/intune-company-portal/darwin.json

=== Install Script (no changes) ===
=== Uninstall // c44baeb6 -> 82b077a5 ===

--- /tmp/old.GDyHrT	2026-10-31 18:26:46.570549141 +0000
+++ /tmp/new.viy69v	2026-10-31 18:26:46.570549141 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/itsycal/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 70221fd9 -> 3321751a ===

--- /tmp/old.QS4YlG	2026-10-31 18:26:46.605549196 +0000
+++ /tmp/new.9jymM6	2026-10-31 18:26:46.605549196 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/keeper-password-manager/darwin.json

=== Install Script (no changes) ===
=== Uninstall // efff3b92 -> fb583df8 ===

--- /tmp/old.fWys6a	2026-10-31 18:26:46.637549247 +0000
+++ /tmp/new.bwSAHZ	2026-10-31 18:26:46.637549247 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/kicad/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 437634ef -> fff2dc2f ===

--- /tmp/old.hDV99X	2026-10-31 18:26:46.672549302 +0000
+++ /tmp/new.4yiCgE	2026-10-31 18:26:46.672549302 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/krisp/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 10cabb7a -> 39e0e845 ===

--- /tmp/old.5rRxr1	2026-10-31 18:26:46.706549355 +0000
+++ /tmp/new.VKd1L4	2026-10-31 18:26:46.706549355 +0000
@@ -245,6 +245,25 @@
   sleep 3
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -252,19 +271,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -283,11 +301,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -301,9 +319,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/microsoft-auto-update/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 911768de -> 91a8b766 ===

--- /tmp/old.pKKAso	2026-10-31 18:26:46.740549409 +0000
+++ /tmp/new.TM5RJs	2026-10-31 18:26:46.740549409 +0000
@@ -218,6 +218,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -225,19 +244,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -256,11 +274,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -274,9 +292,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/microsoft-azure-storage-explorer/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ebe9806f -> ec318db3 ===

--- /tmp/old.hgUVHu	2026-10-31 18:26:46.772549460 +0000
+++ /tmp/new.KamgoL	2026-10-31 18:26:46.772549460 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/microsoft-defender/darwin.json

=== Install Script (no changes) ===
=== Uninstall // cb7b9d11 -> 8bbf0b48 ===

--- /tmp/old.S2bIMi	2026-10-31 18:26:46.805549512 +0000
+++ /tmp/new.lSi2CY	2026-10-31 18:26:46.806549513 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/microsoft-word/darwin.json

=== Install Script (no changes) ===
=== Uninstall // f9fa325d -> a05f7a87 ===

--- /tmp/old.yOVdTY	2026-10-31 18:26:46.839549565 +0000
+++ /tmp/new.RxQTEW	2026-10-31 18:26:46.839549565 +0000
@@ -206,6 +206,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -213,19 +232,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -244,11 +262,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -262,9 +280,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/tableau-prep/darwin.json

=== Install Script (no changes) ===
=== Uninstall // fe815c96 -> 71945cf2 ===

--- /tmp/old.TQMlyA	2026-10-31 18:26:46.873549619 +0000
+++ /tmp/new.4XOcNd	2026-10-31 18:26:46.873549619 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/tableau/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 192278a1 -> dcc28c47 ===

--- /tmp/old.AJajrG	2026-10-31 18:26:46.906549671 +0000
+++ /tmp/new.fh5g5k	2026-10-31 18:26:46.906549671 +0000
@@ -111,6 +111,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -118,19 +137,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -149,11 +167,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -167,9 +185,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/teleport-suite/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 0fdc0ba0 -> fc44df6d ===

--- /tmp/old.8hEGen	2026-10-31 18:26:46.942549728 +0000
+++ /tmp/new.19bu4d	2026-10-31 18:26:46.942549728 +0000
@@ -73,6 +73,25 @@
   fi
 }
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -80,19 +99,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -111,11 +129,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -129,9 +147,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/termius/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 0272f4cf -> 02d96b6b ===

--- /tmp/old.vFkut3	2026-10-31 18:26:46.975549780 +0000
+++ /tmp/new.D8hckc	2026-10-31 18:26:46.975549780 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/twobird/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 74f516da -> ac42231b ===

--- /tmp/old.01R6O8	2026-10-31 18:26:47.010549835 +0000
+++ /tmp/new.8fvdWP	2026-10-31 18:26:47.010549835 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/usage-app/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 4b90c863 -> 2c804413 ===

--- /tmp/old.Mvo7D3	2026-10-31 18:26:47.045549890 +0000
+++ /tmp/new.pgjDzk	2026-10-31 18:26:47.045549890 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/visual-paradigm/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 0ca7d6e3 -> c336c0cf ===

--- /tmp/old.pWSGHG	2026-10-31 18:26:47.079549944 +0000
+++ /tmp/new.rIHMrG	2026-10-31 18:26:47.079549944 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/webex/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ae33236a -> 141a750e ===

--- /tmp/old.NLy9fh	2026-10-31 18:26:47.111549994 +0000
+++ /tmp/new.6CmabV	2026-10-31 18:26:47.111549994 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 send_signal() {
   local signal="$1"
   local bundle_id="$2"
@@ -91,19 +110,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -122,11 +140,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -140,9 +158,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/wezterm/darwin.json

=== Install Script (no changes) ===
=== Uninstall // ef8ecccd -> 854418e2 ===

--- /tmp/old.K3Vugg	2026-10-31 18:26:47.146550049 +0000
+++ /tmp/new.ZfsnAf	2026-10-31 18:26:47.146550049 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

ee/maintained-apps/outputs/whispering/darwin.json

=== Install Script (no changes) ===
=== Uninstall // e0aaf940 -> b20cc11a ===

--- /tmp/old.nJtED5	2026-10-31 18:26:47.180550103 +0000
+++ /tmp/new.vRyhOO	2026-10-31 18:26:47.181550105 +0000
@@ -45,6 +45,25 @@
 }
 
 
+remove_system_path() {
+  local target="$1"
+  local parent name rel top expected
+  parent="$(dirname "$target")"
+  name="$(basename "$target")"
+  rel="${parent#/}"
+  top="/${rel%%/*}"
+  expected="$(cd -P "$top" 2>/dev/null && pwd -P)${parent#"$top"}"
+
+  (
+    cd -P "$parent" 2>/dev/null || exit 1
+    if [[ "$(pwd -P)" != "$expected" ]]; then
+      echo "Skipped $target: a folder in its path is a symlink."
+      exit 0
+    fi
+    rm -rf "./$name"
+  ) || echo "Failed to remove $target."
+}
+
 trash() {
   local logged_in_user="$1"
   local target_file="$2"
@@ -52,19 +71,18 @@
   local rand="$(jot -r 1 0 99999)"
   local trash=""
 
-  # At the login window there's no user Trash to move files into, so user
-  # paths are skipped and system paths are deleted outright. Removal is
-  # best-effort and never fails the uninstall.
-  if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
-    if [[ "$target_file" == ~* ]]; then
+  # Paths in the user's home are moved to their Trash as the user: the user
+  # controls those paths and ~/.Trash, so as root the move could be redirected
+  # into a root-only directory. Other paths are deleted as root by
+  # remove_system_path. At the login window there's no user Trash, so home
+  # paths are skipped. Removal is best-effort and never fails the uninstall.
+  if [[ "$target_file" == ~* ]]; then
+    if [[ -z "$logged_in_user" || "$logged_in_user" == "root" || "$logged_in_user" == "loginwindow" ]]; then
       echo "Not logged into a non-root GUI; skipping $target_file."
       return
     fi
-  else
     # replace ~ with /Users/$logged_in_user
-    if [[ "$target_file" == ~* ]]; then
-      target_file="/Users/$logged_in_user${target_file:1}"
-    fi
+    target_file="/Users/$logged_in_user${target_file:1}"
     trash="/Users/$logged_in_user/.Trash"
   fi
 
@@ -83,11 +101,11 @@
       file_name="$(basename "$file")"
       echo "removing $file."
       if [[ -z "$trash" ]]; then
-        rm -rf "$file" || echo "Failed to remove $file."
+        remove_system_path "$file"
       else
         # The per-match counter keeps matches that share a basename from
         # overwriting each other in the trash.
-        mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
+        sudo -u "$logged_in_user" mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}" || echo "Failed to move $file to the Trash."
       fi
     done < <(compgen -G "$target_file" 2>/dev/null)
     if [[ "$matched" == false ]]; then
@@ -101,9 +119,9 @@
   if [[ -e "$target_file" ]]; then
     echo "removing $target_file."
     if [[ -z "$trash" ]]; then
-      rm -rf "$target_file" || echo "Failed to remove $target_file."
+      remove_system_path "$target_file"
     else
-      mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
+      sudo -u "$logged_in_user" mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" || echo "Failed to move $target_file to the Trash."
     fi
   else
     echo "$target_file doesn't exist."

@allenhouchins
allenhouchins marked this pull request as ready for review November 1, 2026 01:49
Copilot AI balanced review requested due to automatic review settings November 1, 2026 01:49
@allenhouchins
allenhouchins merged commit 49dce45 into main Nov 1, 2026
51 of 54 checks passed
@allenhouchins
allenhouchins deleted the allenhouchins/fma-trash-as-user-1 branch November 1, 2026 01:49
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57087)

Part 2 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 91 macOS outputs, `aldente` through `captain`. Each
changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57088)

Part 3 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `capto` through `dia`. Each
changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57089)

Part 4 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `dialpad` through
`free-download-manager`. Each changes only its uninstall script and
`uninstall_script_ref`: no versions, SHAs, URLs, install scripts or
queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57090)

Part 5 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `front` through `julia-app`. Each
changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57091)

Part 6 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `jump-desktop` through
`microsoft-excel`. Each changes only its uninstall script and
`uninstall_script_ref`: no versions, SHAs, URLs, install scripts or
queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57092)

Part 7 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `microsoft-onenote` through
`openboard`. Each changes only its uninstall script and
`uninstall_script_ref`: no versions, SHAs, URLs, install scripts or
queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57093)

Part 8 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `opencloud` through `raycast`.
Each changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Root-executed deletion and user-owned Trash moves lack real fleetd-on-macOS validation, while sudo-stubbed tests do not establish TCC behavior or symlink-race safety.

No open issues 🎉

What changed in this PR

This PR begins an 11-part hardening update for Fleet-maintained macOS app uninstalls. It updates the shared generator, custom uninstall scripts, and selected outputs so user-home cleanup runs as the console user rather than root.

Changes:

  • Moves home-directory files to Trash with sudo -u, without falling back to root when a move fails.
  • Deletes system cleanup paths through a helper that rejects symlinked parent folders below the top level.
  • Expands regression tests for user moves, system deletion, symlink protection, failure handling, and custom-script consistency.
File Description
ee/​maintained-apps/​outputs/​wezterm/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​visual-paradigm/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​usage-app/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​twobird/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​termius/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​microsoft-azure-storage-explorer/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​cycling74-max/​darwin.json Embeds hardened home and shared-directory cleanup.
ee/​maintained-apps/​outputs/​comet/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​alcove/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​akiflow/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​affinity-publisher@1/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​affinity-photo/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​activitywatch/​darwin.json Updates embedded uninstall helpers and script reference.
ee/​maintained-apps/​outputs/​4k-youtube-to-mp3/​darwin.json Updates embedded uninstall helpers, including glob cleanup.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​wezterm-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​teleport-suite-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​microsoft-auto-update-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​microsoft_word_uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​krisp-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​kicad-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​homebrew-app-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​gpg-suite-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​forklift-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​dymo-connect-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​cycling74-max-uninstall.sh Hardens home and shared-directory cleanup.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​crashplan-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​cleanmymac-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​citrix-workspace-uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​camtasia_uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​1password_uninstall.sh Adopts hardened cleanup helpers.
ee/​maintained-apps/​ingesters/​homebrew/​scripts.go Generates user-scoped Trash moves and guarded system deletion.
ee/​maintained-apps/​ingesters/​homebrew/​scripts_test.go Adds cleanup behavior checks and helper consistency assertions.

Review effort: Balanced

allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57094)

Part 9 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `readest` through `soundsource`.
Each changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
#57095)

Part 10 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `sourcetree` through `twingate`.
Each changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
allenhouchins added a commit that referenced this pull request Nov 1, 2026
#57096)

Part 11 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. Part 1, #57086, has the
generator change, the tests, the custom scripts, and the frozen outputs;
see it for the full explanation. The 11 PRs touch disjoint files, so
they merge in any order without conflicts, and together they match
#56846's tree exactly. Merge #57086 first.

## Change

Generated macOS FMA uninstall scripts now move `~` paths from a cask's
`zap trash:` into the console user's Trash as that user, with `sudo -u`,
instead of as root. Other paths are deleted as root by a new
`remove_system_path` helper, which skips a path when a folder in it
below the top level is a symlink.

This PR regenerates 90 macOS outputs, `typeface` through `zwift`. Each
changes only its uninstall script and `uninstall_script_ref`: no
versions, SHAs, URLs, install scripts or queries.

The darwin validator runs on these apps, so expect some failures from
apps that had already drifted upstream. The Check Script Diff job will
fail here: its comment is over GitHub's 65,536-character limit at this
size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- The tests are in #57086. `go test ./cmd/maintained-apps/...
./ee/maintained-apps/...` passes on this branch alone.
- [ ] QA'd all new/changed functionality manually
  - Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)
@codecov

codecov Bot commented Nov 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.12%. Comparing base (d5bc53a) to head (65f5a36).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #57086      +/-   ##
==========================================
- Coverage   78.14%   78.12%   -0.02%     
==========================================
  Files        4428     4428              
  Lines      279336   279345       +9     
  Branches    16431    16431              
==========================================
- Hits       218278   218246      -32     
- Misses      60862    60903      +41     
  Partials      196      196              
Flag Coverage Δ
backend 79.33% <100.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch was previously deployed

1 inactive deployment
Docker Hub — 65f5a368 Deployed Oct 31, 2026 by allenhouchins via publish #111946
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants