Open-source managed software installation for macOS — repo structure, pkginfo and manifest reference, commands, and the AutoPkg patching loop.
| Area | Concept | What it is | Notes |
|---|---|---|---|
| Repo | pkgs/ | The actual installer payloads — .pkg, .dmg, .mpkg files. | The only large directory. Everything else in the repo is small text files. |
| Repo | pkgsinfo/ | One plist per item describing it: name, version, where the installer lives, how to detect it's installed, dependencies. | The heart of Munki. Everything interesting is expressed here. |
| Repo | catalogs/ | A compiled index of every pkginfo, generated by makecatalogs. | Never hand-edit these. They're build output — edit pkgsinfo and regenerate. |
| Repo | manifests/ | Per-machine or per-group lists declaring what a client should have. | A manifest can include other manifests, which is how you build a role hierarchy. |
| Repo | icons/ | PNG icons displayed in Managed Software Center. | Populate with iconimporter. Cosmetic, but a catalogue without icons looks abandoned. |
| Repo | client_resources/ | Optional branding for Managed Software Center. | Worth doing — a branded catalogue gets used, an unbranded one gets ignored. |
| Manifest | managed_installs | Items Munki keeps installed. If missing or outdated, it installs or updates them. | Self-healing: delete the app and it comes back at the next run. |
| Manifest | managed_uninstalls | Items Munki actively removes and keeps removed. | Requires the pkginfo to define a working uninstall method. |
| Manifest | optional_installs | Items offered in Managed Software Center for users to install themselves. | The self-service layer. Every non-mandatory app belongs here. |
| Manifest | managed_updates | Items Munki updates if already present, but won't install fresh. | Useful for "keep it current if they have it" software. |
| Manifest | included_manifests | Nested manifests — the mechanism for composing role-based hierarchies. | e.g. a machine manifest includes design_team, which includes site_default. |
| Manifest | conditional_items | Sections applied only when an NSPredicate condition is true. | Branch on OS version, hostname, architecture, or a custom conditional script. |
| Pkginfo | installs array | Explicit list of files/apps and versions Munki checks to decide if an item is installed. | More reliable than receipts for drag-and-drop apps. Usually what you want. |
| Pkginfo | receipts | Package receipt IDs and versions recorded by the macOS installer. | Works for real .pkg installs. Useless for apps dragged from a DMG. |
| Pkginfo | unattended_install | Allows installation in the background without user interaction. | Only set it when the installer genuinely is silent and the app isn't in use — otherwise you interrupt people. |
| Pkginfo | blocking_applications | Apps that must be closed before installing. | Prevents replacing a running application underneath the user — a real source of corruption. |
| Pkginfo | update_for | Marks this item as an update to another item rather than a standalone install. | How you deliver a plugin or patch tied to a parent application. |
| Pkginfo | requires | Dependencies installed first. | Munki resolves the order. Avoid deep chains — they're hard to debug. |
| Pkginfo | supported_architectures | Restricts an item to arm64 or x86_64. | Essential where a vendor ships separate Apple silicon and Intel builds. |
| Pkginfo | force_install_after_date | Hard deadline after which Munki installs regardless, logging the user out if needed. | Powerful and disruptive. Use for security updates, communicate it first. |
| Client | SoftwareRepoURL | Where the client fetches the repo from. | The one setting that must be right. Deliver it via configuration profile, not a local defaults write. |
| Client | ClientIdentifier | Which manifest this machine uses. | If unset, Munki tries hostname, then serial, then site_default — see the Gotchas tab. |
| Client | Managed Software Center | The user-facing app: browse optional installs, see pending updates, trigger installs. | Munki's self-service front end and the main reason users tolerate managed software. |
Create the repo directory structure
Six directories. That's the entire server-side product.
Install the admin tools and configure munkiimport
Grab the current munkitools installer from the project's releases. Then point the tools at your repo — this writes ~/Library/Preferences/com.googlecode.munki.munkiimport.plist.
Serve the repo over HTTPS
Any static web server. The one thing that matters: do not allow directory listing, and make sure .plist files are served as plain files rather than being interpreted.
Create a default manifest
site_default is the fallback every client lands on if nothing more specific matches.
Import your first item
munkiimport copies the installer into pkgs/, generates a pkginfo, and opens it for editing.
Configure clients by profile, not by hand
Deliver ManagedInstalls preferences as a configuration profile from your MDM. A profile is enforced, versioned, and removable; a local defaults write is none of those things and will drift.
Test on one machine before anything else
Verbose output tells you exactly which manifest and catalog it resolved — which is the answer to most first-run problems.
- Always HTTPS. Munki downloads and installs software with root privileges. Plain HTTP is a remote code execution path.
- The repo is read-only to clients. Only GET is ever needed — deny everything else at the web server.
- Basic auth is the common approach, with credentials delivered by configuration profile. Client certificates are stronger if you can manage the PKI.
- Never allow directory listing. An indexable repo tells an attacker your entire software inventory and versions.
- Munki supports middleware for signed requests — useful for S3 or CDN-backed repos where basic auth isn't practical.
- Anyone who can write to the repo can run code as root on every managed Mac. Treat repo write access as production infrastructure access.
| Where | Command | What it does |
|---|---|---|
| Client | sudo managedsoftwareupdate | Check the repo and install anything pending. The core client command. |
| Client | sudo managedsoftwareupdate --checkonly | Check what would be installed without installing. Safe to run any time. |
| Client | sudo managedsoftwareupdate --installonly | Install what's already been downloaded, skipping the check. |
| Client | sudo managedsoftwareupdate --auto | The mode the LaunchDaemon runs — respects unattended rules and user presence. |
| Client | sudo managedsoftwareupdate -vv | Verbose. Shows the manifest and catalogs it resolved — the first debugging step. |
| Client | managedsoftwareupdate --version | Installed Munki version. Include it in any bug report. |
| Client | defaults read /Library/Preferences/ManagedInstalls | Dump effective client config — repo URL, identifier, and all options. |
| Client | tail -f /Library/Managed\ Installs/Logs/ManagedSoftwareUpdate.log | Follow the client log live. Where the real answer usually is. |
| Client | cat /Library/Managed\ Installs/InstallInfo.plist | Exactly what Munki has decided to do on the next run. |
| Admin | munkiimport /path/to/App.dmg | Import an installer: copies to pkgs/, generates pkginfo, opens it to edit. |
| Admin | munkiimport --configure | Set repo path, default catalog, pkginfo editor. |
| Admin | makecatalogs /path/to/repo | Rebuild catalogs from pkgsinfo. Run after every pkginfo change or clients won't see it. |
| Admin | makepkginfo /path/to/item | Generate a pkginfo without importing — useful for inspecting what Munki detects. |
| Admin | makepkginfo -f /Applications/App.app | Generate an installs array entry from an existing app. |
| Admin | iconimporter /path/to/repo | Extract app icons into icons/ so Managed Software Center looks populated. |
| Manifest | manifestutil | Interactive shell for manifest editing. Tab completion works. |
| Manifest | manifestutil new-manifest <name> | Create a manifest. |
| Manifest | manifestutil add-catalog production --manifest <name> | Attach a catalog. A manifest with no catalog resolves nothing. |
| Manifest | manifestutil add-pkg Firefox --section managed_installs --manifest <name> | Add an item to a section. |
| Manifest | manifestutil add-included-manifest <child> --manifest <parent> | Nest manifests — the basis of role hierarchies. |
| Manifest | manifestutil display-manifest <name> | Print a manifest's full contents. |
| Manifest | manifestutil list-manifests | Every manifest in the repo. |
| Manifest | manifestutil find <name> | Find which manifests reference an item — before removing it. |
| AutoPkg | autopkg run -v Firefox.munki | Download, package, and import into Munki automatically. |
| AutoPkg | autopkg repo-add recipes | Add the community recipe repository. |
| AutoPkg | autopkg search <app> | Find an existing recipe before writing one yourself. |
A manifest hierarchy that scales
- Standard pattern is three catalogs: testing → staging → production.
- An item's
catalogsarray controls which stage it's visible in. Promotion is editing that array and runningmakecatalogs. - Your own Mac's manifest points at
testing. A pilot group points atstaging. Everyone else is onproduction. - New imports default to
testing— so nothing reaches users until you deliberately promote it. - This costs nothing to set up and is the single biggest protection against shipping a broken package fleet-wide.
The weekly patching loop
Let AutoPkg do the fetching
Manually downloading installers doesn't scale past a handful of apps. AutoPkg's .munki recipes download, verify, package, and import in one run.
Schedule it and get notified
Run AutoPkg on a schedule via AutoPkgr or a LaunchDaemon, with Slack or email notification so you know when something new landed.
Test on your own machine first
New items land in testing. Your Mac is on that catalog, so you get it immediately. Install it. Open the app. Confirm it actually works.
Promote to staging, wait, then production
Edit the pkginfo's catalogs array, run makecatalogs. Give the pilot group a few days before going wide. Most bad packages surface within 48 hours.
Prune old versions
The repo grows quietly. Keep the current and one previous version of each item; delete older pkgs and their pkgsinfo, then makecatalogs again.
Munki alongside an MDM
| Job | MDM | Munki |
|---|---|---|
| Enrollment (ADE / zero-touch) | ✔ Only MDM can | — |
| Configuration profiles | ✔ Only MDM can | — (can deliver, but MDM is correct) |
| PPPC / privacy approvals | ✔ MDM-delivered profiles only | — |
| FileVault key escrow | ✔ | — |
| Third-party app deployment | Limited | ✔ Munki's core strength |
| Self-service catalogue | Varies | ✔ Managed Software Center |
| Complex install logic / conditions | Limited | ✔ Far more expressive |
| macOS updates | ✔ Prefer DDM enforcement | Possible, but MDM is better now |
- MDM enrolls the Mac, applies profiles, escrows the FileVault key, and installs the Munki client package.
- Munki takes over all third-party software from there.
- Deliver Munki's
ManagedInstallssettings as an MDM configuration profile — enforced and versioned, rather than a local write that drifts. - This works with any MDM. SimpleMDM even hosts a Munki repo for you; Jamf, Mosyle, and Intune all pair with a self-hosted one.
Gotchas
- By far the most common Munki mistake. You edit a pkginfo, nothing changes on clients, and you assume something is broken.
- Catalogs are compiled output. Clients read catalogs, never pkgsinfo directly.
- Run
makecatalogsafter every pkginfo change — import, edit, promotion, or deletion. - Make it muscle memory, or wrap your workflow in a script that always calls it.
- If
ClientIdentifierisn't set, Munki tries in order: fully-qualified hostname, then short hostname, then hardware serial, thensite_default. - That's a useful default — name a manifest after a serial and that Mac picks it up with no client config at all.
- It also means a renamed Mac can silently change manifests. If a machine suddenly gets the wrong software, check this first.
managedsoftwareupdate -vvprints exactly which manifest was resolved. Always check before theorising.
- Receipts only work for real
.pkginstalls. An app dragged from a DMG leaves no receipt, so Munki can't tell it's installed and reinstalls it every run. - For anything copied rather than installed, use an
installsarray pointing at the app bundle and its version key. - Generate one with
makepkginfo -f /Applications/App.apprather than hand-writing it. - Symptom to recognise: an item that installs successfully on every single run. That's a detection problem, not an installer problem.
- Only set it where the installer is genuinely silent and non-disruptive.
- Always pair it with
blocking_applicationsso Munki won't swap an app out from under someone using it. - Anything requiring a restart or logout should not be unattended — surface it in Managed Software Center instead.
force_install_after_datewill log a user out to meet the deadline. Communicate before using it.
- Munki installs as root on every managed Mac. Anyone who can write to the repo can run arbitrary code fleet-wide.
- Serve over HTTPS only, never plain HTTP.
- Restrict repo write access as tightly as production server access — because that's what it is.
- Disable directory listing so your software inventory isn't publicly enumerable.
- Use
supported_architectureswhere a vendor ships separate arm64 and x86_64 builds — shipping the wrong one is a confusing failure. - Some installers need Rosetta; deploy it as a dependency rather than assuming it's present.
- For macOS updates themselves, prefer MDM. Declarative Device Management enforcement is more reliable than Munki's Apple-update handling on modern macOS, especially given Secure Token requirements on Apple silicon.
Debugging a client, in order
| Symptom | Check |
|---|---|
| Nothing installs at all | managedsoftwareupdate --checkonly -vv — does it resolve a manifest? A 404 on the manifest means ClientIdentifier is wrong or the manifest doesn't exist. |
| Item is in the manifest but ignored | Is the item in a catalog the manifest actually references? Did you run makecatalogs? |
| Reinstalls on every run | Detection problem — receipts on a drag-install app. Switch to an installs array. |
| Downloads then fails to install | ManagedSoftwareUpdate.log for the installer's own error. Often a blocking app or an unmet minimum OS. |
| 403 / 401 fetching the repo | Basic auth credentials missing or wrong in the client config. Test with curl -u user:pass <repo>/catalogs/production. |
| Works manually, not on schedule | Manual runs are root; the LaunchDaemon may hit a different network or proxy state. Check the log timestamps for the scheduled attempts. |
| Optional install missing from MSC | Confirm it's in optional_installs, in a referenced catalog, and passes minimum_os_version and architecture checks. |