top of page

AI-Assisted Munki: Generating pkginfo Without the Boilerplate

Writer: MacSmithAI
MacSmithAI
Oct 9
7 min read

Importing one app into a Munki repo is a few minutes' work. Importing forty of them eats a day, because the part that takes the time is not the import — it is the metadata. The installer gives you a name and a version. Everything that makes the item behave correctly on 10,000 Macs is a judgement call you type by hand, forty times, into forty nearly identical property lists.


That is exactly the shape of work a language model is good at: structured, repetitive, heavily conventional, and verifiable after the fact. It is also the shape of work where a confidently wrong answer ships silently, because a pkginfo file that parses fine can still be wrong in a way nobody notices until a laptop reboots in the middle of a customer call.


So here is where AI belongs in a Munki workflow, where it does not, and what the boundary looks like in practice.


Why not just let AutoPkg do it?

Fair question, and for a lot of software the answer is "you should." AutoPkg's MunkiImporter processor already does the mechanical half. Its documented inputs include pkginfo, described as a "Dictionary of pkginfo keys to copy to generated pkginfo," plus repo_subdirectory — "The subdirectory under pkgs to which the item will be copied, and under pkgsinfo where the pkginfo will be created" — and additional_makepkginfo_options, an "Array of additional command-line options that will be inserted when calling 'makepkginfo'." If a trusted recipe exists for the app, the recipe override is where your house conventions go, and no AI is required or wanted.


The gap is everything else: the vendor package that arrives by email, the in-house tool your team built last quarter, the line-of-business installer with no upstream recipe and no prospect of one. Those are the imports that eat the afternoon, and they are the ones where a draft you review beats a blank file you fill in.


This is also the natural sequel to giving Claude read-only access to the Munki repo. That post deliberately stopped at read. This one is about what the model produces once it can see how your repo already does things — and it still never writes to the repo itself.


What the tools generate, and what they leave you

Munki's own documentation is unusually honest about this. The Pkginfo Files page describes pkginfo files as "XML-formatted Apple Property Lists," says Munki clients never read them directly but query catalogs built from them, and then says of the generated file that admins "can and should add additional information (and sometimes correct the autogenerated info!)."


Look at what makepkginfo actually takes on the command line and you can see why. Its documented options are largely about pointing at things: -f FILE/--file=FILE for "Path to a filesystem item installed by this package, typically an application," -p PKGNAME/--pkgname=PKGNAME, -i ITEM/--itemname=ITEM, -a ITEM/--appname=ITEM, -d DESTINATIONPATH/--destinationpath=DESTINATIONPATH, the four script flags, -o/-g/-m for owner, group and mode, and -c CATALOG/--catalog=CATALOG, which "Specifies in which catalog the item should appear" and defaults to testing. The usage line is simply makepkginfo [/path/to/package_or_dmg] [-f /path/to/item/it/installs ...].


None of that decides whether the app can be installed while the user is logged in, which running applications must be closed first, what the item should be called in Managed Software Center, or which OS versions it is safe on. Those keys exist — the Supported Pkginfo Keys page documents dozens of them — but they are yours to fill in.


The keys to hand off, and the keys to keep

Key

What Munki does with it

Who decides it

installs

Checked for presence and version; takes precedence over receipts

Generate it from the installer, never from a model

receipts

Package receipts checked for presence and version

Generated by the tool; still matters at removal time

installcheck_script

Runs to decide if the item needs installing; exit 0 means not installed

AI can draft it, you must test the inverted exit codes

unattended_install

Lets Munki install without notifying the logged-in user

You - this one has a user-visible blast radius

blocking_applications

Applications that must be closed before the install proceeds

AI proposes candidates, you confirm against the real app

minimum_os_version / maximum_os_version

Lowest and highest compatible OS version

You, from the vendor's own release notes

supported_architectures

arm64 or x86_64

Check the binary, do not infer it from the app name

display_name / description / category / developer

What the user sees in Managed Software Center

Ideal AI work - prose and house convention

uninstallable / uninstall_method

Whether and how Munki can remove the item

You - a wrong uninstall method is how you strand an app


The split is the whole method. Anything derived from the installer itself should be generated from the installer itself. Anything that is a convention, a judgement, or a sentence a human will read is worth drafting with AI and reviewing in a diff.


Precedence is the part a model gets backwards

This is the trap, and it is worth memorising before you let any generated pkginfo near a catalog.


Munki checks installation status by several methods with a strict order of precedence, and the How Munki Decides page is explicit that "only the highest priority method is used." Highest first: OnDemand, then installcheck_script, then version_script (new in Munki 7), then installs, then receipts. So if an installs array is present, "the receipts will be ignored for purposes of determining installation status" — receipts may still matter at removal time, but not for deciding whether to install.


And the exit codes are inverted relative to every other check script you have ever written. For installcheck_script, the page states that "an exit code of 0 indicates that the item is currently not installed," and that all non-zero exit codes mean the item is installed. That is the opposite of the convention a model will reach for by default, and it is the single most common thing to find wrong in a generated draft. An inverted installcheck_script does not error. It just quietly reinstalls an app on every run, or never installs it at all.


Belt and braces: ask for the script, then test it on a machine where the app is present and a machine where it is not, and check the exit code with echo $? before you believe either.


A pipeline that keeps the generated part honest

The order matters more than the prompt. Generate the factual skeleton first, then hand the model something real to work from:


# 1. Let the tool produce the facts from the actual installer.
#    "The generated plist is printed to STDOUT." — makepkginfo docs
makepkginfo /Volumes/repo/pkgs/apps/VendorTool-3.4.pkg > /tmp/vendortool-draft.plist

# 2. Give the model the draft plus a known-good pkginfo from your own repo
#    as the style reference, and ask only for the judgement keys.

# 3. Review the diff, then import interactively
munkiimport /Volumes/repo/pkgs/apps/VendorTool-3.4.pkg

The prompt that works is narrow and refuses to invent:


Here is makepkginfo output for a vendor .pkg, and a pkginfo from our repo
that follows our conventions.

Fill in ONLY these keys: display_name, description, category, developer,
blocking_applications, unattended_install, minimum_os_version,
supported_architectures, uninstallable, uninstall_method.

Rules:
- Do not add, remove or alter name, version, installs, receipts or
  installer_item_location. Those came from the installer.
- If you cannot determine a value from what I gave you, omit the key and
  say which fact you are missing. Do not guess a version number.
- Match the description style of the reference pkginfo.
- Flag anything that looks inconsistent with the reference.

That last instruction earns its keep. The model reading both files will often catch the thing you were about to miss — a category that does not match any other item in the repo, or a supported_architectures of arm64 on a package that clearly ships a universal binary. The Supported Pkginfo Keys page notes the "Currently useful values" there are arm64 or x86_64.


Keep the write boundary where Munki already put it

Resist the urge to automate the import. Munki's interactive munkiimport already contains the review gates you want: it prompts "Edit pkginfo before upload? [y/n]", opens the file in your configured pkginfo editor, and asks "Rebuild catalogs? [y/n]" before running makecatalogs. Use --nointeractive and those prompts disappear — you then have to run makecatalogs yourself afterwards. The Makecatalogs page is blunt about the cadence: "You should run this after any change to pkginfo items to update all catalogs."


One flag to never put in an automated path: makecatalogs documents -f, --force as "Disable sanity checks." A generated pkginfo is precisely the input those checks exist for.


What changes at fleet scale

On one Mac, a bad pkginfo is an annoyance you notice and fix. On ten thousand, the blast radius is set by the catalogs key, which is why makepkginfo defaulting to testing is a gift rather than an inconvenience. Generated metadata should land in testing, go to a pilot group, and be promoted by a human who looked at the diff — the same three-layer discipline as reviewing shell scripts before they hit 10,000 Macs, and for the same reason. The cost of a wrong unattended_install on an app with blocking_applications is not a failed install. It is a forced quit during somebody's quarterly review.


The practical takeaway

Use AI for the keys a human would have typed from judgement, and never for the keys the installer already knows.


Concretely: run makepkginfo against the real installer and treat its output as immutable fact. Hand that output plus one known-good pkginfo from your own repo to the model, and ask only for display_name, description, category, developer, blocking_applications, unattended_install, the OS and architecture bounds, and the uninstall keys. Ask it to omit anything it cannot derive rather than guess. If you want an installcheck_script, take the draft but verify the inverted exit codes by hand on two machines. Then import interactively, let it land in testing, and run makecatalogs.


Done that way, forty imports becomes an afternoon of reviewing diffs instead of a day of typing — and every factual key in the repo still came from the package rather than from a model's best guess.



Comments


bottom of page