Repository navigation
macOS FMA uninstalls: move trashed files as the user, not root - #56846
allenhouchins wants to merge 9 commits into
Conversation
The trash() helper in generated macOS FMA uninstall scripts ran as root and moved files into the console user's ~/.Trash. Both the files and ~/.Trash 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`. If the user's move fails, it's logged and root doesn't take over. - Other paths (e.g. /Library/LaunchDaemons, /Users/Shared) are deleted as root with rm -rf instead of moved into the user's Trash, which is what the helper already does at the login window. The 15 custom uninstall scripts with a copy of trash() carry the new one, and the 971 macOS outputs that embed it are updated. Only each output's uninstall script and its ref change.
# Conflicts: # ee/maintained-apps/outputs/aldente/darwin.json # ee/maintained-apps/outputs/biscuit/darwin.json # ee/maintained-apps/outputs/clickup/darwin.json # ee/maintained-apps/outputs/cog-app/darwin.json # ee/maintained-apps/outputs/copilot-money/darwin.json # ee/maintained-apps/outputs/cryptomator/darwin.json # ee/maintained-apps/outputs/dbgate/darwin.json # ee/maintained-apps/outputs/dcv-viewer/darwin.json # ee/maintained-apps/outputs/dropbox/darwin.json # ee/maintained-apps/outputs/firefox@nightly/darwin.json # ee/maintained-apps/outputs/houdahspot/darwin.json # ee/maintained-apps/outputs/lens/darwin.json # ee/maintained-apps/outputs/macpacker/darwin.json # ee/maintained-apps/outputs/nordvpn/darwin.json # ee/maintained-apps/outputs/ollama/darwin.json # ee/maintained-apps/outputs/opencode-desktop/darwin.json # ee/maintained-apps/outputs/prisma-browser/darwin.json # ee/maintained-apps/outputs/proton-pass/darwin.json # ee/maintained-apps/outputs/qlab/darwin.json # ee/maintained-apps/outputs/recut/darwin.json # ee/maintained-apps/outputs/rive/darwin.json # ee/maintained-apps/outputs/rocket-typist/darwin.json # ee/maintained-apps/outputs/sequel-ace/darwin.json # ee/maintained-apps/outputs/session/darwin.json # ee/maintained-apps/outputs/shapr3d/darwin.json # ee/maintained-apps/outputs/sourcetree/darwin.json
# Conflicts: # ee/maintained-apps/outputs/airtame/darwin.json # ee/maintained-apps/outputs/antigravity/darwin.json # ee/maintained-apps/outputs/apparency/darwin.json # ee/maintained-apps/outputs/arc/darwin.json # ee/maintained-apps/outputs/beekeeper-studio/darwin.json # ee/maintained-apps/outputs/betterdisplay/darwin.json # ee/maintained-apps/outputs/bitwarden/darwin.json # ee/maintained-apps/outputs/boltai/darwin.json # ee/maintained-apps/outputs/bricklink-studio/darwin.json # ee/maintained-apps/outputs/chatgpt/darwin.json # ee/maintained-apps/outputs/claude/darwin.json # ee/maintained-apps/outputs/cleanmymac/darwin.json # ee/maintained-apps/outputs/clop/darwin.json # ee/maintained-apps/outputs/crashplan/darwin.json # ee/maintained-apps/outputs/firefox@nightly/darwin.json # ee/maintained-apps/outputs/framer/darwin.json # ee/maintained-apps/outputs/granola/darwin.json # ee/maintained-apps/outputs/home-assistant/darwin.json # ee/maintained-apps/outputs/insomnia/darwin.json # ee/maintained-apps/outputs/joplin/darwin.json # ee/maintained-apps/outputs/kiro-cli/darwin.json # ee/maintained-apps/outputs/lm-studio/darwin.json # ee/maintained-apps/outputs/macwhisper/darwin.json # ee/maintained-apps/outputs/morgen/darwin.json # ee/maintained-apps/outputs/mullvad-browser/darwin.json # ee/maintained-apps/outputs/nextcloud/darwin.json # ee/maintained-apps/outputs/nordvpn/darwin.json # ee/maintained-apps/outputs/okta-advanced-server-access/darwin.json # ee/maintained-apps/outputs/opencode-desktop/darwin.json # ee/maintained-apps/outputs/opera/darwin.json # ee/maintained-apps/outputs/parallels/darwin.json # ee/maintained-apps/outputs/pgadmin4/darwin.json # ee/maintained-apps/outputs/picview/darwin.json # ee/maintained-apps/outputs/rectangle-pro/darwin.json # ee/maintained-apps/outputs/recut/darwin.json # ee/maintained-apps/outputs/rive/darwin.json # ee/maintained-apps/outputs/splice/darwin.json # ee/maintained-apps/outputs/superhuman-docs/darwin.json # ee/maintained-apps/outputs/superhuman/darwin.json # ee/maintained-apps/outputs/teleport-connect/darwin.json # ee/maintained-apps/outputs/teleport-suite/darwin.json # ee/maintained-apps/outputs/tor-browser/darwin.json # ee/maintained-apps/outputs/tradingview/darwin.json # ee/maintained-apps/outputs/trezor-suite/darwin.json # ee/maintained-apps/outputs/ua-connect/darwin.json # ee/maintained-apps/outputs/vorssaint/darwin.json # ee/maintained-apps/outputs/willow-voice/darwin.json # ee/maintained-apps/outputs/workflowy/darwin.json # ee/maintained-apps/outputs/zen/darwin.json # ee/maintained-apps/outputs/zotero/darwin.json
**Related issue:** Follow-up to #55865, which fixes the same bug for cask `rmdir`. Generated macOS uninstall scripts emit `sudo rm -rf '~/...'` for cask `delete` entries. The tilde is single-quoted, so it never expands: the command targets a relative path named `~` and the file is never removed. Uninstall scripts have no `set -e`, so this fails silently. On main, it affects 7 apps. For example, the Elgato Stream Deck, Wave Link, Loopback, and Splashtop Streamer LaunchAgent plists are left behind. - New `delete_user_path` helper in the homebrew ingester. It resolves `~` to `/Users/$LOGGED_IN_USER`, the way `trash` does, and skips the path when there's no GUI user: no console user, root, or the login window, the same check `trash` makes. - **`rm` runs as the console user** (`sudo -u "$logged_in_user" rm -rf`), not root, matching #56846's `trash()`. The user controls every folder in a `~` path, so as root a symlink they planted (for example `~/Library/LaunchAgents` → `/Library/LaunchDaemons`) would redirect the delete outside their home. If the user's delete fails, the script logs `Failed to remove <path>.` and root doesn't retry. Files in the home folder that the user can't delete (owned by root) are left behind. - The generator also keeps brew's guards for `~` paths: it skips the home folder itself (`~`, `~/`) and any path with `.` or `..` segments. The helper also refuses anything that isn't `~/` followed by a path. Brew additionally refuses a list of standard home folders, such as `~/Library` and `~/Documents`. That list isn't copied here, and no current cask deletes one of them. - Absolute `delete` paths are unchanged (`sudo rm -rf '<path>'`), so no other output changes. - Regenerated the uninstall scripts of the 7 affected outputs: elgato-stream-deck, elgato-wave-link, loopback, splashtop-streamer, steam, topaz-gigapixel-ai, and topaz-photo-ai. Only `uninstall_script_ref` and its `refs` entry change: no version, URL, SHA, or install script changes. As of 2026-10-29, each uninstall script is byte-identical to what the ingester generates from the current cask, which is at main's version. None of the 7 apps is frozen or uses a custom script. Not in this PR: absolute `delete` paths with globs, such as `'/Library/Logs/DiagnosticReports/zoom.us*'`, are also single-quoted, so they never expand either. That affects 13 apps and needs its own change, since it means expanding globs for a root `rm -rf`. Shared code changed: `ee/maintained-apps/ingesters/homebrew/scripts.go`. The validator will run on the 7 changed outputs. #56846 also changes these 7 outputs, so whichever merges second needs them regenerated. # 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. (Cask paths are still emitted through `shellSingleQuote`. The helper only uses them as `"..."` arguments, and `~` paths that could leave the home folder are never emitted.) ## Testing - [x] Added/updated automated tests (`TestUninstallScriptDeleteResolvesHome` checks: - the generated lines, including an apostrophe in the path - that absolute paths still emit `sudo rm -rf '<path>'` - that `~`, `~/`, `~//`, `..`/`.` segments, and `~user` paths are never emitted - running the generated script under bash: `~` paths are deleted through `sudo -u <console user>` under `/Users/<console user>`, and skipped with no console user, as root, or at the login window - that a failed delete as the user is reported and not retried as root - that the helper on its own refuses anything outside `~/`) - [ ] QA'd all new/changed functionality manually (All 7 regenerated scripts pass `bash -n`. BSD `rm -rf` on an absolute path works when the working directory isn't readable, as root's working directory isn't under `sudo -u`. Not yet run end-to-end on a host.) `go test ./ee/maintained-apps/... ./cmd/maintained-apps/...` passes. golangci-lint (CI's pinned v2.14.0) reports 0 issues on the ingester, both full and with `--new-from-merge-base=origin/main`. ## AI **AI:** Claude Code (claude-opus-5-5) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Uninstalling affected apps now correctly removes their user-specific files from the logged-in user’s home directory. * Cleanup skips these files when no eligible logged-in user is available, and reports failures instead of retrying deletion with elevated privileges. * Other uninstall cleanup steps remain unchanged. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…issue-157-2eae5b # Conflicts: # ee/maintained-apps/outputs/elgato-stream-deck/darwin.json # ee/maintained-apps/outputs/elgato-wave-link/darwin.json # ee/maintained-apps/outputs/loopback/darwin.json # ee/maintained-apps/outputs/splashtop-streamer/darwin.json # ee/maintained-apps/outputs/steam/darwin.json # ee/maintained-apps/outputs/topaz-gigapixel-ai/darwin.json # ee/maintained-apps/outputs/topaz-photo-ai/darwin.json
trash() deletes paths outside the user's home as root. rm -rf doesn't follow a symlink at the end of a path, but it does follow one in a parent folder, so a user who pre-creates a folder in /Users/Shared as a symlink (e.g. /Users/Shared/TechSmith for Camtasia) could redirect the delete to a root-only location. The new remove_system_path helper cd -P's into the real parent folder, checks it's the folder the path names (only top-level symlinks such as /tmp and /etc are trusted), and deletes ./<name> from there, so a swap after the check can't redirect it. Applied to the generator, the 15 custom uninstall scripts, and all 971 macOS outputs that embed trash().
…issue-157-2eae5b All 971 macOS outputs conflicted on the uninstall script. Each was rebuilt from main's version with the new trash() and remove_system_path(), and Dia, Superset, T3 Code and UPDF (added on main) got the same change. Lingon X is removed, as on main.
…issue-157-2eae5b The 17 macOS outputs #56585 regenerated conflicted on the uninstall script. Each was rebuilt from main's version with the new trash() and remove_system_path().
…57081) **Related issue:** Found in #56846 (darwin validator, [run 41213361619](https://github.com/fleetdm/fleet/actions/runs/41213361619/job/124610472624), shard 5/33) ## Problem The macOS Citrix Workspace uninstall sometimes leaves `/Library/Application Support/Citrix Workspace Updater/Citrix Workspace Updater.app` (`com.citrix.ReceiverUpdater`, 26.09.0 / 700026.09.0.17) behind. The darwin validator then reports "App still present after uninstall", because it matches any app named like "Citrix Workspace". It also shows up on real Macs, for example in the osquery-perf software library. ## Where the updater comes from The pkg doesn't install that folder. I read the pkg's TOC, `Distribution`, `PackageInfo`, `Bom` and `Scripts` with HTTP range requests (1.5 MB of the 387 MB pkg): - It has one component, `com.citrix.ICAClient`. Its payload goes to the staging tree `/Library/Citrix Workspace/CitrixWorkspaceInstaller`, and the postinstall moves the app into `/Applications`. The updater exists in the payload only inside the app bundle, at `Contents/Frameworks/CitrixWorkspaceApps.framework/Versions/26.09.0.17/Helpers/CitrixWorkspaceApps/Citrix Workspace Updater.app`. - No payload, receipt or pkg script writes `/Library/Application Support/Citrix Workspace Updater`. So no `pkgutil` entry, in the cask or anywhere else, can cover it. - For silent installs (`installer -pkg`, which is how Fleet installs it), the postinstall's `configureMsTeamsVDIPlugin` writes `CWAInstallAUCmd = true` to the user's `com.citrix.receiver.nomas` defaults. Its comment says ReceiverHelper reads that marker, behind a LaunchDarkly kill switch, and installs the Microsoft Teams VDI plugin through the Citrix Workspace updater service. The failing run's harden-runner events show that sequence, right after the install: | Time (UTC) | Process | |---|---| | 16:17:41 | Install done, `/Applications/Citrix Workspace.app` detected | | 16:17:45 | `Citrix Workspace` running | | 16:17:50 | `/Library/Application Support/Citrix Workspace Updater/Citrix Workspace Updater.app/.../Citrix Workspace Updater` (pid 30319) contacts `download.microsoft.com` | | 16:17:52 | A second `/usr/sbin/installer` (pid 30662) runs | | 16:17:54 | Uninstall script starts; another updater process (pid 30683) is running from the same folder | | 16:18:12 | The validator finds the updater there | So the updater is copied out of the app bundle at runtime and run as root, to install an add-on. Whether it happens before the uninstall depends on the feature flag, the network and timing. That's why the same app passed other full-catalog runs the same day. ## Change - New custom uninstall script, `inputs/homebrew/scripts/citrix-workspace-uninstall.sh`. It's the current generated script, byte for byte, plus one block after `/Applications/Citrix Workspace.app` and `/Library/Citrix Workspace` are removed: - It stops any process running from `/Library/Application Support/Citrix Workspace Updater/`, with TERM, then KILL after 5 seconds. The match is anchored to the start of the command line, so only processes running from that folder match. - Then it deletes the folder. - At that point, the launchd jobs (including `com.citrix.ctxworkspaceupdater` and `com.citrix.ReceiverUpdaterHelper`) are booted out and their plists deleted, ReceiverHelper has been quit, and the app bundle the updater is copied from is gone. Nothing is left that could copy it back. - I regenerated the output with the ingester. Only `uninstall_script_ref` and its script changed: same version (26.09.0), URL, SHA and install script. Not in this PR: the Teams VDI plugin the updater may have installed by then. I couldn't confirm its pkg ID or install path without capturing it from a real run. If #56846 merges first, this script's copy of `trash()` needs the same update, and vice versa. # 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. - The new block uses only a fixed path. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - It waits at most 5 seconds before KILL. ## Testing - [x] QA'd all new/changed functionality manually - I ran the new block on macOS against fake processes running from a scratch copy of the folder. One exited on TERM, one ignored TERM and was killed after 5 seconds, and one that only named the path as an argument was left running. The folder was removed. - `go test ./ee/maintained-apps/... ./cmd/maintained-apps/...` passes. - Not tested: a full install and uninstall on a Mac where the updater actually ran. This PR's darwin validation runs citrix-workspace, but the updater only runs sometimes, so a pass there doesn't prove the fix. ## AI **AI:** Claude Code (claude-opus-5-5)
# Conflicts: # ee/maintained-apps/outputs/citrix-workspace/darwin.json
There was a problem hiding this comment.
🔵 Needs a closer look
Root cleanup and privilege/TCC changes lack end-to-end fleetd validation on a real Mac, leaving runtime behavior and symlink-race safety unverified by sudo-stubbed tests.
No open issues 🎉
What changed in this PR
This PR hardens macOS Fleet-maintained app uninstall cleanup by moving home-directory files as the console user rather than root. It extends the existing login-window handling while keeping cleanup best-effort.
Changes:
- Moves home-directory paths to Trash with
sudo -u, without retrying as root on failure. - Deletes system paths through a helper that checks for symlinked parent directories.
- Updates custom scripts and generated manifests, with regression tests for user moves, system deletion, symlink protection, and failure handling.
| File | Description |
|---|---|
ee/maintained-apps/outputs/yippy/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/wordservice/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/wave/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/timescribe/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/teacode/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/slack/darwin.json |
Applies user Trash moves and guarded system cleanup. |
ee/maintained-apps/outputs/selfcontrol/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/mos/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/middleclick/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/marvel/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/helium/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/flycut/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/firealpaca/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/cycling74-max/darwin.json |
Embeds hardened custom cleanup and updates its reference. |
ee/maintained-apps/outputs/batfi/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/outputs/alloy/darwin.json |
Refreshes uninstall script and reference. |
ee/maintained-apps/inputs/homebrew/scripts/wezterm-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/teleport-suite-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/microsoft-auto-update-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/microsoft_word_uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/krisp-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/kicad-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/homebrew-app-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/gpg-suite-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/forklift-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/dymo-connect-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/cycling74-max-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/crashplan-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/cleanmymac-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/citrix-workspace-uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/camtasia_uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/inputs/homebrew/scripts/1password_uninstall.sh |
Synchronizes hardened cleanup helpers. |
ee/maintained-apps/ingesters/homebrew/scripts.go |
Generates user-level Trash moves and guarded system deletion. |
ee/maintained-apps/ingesters/homebrew/scripts_test.go |
Expands cleanup regression tests and custom-helper consistency checks. |
Review effort: Balanced
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #56846 +/- ##
==========================================
- Coverage 78.14% 78.14% -0.01%
==========================================
Files 4428 4428
Lines 279336 279345 +9
Branches 16431 16431
==========================================
+ Hits 218278 218282 +4
- Misses 60862 60867 +5
Partials 196 196
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…#57086) 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: - 2/11: #57087, `aldente` through `captain` - 3/11: #57088, `capto` through `dia` - 4/11: #57089, `dialpad` through `free-download-manager` - 5/11: #57090, `front` through `julia-app` - 6/11: #57091, `jump-desktop` through `microsoft-excel` - 7/11: #57092, `microsoft-onenote` through `openboard` - 8/11: #57093, `opencloud` through `raycast` - 9/11: #57094, `readest` through `soundsource` - 10/11: #57095, `sourcetree` through `twingate` - 11/11: #57096, `typeface` through `zwift` ## 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 (#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 #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 - [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 - [ ] 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)
…#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)
…#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)
…#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)
…#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)
…#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)
…#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)
…#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)
…#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)
#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)
#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)
Summary
Hardening for the
trash()helper in generated macOS FMA uninstall scripts. Fleet runs these scripts as root.trash()moved the cask'szappaths into the console user's~/.Trashwith a rootmv -f. Those paths and~/.Trashboth belong to that user, so root shouldn't be the one writing there.~/...) are moved to the Trash as the user, withsudo -u "$logged_in_user" mv -f. If the user's move fails, the script logsFailed to move <path> to the Trash.and root doesn't take over.remove_system_pathhelper instead of being moved into the user's Trash. Across the outputs that's 109 entries in 65 apps, 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).rm -rfdoesn'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/TechSmithas a symlink first could aim Camtasia's/Users/Shared/TechSmith/Camtasiadelete at a root-only folder.remove_system_pathusescd -Pto 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/tmpand/etcare followed, because users can't write to/. A path that fails the check is left in place and logged asSkipped <path>: a folder in its path is a symlink.~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
~/.Trashby name (only listing it is blocked), so the move as the user doesn't depend on fleetd's TCC grants.Outputs
trash()have the new one, plusremove_system_path().TestCustomUninstallScriptsUseCurrentTrashkeeps both identical to the generator.trash(), frozen apps included. In each, only the uninstall script and its ref change. Version, URL, sha256, install script and queries are byte-identical tomain. This includes the 7 outputs FMA: resolve ~ in cask delete paths #56572 changed, rebuilt from main's version. Dia, Superset, T3 Code and UPDF, added on main since, carry the new helper too, and Lingon X is gone, as on main.cmd/maintained-apps, and each uninstall script matched this PR's byte for byte. They include 3 apps FMA: resolve ~ in cask delete paths #56572 changed, 8 apps with/Users/Sharedzappaths (such as Ableton Live Suite, FileMaker Pro and iMazing), Docker Desktop, Little Snitch, Zoom, Santa, Google Chrome, Slack and Firefox. An earlier pass over 21 apps, including 1Password, Camtasia and apps FMA: resolve ~ in cask rmdir and run it after trash #55865 changed, also matched beforeremove_system_pathwas added.Checklist for submitter
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
TestUninstallScriptTrash(renamed fromTestUninstallScriptTrashWithoutGUIUser) runs generatedtrashcalls in bash with the user's home under a temp dir. A baremvis stubbed to fail, so the test fails if root ever moves a file. It checks that:~/.Trashthroughsudo -uThe test fails against the old helper, and also fails with the symlink check in
remove_system_pathturned off.Also checked:
ee/maintained-appsandcmd/maintained-appstests passbash -ntrash()beforeremove_system_pathwas added, but hasn't been rerun sinceremove_system_pathdeletes a normal path, skips one under a symlinked folder, removes a symlink at the end of the path without touching its target, and follows/tmpNot yet run as root by fleetd on a real Mac.
AI
AI: Claude Code (claude-opus-5-5)