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

macOS FMA uninstalls: move trashed files as the user, not root - #56846

Closed
allenhouchins wants to merge 9 commits into
mainfrom
allenhouchins/github-issue-157-2eae5b
Closed

allenhouchins wants to merge 9 commits into
mainfrom
allenhouchins/github-issue-157-2eae5b

Conversation

@allenhouchins

@allenhouchins allenhouchins commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

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. 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).
  • 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

  • Custom scripts: the 16 custom uninstall scripts that carry a copy of trash() have the new one, plus remove_system_path(). TestCustomUninstallScriptsUseCurrentTrash keeps both identical to the generator.
  • 974 of 984 macOS outputs embed trash(), frozen apps included. In each, only the uninstall script and its ref change. Version, URL, sha256, install script and queries are byte-identical to main. 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.
  • Cross-check: with the final generator, I regenerated 18 apps with 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/Shared zap paths (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 before remove_system_path was added.
  • Because every macOS output changes, darwin validation runs the whole catalog. Expect some failures from apps that had already drifted upstream. Other open PRs that change these outputs will need them rebuilt after whichever merges first.

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:

  • the ee/maintained-apps and cmd/maintained-apps tests pass
  • golangci-lint v2.14.0 reports 0 issues
  • every macOS uninstall script and custom script passes bash -n
  • shellcheck showed no new findings in trash() before remove_system_path was added, but hasn't been rerun since
  • 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)

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
allenhouchins added a commit that referenced this pull request Oct 30, 2026
**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().
allenhouchins added a commit that referenced this pull request Oct 31, 2026
…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
@allenhouchins
allenhouchins marked this pull request as ready for review October 31, 2026 18:19
Copilot AI balanced review requested due to automatic review settings October 31, 2026 18:19

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 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

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.14%. Comparing base (d5bc53a) to head (37bb319).

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              
Flag Coverage Δ
backend 79.34% <100.00%> (-0.01%) ⬇️

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.

allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#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)
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)
@allenhouchins

Copy link
Copy Markdown
Member Author

Split into 11 PRs under 100 files each so they get auto-approved: #57086 (generator, tests, custom scripts and frozen outputs; merge first), then #57087–#57096 (the rest of the generated outputs, alphabetical). Together they match this PR's tree exactly.

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)

This branch was previously deployed

1 inactive deployment
Docker Hub — 37bb319e Deployed Oct 31, 2026 by allenhouchins via publish #111945
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.

2 participants