# Fleet-maintained apps (FMA) > **Using Claude Code?** The `new-fma` skill ([.claude/skills/new-fma/SKILL.md](../../.claude/skills/new-fma/SKILL.md)) automates this workflow and bakes in the gotchas this README doesn't cover — verifying the installed app's identity with `msiinfo`/`PlistBuddy` instead of trusting winget/cask metadata, handling bootstrapper installers, parsing unquoted `UninstallString`s, version-matching quirks, and more. Just ask it to "add X as a macOS/Windows FMA." ## Adding a new app (macOS) 1. Find the app's metadata in its [Homebrew formulae](https://formulae.brew.sh/) 2. Create a new manifest file called `$YOUR_APP_NAME.json` in the `inputs/homebrew/` directory. For example, if you wanted to add Box Drive, create the file `inputs/homebrew/box-drive.json`. 3. Fill out the file according to the [input schema below](#macos-input-file-schema). For our example Box Drive app, it would look like this: ```json { "name": "Box Drive", "slug": "box-drive/darwin", "unique_identifier": "com.box.desktop", "token": "box-drive", "installer_format": "pkg", "default_categories": ["Productivity"] } ``` 4. Run the following command from the root of the Fleet repo to generate the app's output data: ```bash go run cmd/maintained-apps/main.go --slug="" --debug ``` 5. The contributor is responsible for adding the icon to Fleet (e.g. the TypeScript and website PNG components of [#29175](https://github.com/fleetdm/fleet/pull/29175/files)). These are generated using the [generate-icons](https://github.com/fleetdm/fleet/tree/main/tools/software/icons) script. **The script automatically adds the import statement and map entry to `frontend/pages/SoftwarePage/components/icons/index.ts`**, so you don't need to manually update the index file. 6. Add a description for the app in `outputs/apps.json` file. You can use descriptions from [Homebrew formulae](https://formulae.brew.sh/). For consistency and presentation on the website, the description should follow sentence casing and the following format: `` is a(n) (copy description from Homebrew)., making sure to end with a `.`. 7. Open a PR to the `fleet` repository with the above changes. The [#g-software Engineering Manager (EM)](https://fleetdm.com/handbook/company/product-groups#software-group) is automatically added reviewer. Also, @ mention the [Fleet-maintained apps DRI](https://fleetdm.com/handbook/company/communications#:~:text=Fleet%2Dmaintained%20apps). 8. If the app passes automated tests, it is approved and merged. The EM reviews the PR within 3 business days. The app should appear shortly in the Fleet-maintained apps section when adding new software to Fleet. The app icon will not appear in Fleet until the following release. ### macOS input file schema | Name | Type | Description | |--------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | string | **Required.** User-facing name of the application. | | `unique_identifier` | string | **Required.** Platform-specific unique identifier (e.g., bundle identifier on macOS). | | `token` | string | **Required.** Homebrew's unique identifier. It's the `token` field of the Homebrew API response. | | `installer_format` | string | **Required.** File format of the installer (`zip`, `dmg`, `pkg`). Determine via the file extension in the Homebrew API `url` field or by downloading the installer if the extension isn’t present. | | `slug` | string | **Required.** Identifies the app/platform combination (e.g., `box-drive/darwin`). Used to name manifest files and reference the app in [Fleet's best practice GitOps](https://fleetdm.com/docs/configuration/yaml-files#fleet-maintained-apps). Format: `/`, where app name is filesystem-friendly and platform is `darwin`. | | `default_categories` | string | **Required.** Default categories for self-service if none are specified. Valid values: `Browsers`, `Communication`, `Developer Tools`, `Productivity`. | | `pre_uninstall_scripts` | string | Command lines run **before** the generated uninstall script (e.g., for [Box](inputs/homebrew/box-drive.json)). | | `post_uninstall_scripts` | string | Command lines run **after** the generated uninstall script (e.g., for [Box](inputs/homebrew/box-drive.json)). | | `install_script_path` | string | Filepath to a custom install script (`.sh`). Overrides the generated install script. Script must be placed in `inputs/homebrew/scripts/`. | | `uninstall_script_path` | string | Filepath to a custom uninstall script (`.sh`). Overrides the generated uninstall script. Cannot be used together with `pre_uninstall_scripts` or `post_uninstall_scripts`. Script must be placed in `inputs/homebrew/scripts/`. | | `cask_path` | string | Path (relative to the repo root) to a local file containing the cask JSON in the same schema as `https://formulae.brew.sh/api/cask/.json`. Used to commit cask metadata for third-party taps directly into this repo under [`inputs/homebrew/custom-tap/`](inputs/homebrew/custom-tap/). See [Ingesting apps from a custom tap](#ingesting-apps-from-a-custom-tap) below. | ### Ingesting apps from a custom tap Apps that live in a third-party Homebrew tap (not `Homebrew/homebrew-cask`) are not proxied by `https://formulae.brew.sh/api/`. To ingest them, commit both the `.rb` source and the generated `.json` into [`inputs/homebrew/custom-tap/`](inputs/homebrew/custom-tap/), laid out like a Homebrew tap: ``` custom-tap/ ├── Casks/.rb # Cask DSL source ├── api/.json # Generated with regenerate.sh └── regenerate.sh # Rebuild api/*.json from Casks/*.rb ``` 1. Write the cask DSL in `inputs/homebrew/custom-tap/Casks/.rb`. 2. Run `./regenerate.sh` from inside `custom-tap/` to produce `api/.json`. Requires macOS with Homebrew and `jq`. 3. In the app's input manifest (`inputs/homebrew/.json`), set `cask_path` to `ee/maintained-apps/inputs/homebrew/custom-tap/api/.json`. See `inputs/homebrew/fleet-desktop.json` for an example. See [`inputs/homebrew/custom-tap/README.md`](inputs/homebrew/custom-tap/README.md) for the full contributor flow. Apps without `cask_path` continue to be fetched from `formulae.brew.sh`. ## Adding a new app (Windows) 1. Find the Winget `PackageIdentifier` in the relevant [winget-pkgs repo manifest](https://github.com/microsoft/winget-pkgs/tree/master/manifests). 2. Get the unique identifier that Fleet will use for matching the software with software inventory: - On a test Windows host, install the app manually, then run the following PowerShell script that correlates to the defined `installer_scope`: - Machine scope: `Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -like '**'} | Select-Object DisplayName, DisplayVersion, Publisher` - User scope: `Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -like '**'} | Select-Object DisplayName, DisplayVersion, Publisher` If the `unique_identifier` doesn't match the `DisplayName`, then Fleet will incorrectly create two software titles when the Fleet-maintained app is added and later installed. One title for the Fleet-maintained app and a separate title for the inventoried software. 3. Fill out the file according to the [input schema](#windows-input-file-schema). For example, Box Drive looks like this: ```json { "name": "Box Drive", "slug": "box-drive/windows", "package_identifier": "Box.Box", "unique_identifier": "Box", "installer_arch": "x64", "installer_type": "msi", "installer_scope": "machine", "default_categories": ["Productivity"] } ``` 4. Run `go run cmd/maintained-apps/main.go --slug="/windows" --debug` from the root of the Fleet repo to generate the app's output data, replacing `` with your app's name, for example: ```bash go run cmd/maintained-apps/main.go --slug="box-drive/windows" --debug ``` 5. The contributor is responsible for adding the icon to Fleet (e.g. the TypeScript and website PNG components of [#29175](https://github.com/fleetdm/fleet/pull/29175/files)). These are generated using the [generate-icons](https://github.com/fleetdm/fleet/tree/main/tools/software/icons) script. **The script automatically adds the import statement and map entry to `frontend/pages/SoftwarePage/components/icons/index.ts`**, so you don't need to manually update the index file. 6. Add a description for the app in outputs/apps.json file. You can use descriptions from the wingest manifest. 7. Open a PR to the fleet repository with the above changes. The [#g-software Engineering Manager (EM)](https://fleetdm.com/handbook/company/product-groups#software-group) is automatically added reviewer. Also, @ mention the #g-software Product Designer (PD) in a comment that points them to the new icon. This way, the icon change gets a second pair of eyes. 8. If the app passes automated tests, it is approved and merged. The EM reviews the PR within 3 business days. The app should appear shortly in the Fleet-maintained apps section when adding new software to Fleet. The app icon will not appear in Fleet until the following release. ### Windows input file schema | Name | Type | Description | |--------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | string | **Required.** User-facing name of the application. | | `unique_identifier` | string | **Required.** Platform-specific unique identifier. For Windows, this is the `DisplayName`. | | `package_identifier` | string | **Required.** The `PackageIdentifier` from winget. Fleet uses this to pull the correct metadata for the app. | | `slug` | string | **Required.** Identifies the app/platform combination (e.g., `box-drive/windows`). Used to name manifest files and reference the app in [Fleet's best practice GitOps](https://fleetdm.com/docs/configuration/yaml-files#fleet-maintained-apps). Format: `/`, where app name is filesystem-friendly and platform is `darwin`. | | `installer_arch` | string | **Required.** `x64` or `x86` (most apps use `x64`). | | `installer_type` | string | **Required.** `exe`, `msi`, or `msix` (file type, not vendor tech like "wix") | | `installer_scope` | string | **Required.** `machine` or `user` (prefer `machine` for managed installs) | | `default_categories` | string | **Required.** Default categories for self-service if none are specified. Valid values: `Browsers`, `Communication`, `Developer Tools`, `Productivity`. | | `install_script_path` | string | Filepath to a custom install script (`.ps1`). Overrides the generated install script. Script must be placed in `inputs/winget/scripts/`. For `.msi` apps, the ingestor automatically generates install scripts. Do not add scripts unless you need to override the generated behavior. For `.exe` apps, you must provide PowerShell scripts that run the installer file directly. Fleet stores the installer and sends it to the host at install time; your script must execute it using the `INSTALLER_PATH` environment variable. | | `uninstall_script_path` | string | Filepath to a custom uninstall script (`.ps1`). Overrides the generated uninstall script. Script must be placed in `inputs/winget/scripts/`. For `.msi` apps, the ingestor automatically generates uninstall scripts. Do not add scripts unless you need to override the generated behavior. For `.exe` apps, you must provide a script to uninstall the app. Scripts for `.exe` apps are vendor-specific. Use the vendor’s documented silent uninstall switch or the registered UninstallString (if available), ensuring the script runs silently and returns the installer’s exit code. | | `fuzzy_match_name` | boolean | If the `unique_identifier` doesn't match the `DisplayName`, use `fuzzy_match_name` to specify that Fleet uses "fuzzy matching" to match the Fleet-maintained app and the inventoried software. For example, for Pritunl, the `unique_identifier` is "Pritunl" and the inventories software's `DisplayName` is "Pritunl Client". With `fuzzy_match_name` set to true, Pritunl app will be matched to the inventories software. | | `requires_client_os` | boolean | Set to `true` when the installer refuses to run on Windows Server SKUs (e.g., Dell Display and Peripheral Manager). Fleet's ingestion ignores this field; CI reads it to route validation to the `windows-11-arm` runner (the only GitHub-hosted client-OS Windows runner) instead of the default Windows Server x64 runner. | #### Windows troubleshooting - App not found in Fleet UI: ensure `apps.json` was updated by the generator and your override URL is correct - Install fails silently: confirm your `installer_type`, `installer_arch`, and `installer_scope` match the selected winget installer; run your PowerShell script manually on a test host - Uninstall doesn’t remove the app: prefer explicit uninstall scripts; otherwise, ensure the winget manifest exposes `ProductCode` or `UpgradeCode` - Hash mismatch errors: if the upstream manifest is in flux, you can set `ignore_hash: true` in the input JSON (use sparingly) #### Can I do this on macOS? The instructions below are meant to be run on a Windows host. But, you can run most of this on a macOS host, as well: - You can author Windows inputs and run the generator on macOS. The ingester is Go code that fetches data from winget/GitHub and works cross‑platform. - To find the PackageName and Publisher, you can look in the locale and installer yaml files in the winget-pkgs repo. - Validation and testing still require a Windows host (to verify programs.name and to run install/uninstall). ## Updating existing Fleet-maintained apps Fleet-maintained apps need to be updated as frequently as possible while maintaining reliability. This is currently a balancing act as both scenarios below result in customer workflow blocking bugs: - App vendor updates to installers can break install/uninstall scripts - App vendors will deprecate download links for older installers A Github action periodically creates a PR that updates one or more apps in the catalog by: - Bumping versions - Regenerating install/uninstall scripts Each app updated in the PR must be validated independently. Only merge the PR if all apps changed meet the following criteria: - [X] App can be downloaded using manifest URL - [X] App installs successfully on host using manifest install script - [X] App exists on host - [X] App uninstalls successfully on host using manifest uninstall script If an app does not pass test criteria: - [Freeze the app](#freezing-an-existing-fleet-maintained-app) - File a bug for tracking ## Freezing an existing Fleet-maintained app If any app fails validation: 1. Do not merge the PR as-is. 2. Add `"frozen": true"` to the failing app's input file (e.g.,`inputs/homebrew/.json`). 3. Revert its corresponding output manifest file (e.g., `outputs/.json`) to the version in the `main` branch: ```bash git checkout origin/main -- ee/maintained-apps/outputs/.json ``` 4. Validate changes in the frozen input file by running the following. This should output no errors and generate no changes. ```bash go run cmd/maintained-apps/main.go --slug="" --debug ``` 5. Commit both the input change and the output file revert to the same PR.