Skip to main content

new-fma

Add a Fleet-maintained app (FMA) for macOS (Homebrew) and/or Windows (winget), or write/clean up an FMA's custom install or uninstall script. Use when asked to "add X as a macOS/Windows FMA", "add a Fleet-maintained app", to debug FMA validator failures, or to review comments in an FMA script. Emphasizes verifying installer metadata with real tools (msitools, plist) instead of guessing, proving where an installer actually lands when run as SYSTEM, and keeping shipped script comments admin-facing.

跳到安装

来源信息

仓库
fleetdm/fleet
最近来源活动
2026年9月14日 17:04
检测到的 SKILL.md 语言
英语
星标
6,865
分支
1,019

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
new-fma
description
Add a Fleet-maintained app (FMA) for macOS (Homebrew) and/or Windows (winget), or write/clean up an FMA's custom install or uninstall script. Use when asked to "add X as a macOS/Windows FMA", "add a Fleet-maintained app", to debug FMA validator failures, or to review comments in an FMA script. Emphasizes verifying installer metadata with real tools (msitools, plist) instead of guessing, proving where an installer actually lands when run as SYSTEM, and keeping shipped script comments admin-facing.
allowed-tools
Bash, Read, Write, Edit, Grep, Glob, WebFetch, WebSearch
model
opus
effort
high
You are adding a Fleet-maintained app (FMA) to this repo: $ARGUMENTS The authoritative contributor docs are [ee/maintained-apps/README.md](../../../ee/maintained-apps/README.md). This skill captures the workflow PLUS the hard-won gotchas the README doesn't cover. Read the README too, but follow the rules here. ## Golden rule: verify, don't guess The single biggest source of wasted cycles is trusting winget/Homebrew metadata for the fields that must match what osquery actually sees on a host. **The catalog metadata (winget `PackageName`/`Publisher`, cask names) frequently does NOT match the installed app's registry/bundle identity.** Always confirm identity fields against the real installer: - **Windows `unique_identifier`** must equal the registry **DisplayName** (osquery `programs.name`). - **Windows publisher** in the exists query must equal the registry **Publisher** (osquery `programs.publisher`). - **macOS `unique_identifier`** must equal the app's **CFBundleIdentifier**. - **Version** must reconcile with what osquery reports (`programs.version` on Windows; `bundle_short_version`/`bundle_version` on macOS). The same rule applies to **where the app installs**: verify it on a host running as SYSTEM rather than believing the manifest's `Scope`. See [Per-user installers and the SYSTEM context](#per-user-installers-and-the-system-context). Real examples from this codebase where the metadata lied: | App | winget/cask says | Registry/bundle actually is | |-----|------------------|------------------------------| | Amazon Corretto | PackageName "Amazon Corretto 25" | DisplayName `Amazon Corretto (x64)` (no version), Publisher `Amazon` | | Genesys Cloud | PackageName "GenesysCloud" | DisplayName `GenesysCloud` (you'd guess "Genesys Cloud") | | P4V | PackageName "P4 Apps", locale Publisher "Perforce Software, Inc." | DisplayName `P4 Apps`, Publisher `Perforce Software` | | GoToMeeting | MSI ProductName "GoToMeeting 10.19.19950" | registry DisplayName `GoToMeeting 10.19.0.19950` (bootstrapper!) | ## Prerequisites (one-time) ```bash brew install msitools # provides msiinfo for MSI inspection (macOS dev box) gh auth status # gh CLI for reading winget-pkgs manifests ``` ## Verification toolkit ### 1. Read the winget manifest (Windows) ```bash # List packages under a publisher, then versions (NOTE: dirs sort alphabetically, # so "21.0.11" sorts before "21.0.9" — use sort -V to find the true latest) gh api 'repos/microsoft/winget-pkgs/contents/manifests/<x>/<Publisher>' --jq '.[].name' gh api 'repos/microsoft/winget-pkgs/contents/manifests/<x>/<Pub>/<Pkg>' --jq '.[].name' | sort -V | tail # Installer manifest: InstallerType, Scope, arch, URL, SHA, ProductCode, UpgradeCode, InstallerSwitches gh api 'repos/microsoft/winget-pkgs/contents/manifests/<x>/<Pub>/<Pkg>/<ver>/<Pkg>.installer.yaml' --jq '.content' | base64 -d # Locale manifest: Publisher, PackageName, ShortDescription gh api 'repos/.../<Pkg>.locale.en-US.yaml' --jq '.content' | base64 -d | grep -E "Publisher:|PackageName:|ShortDescription:" ``` ### 2. Inspect the MSI (Windows) — the authoritative source for identity ```bash curl -sIL "<InstallerUrl>" | grep -i content-length # check size first cd /tmp && curl -sL -o app.msi "<InstallerUrl>" msiinfo export /tmp/app.msi Property | grep -iE "ProductName|ARPDISPLAY|Manufacturer|ProductVersion|UpgradeCode|ProductCode|ALLUSERS|ARPSYSTEMCOMPONENT" msiinfo export /tmp/app.msi Registry # custom ARP writes, if any rm -f /tmp/app.msi ``` Map MSI properties → FMA fields: - `ProductName` → registry DisplayName → `unique_identifier` (unless `ARPDISPLAYNAME` overrides it) - `Manufacturer` → registry Publisher → `program_publisher` (if it differs from the winget locale Publisher) - `ProductVersion` → expected `programs.version` (but see bootstrapper caveat below) - `UpgradeCode` → for upgrade-code uninstall scripts - `ALLUSERS=1` → installs per-machine regardless of switches - **`ARPSYSTEMCOMPONENT=1` → STOP: this is a bootstrapper (see Pitfall 2)** ### 3. Inspect the macOS app bundle (DMG) ```bash cd /tmp && curl -sL -o app.dmg "<cask url>" MP=$(mktemp -d); hdiutil attach -nobrowse -readonly -mountpoint "$MP" app.dmg >/dev/null APP=$(find "$MP" -maxdepth 1 -name "*.app" | head -1) /usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$APP/Contents/Info.plist" # → unique_identifier /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP/Contents/Info.plist" hdiutil detach "$MP" >/dev/null; rm -f app.dmg ``` (For pkg-format casks, the bundle id is harder to read offline — the cask `zap`/`uninstall` `pkgutil`/`launchctl`/`savedState` paths are strong hints, e.g. `<bundleid>.savedState`.) ### 4. Silent install/uninstall flags — use documented sources, never guess - The winget installer manifest's `InstallerSwitches` (`Silent`, `Custom`) is the first source. - **silentinstallhq.com** has per-app guides with the exact switches (e.g. GoToMeeting uses `/silent`, not `/S`). Use `WebFetch` on `https://silentinstallhq.com/<app>-silent-install-how-to-guide/`. - Cross-check the vendor's own docs. ## Workflow ### macOS (Homebrew cask) 1. Find the cask: `curl -s https://formulae.brew.sh/api/cask/<token>.json` 2. Inspect the DMG/pkg for the real `CFBundleIdentifier` (toolkit #3). 3. Create `ee/maintained-apps/inputs/homebrew/<token>.json` — minimal: `name`, `slug` (`<app>/darwin`), `unique_identifier` (bundle id), `token`, `installer_format` (`dmg`/`pkg`/`zip`), `default_categories`. Install/uninstall scripts auto-generate from the cask (artifacts + zap). 4. Generate, add description, check icon (below). ### Windows (winget) 1. Read the winget manifests (toolkit #1). Pick **machine** scope, **x64** (or the only arch available — some apps are x86-only). 2. **Inspect the MSI** (toolkit #2) to confirm DisplayName, Publisher, version, codes, and to detect bootstrappers. 3. Create `ee/maintained-apps/inputs/winget/<slug-name>.json`: - `name` (catalog display, can be friendly), `slug` (`<app>/windows`), `package_identifier`, `unique_identifier` (= verified DisplayName), `installer_arch`, `installer_type`, `installer_scope`, `default_categories`. - `program_publisher` if registry Publisher ≠ winget locale Publisher. - `fuzzy_match_name` / `exists_query` as needed (below). - `install_script_path` / `uninstall_script_path` for any non-MSI-machine installer. 4. Generate, add description, check icon. ### Installer type mapping (winget `InstallerType` → FMA `installer_type` + silent flags) | winget type | FMA type | install silent | uninstall | |-------------|----------|----------------|-----------| | `msi`, `wix` | `msi` | auto (`msiexec /i /quiet /norestart`) | auto upgrade-code (machine scope only) | | `nullsoft` (NSIS) | `exe` | `/S` | registry UninstallString + `/S` | | `inno` (Inno Setup) | `exe` | `/VERYSILENT /SUPPRESSMSGBOXES /NORESTART` | registry UninstallString + same | | `burn` (WiX bundle) | `exe` | `/quiet /norestart` | bundle UninstallString `/uninstall /quiet /norestart` | | `msix` | `msix` | n/a | n/a | The ingester only auto-generates scripts for **machine-scope MSI**. Everything else needs custom `install_script_path` + `uninstall_script_path`. MSI success codes to treat as success: `0`, `3010` (reboot required), `1641` (reboot initiated). ### Custom script comments: these ship to customers FMA install/uninstall scripts are not internal code. They're returned verbatim by `GET /fleet/software/fleet_maintained_apps/:id` and by the software title endpoint, and rendered in the "Install script" / "Uninstall script" editors of the Edit software modal ([AdvancedOptionsFields.tsx](../../../frontend/pages/SoftwarePage/components/forms/AdvancedOptionsFields/AdvancedOptionsFields.tsx)), where an admin reads them and can edit them. Every comment you leave is product copy — treat it like the app description, not like a commit message. Budget: the Fleet template header (`# Learn more about .exe install scripts:` + URL) if the script started from a template, then **at most ~4 lines** of app-specific comment. Of the 580 scripts in `inputs/*/scripts/`, only 51 open with a longer block than that — a big header is the exception you have to justify, not the norm. **Keep** a comment only if an admin who edits this script would break something without it, or would be surprised at install time: - Host-visible side effects: the app is force-quit, users are logged out, a reboot happens, existing config is preserved or deleted. - Scope and destructiveness decisions — e.g. [box-tools-uninstall.sh](../../../ee/maintained-apps/inputs/homebrew/scripts/box-tools-uninstall.sh): removal sweeps every local user's home, and only the Box Edit subdirectory goes because the parent is shared with Box Drive. - Constraints that must survive an edit: the required switch and why the obvious one is wrong (`/VERYSILENT` — this is Inno Setup, `/S` opens the GUI), removal ordering, "must run as the logged-in user." - Exit-code meanings (`1605` = not installed, `3010` = reboot required). **Cut** — this belongs in the PR description, not the shipped script: - Fleet's own tooling: "the validator's 10-minute timeout", "hangs in CI", "the ingester", "osquery's programs table". A customer has no validator. - Catalog archaeology: what winget/Homebrew metadata claimed vs. reality, `silentinstallhq.com` links, PR/issue numbers. - Debugging narrative: what you tried first and why it failed ("a plain `Start-Process -Wait` would block until killed"). - First person ("we", "our", "ourselves") — describe what the script does, in present tense and sentence case. - Restating the next line (`# Prints the exit code` above a `Write-Host`). If the fact matters at run time rather than at edit time, `Write-Host`/`echo` it instead of commenting it — script output lands in the host's software install details, which is where an admin debugging a failure actually looks. Before/after — [darktable_install.ps1](../../../ee/maintained-apps/inputs/winget/scripts/darktable_install.ps1)'s 20-line header carries three admin-relevant facts and 16 lines of internal history: ```powershell # Learn more about .exe install scripts: # http://fleetdm.com/learn-more-about/exe-install-scripts # # darktable uses an Inno Setup installer: it needs /VERYSILENT (the NSIS /S # switch winget's metadata implies does nothing) and installs machine-wide when # elevated. Its installer stays running after a silent install, so this script # waits for darktable to register in Programs and Features, then stops it. ``` Dropped: that winget mislabeled the installer type, that `PrivilegesRequiredOverridesAllowed=dialog` rules out `/ALLUSERS`, that the lingering process holds the installer file lock, what a plain `-Wait` did. All of it goes in the PR body, where reviewers need it and customers don't see it. Body comments follow the same rule — keep the one above a non-obvious registry match or a load-bearing helper, drop the rest. **When you touch an existing script for any reason, prune its comments in the same edit.** ### Generate, validate, finalize ```bash go run cmd/maintained-apps/main.go --slug="<app>/<platform>" --debug ``` - Output lands in `ee/maintained-apps/outputs/<slug>.json`; an entry is appended to `outputs/apps.json` with an **empty description** — fill it in (sentence case, "`<App>` is a(n)..."). The generator does NOT update `unique_identifier` on an existing apps.json entry — edit it manually if you change it. - Verify the generated SHA matches the manifest, and the exists/patched queries look right: `grep -E 'exists|patched|sha256' outputs/<slug>.json`. - `python3 -m json.tool ee/maintained-apps/outputs/apps.json >/dev/null` to confirm valid JSON. - **Icon**: check `frontend/pages/SoftwarePage/components/icons/index.ts` for a key matching the lowercased catalog `name`. If missing, generate via [tools/software/icons](../../../tools/software/icons) before merge. Icons key off the lowercased `name`, so platforms sharing a `name` share an icon. After generating, confirm the new `SOFTWARE_NAME_TO_ICON_MAP` key really is the lowercased `name` — when `name` and slug differ it is easy to end up keyed off the slug, and the lookup then misses. If an icon component for that name already exists, revert any regenerated `.tsx`/`.png` and reuse it. - The validator is a Windows/macOS host (often **ephemeral** — you can't query it after the run). To cross-compile the Windows validator after editing it: `GOOS=windows go build ./cmd/maintained-apps/validate/`. ## Per-user installers and the SYSTEM context Fleet runs install and uninstall scripts as **SYSTEM**. Most Windows FMA breakage traces back to this, and it does not reproduce in CI (see [Validating for real](#validating-for-real)), so it ships silently. **Always prefer machine-wide.** Try the installer's all-users switch first (`ALLUSERS=1`/`2`, `/ALLUSERS`, `G2MINSTALLFORALLUSERS=1`). Machine-wide installs land in `Program Files` with an HKLM registration and everything downstream just works. **Verify the switch was honoured — do not trust that it was.** Signal accepts `/S /allusers` and silently ignores it, still installing per-user. Install it on a real host as SYSTEM and look at where the payload and the registration actually went. Equally, do not trust the input's `installer_scope`: it comes from the winget manifest and is sometimes a default rather than a fact. `bluej`, `julia-app` and `readest` are all declared `installer_scope: user` yet install machine-wide to `Program Files` under HKLM, because their custom scripts already handle SYSTEM deliberately (`bluej` passes `ALLUSERS=2`). Scope alone is not evidence of a bug. ### When the app has no machine-wide mode Electron/NSIS/Squirrel apps and some Inno apps only ever install into the running user's profile. Run as SYSTEM they land in `C:\Windows\system32\config\systemprofile\AppData\Local\...`, where **no signed-in user can launch them** — the install "succeeds" and is useless. Symptoms vary and none of them says "wrong scope": Notion's installer crashes outright (`0xC0000005`, installing nothing), `amazon-chime` hangs until the timeout, `granola` returns 0 and installs into SYSTEM's profile. The fix is to hand the installer to the signed-in user via a scheduled task. `figma`, `slack`, `brave`, `arc`, `postman`, `notion` and others follow this shape: ```powershell $owner = Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' -ErrorAction SilentlyContinue | Invoke-CimMethod -MethodName GetOwner -ErrorAction SilentlyContinue | Where-Object { $_.User } | Select-Object -First 1
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看