Books Update — 2025

It’s been a while since I wrote about my books. Life has been tugging me in different directions (in a good way, overall). Things are going well, overall, but there was always this nagging feeling that I really should do something about the books. They were getting a bit… well… old…

If you follow this blog, you may have noticed a few posts about packaging recently:

If you are a proud owner of my book “Packaging for Apple Administrators” (thank you very much!) these posts should seem somewhat familiar. It has been nearly nine years since I first published “Packaging” and even though it really held up well, it was in desperate need of some updates. More than merely updates, really. Many of the examples not available online anymore. Seriously, some of the examples are to inspect the iTunes and Silverlight installer pkg…

Surprisingly little has changed in the process of actually building packages, so those sections of the book hold up pretty well. But the environment in which packages are used and deployed on macOS has changed. Quite a lot. GateKeeper and Notarization were new and optional, just a few years ago, but now are a core part of Apple’s security strategy on macOS. Bundle package installers, which I covered in a “legacy” appendix in “Packaging” were completely disabled in macOS Sequoia 15. Imaging Macs with NetInstall was still a thing when I originally wrote the book and how to use and prepare installer packages for those workflows took up some space.

Distribution packages were only required in edge cases for Mac admins. Now they are often (but not always) required to work with device management servers.

On the other hand, back then I did not have any experience with the developer side of packaging. Since then I have written about building tools and apps and integrating the packaging (and signing and notarization workflows) in Xcode and Swift Package Manager. These are workflows that are useful to developers, but less so for Mac Admins.

So, I am happy to announce that I have started the work of updating “Packaging.” It’s a work in progress, and I do not want to commit to any timeline yet. However, I plan to continue to share the progress by posting sections on this blog as I update them.

What will happen to old, outdated “Packaging for Apple Administrators” you might ask? Well, I am going to remove the book from Apple Books in two weeks or so. If you really want to own a copy of this old version, this will be your last chance to purchase it. I didn’t want to remove the book without warning. But, honestly, most of you really don’t have to buy the old version anymore, since I will be posting parts as I update and rewrite.

(If you want to buy a copy to support me, don’t do that on Apple Books. The standard 30% of that revenue will go straight to Apple and honestly, they have enough money. There is now a better way, but more on that later.)

In two or three weeks time, I will remove all books, except “macOS Terminal and Shell” from Apple Books.

If you have purchased the book, it should remain available for you in your library, but maybe make a backup to be sure.

I have spent a few days updating “macOS Terminal and Shell” for the current state of macOS. Since this is my latest book and well, the command line situation hasn’t changed very much since Apple switched the default shell to zsh, there wasn’t much to change. I will keep that book on Apple Books and update it as soon as the new version passes Apple’s review. If you have already purchased “macOS Terminal and Shell” you should get the updated version as soon as I have uploaded it. You should then see a notification in the Books app.

I am also starting a new experiment: you can also purchase “macOS Terminal and Shell” on Ko-Fi. (might be more familiar as “Buy me a Coffee“) This is an experiment and new to me, so apologies if there are some rough edges. This should work if you do not want to or cannot purchase on Apple Books.

Also, I get a larger share of the proceeds. And, should you desire to, you can even pay more than the suggested price. (though, really, no-one has to)

As I said, this is a test run, and I am very curious how it goes. I am excited that this should expand the audience for whom the book is available. (Apple Books is not available in many regions, like India and China.) If the experiment works out for this update of “Terminal and Shell” then I will definitely consider this for “Packaging 2.0” and future books, as well. (I have plenty ideas, but so little time)

Building Simple Component Packages

Building packages can turn complex very quickly. We will start with a few simple examples first and build up from there.

You should read the first two posts in this series, first:

Boring Wallpaper

For our first simple example, imagine your organization insists that every Mac use the same wallpaper (or desktop picture). The first step for an admin is to provide the image file on each Mac.

Apple changed the naming for the background image in macOS Ventura from ‘desktop picture’ to ‘wallpaper’ in macOS Sonoma to match the naming across all their platforms. As we will see, the old name still appears in some places.

Technically, an image file used as a wallpaper image could be stored anywhere on the disk. When you place image files in /Library/Desktop Pictures/ (still the old name) a user will see them as a choice among the pictures in the ‘Wallpaper’ pane in System Settings. They will have to scroll all the way to the right in the ‘Dynamic Wallpapers’ or ‘Pictures’ section but the images you add in that folder will appear there.

The /Library/Desktop Pictures folder does not exist on a ‘clean’ installation of macOS, but the installation package will create it for us.

All the resources for building the package have to be provided in a certain folder structure. It is easiest to contain all of that in a single project folder.

I will provide the instructions to create and build the projects in the command line as they are easier to represent than descriptions of how to achieve something in the graphical user interface. It does not really matter whether you create a folder from Terminal or in Finder. Do what you are comfortable with. However, Getting comfortable with command line tools in Terminal is an important step towards automated package creation workflows.

My book “macOS Terminal and Shell” can teach you the fundamentals. Buy it on Ko-Fi or Apple Books!

I often find it helpful to have a Finder window for the current working directory open next to Terminal. You can open the current working directory in Finder with the open . command.

Create a folder for the project named BoringWallpaper in a location of your choosing.

> mkdir BoringWallpaper
> cd BoringWallpaper

You can download the sample image file I will be using here. Of course you can use another wallpaper image of your choice.

The BoringWallpaper folder will be our project folder. Create another folder inside BoringWallpaper named payload. Inside the payload folder, we will re-create the path to where we want the file be installed in the file system, in this case /Library/Desktop Pictures.

Since the Desktop Pictures folder name contains a space, we need to quote the path in the shell and scripts. You can also escape the space with a backslash \. The effect will be the same. In general, I recommend using tab-completion in the command line for file paths which will take care of special characters.

We can create the entire folder structure at once with mkdir -p:

> mkdir -p "payload/Library/Desktop Pictures"

Then copy the first desktop picture BoringBlueDesktop.png into payload.

> cp /path/to/BoringBlueDesktop.png "payload/Library/Desktop Pictures"

The payload folder will gather all the files we want the package to install.

Your folder structure inside the BoringWallpaper project folder should now look like this:

📁 payload
    📁 Library
        📁 Desktop Pictures
            📄 BoringBlueDesktop.png

The payload folder represents the root of the target volume during installation.This will usually be the root of the system volume /. We recreated the folder structure where we want the file to be installed in the file system. The installer will create intermediate folders that do not yet exist during installation.

In this example, the /Library folder will already exist, but the Desktop Pictures subfolder will not yet exist on a clean system, so it will be created and the image file will be placed inside.

When you run the installer on a system where the Desktop Pictures subfolder already exists, the image will be placed inside that. Should a file with the same name already exist in that location, it will be overwritten. Other files that might be in that folder will generally not be affected.

Introducing pkgbuild

macOS provides the pkgbuild command line tool to create installer package components. Make sure your current working directory is BoringWallpaper and run

> pkgbuild --root payload --install-location / --identifier com.example.BoringWallpaper --version 1 BoringWallpaper.pkg

The --root option designates the root of our payload, so we pass the payload folder.

The identifier should be a string modeled on the reverse DNS of your organization, e.g. com.scriptingosx.itservices.BoringWallpaper. The exact form of the identifier does not really matter as long as it is unique.

We will use com.example.BoringWallpaper for the identifier. For the exercise, you can use those or replace them with your own. When you build packages for production or deployment they should use your organization’s reverse DNS format.

This should create a BoringWallpaper.pkg file in your project folder.

You should now inspect the resulting pkg file with the tools from earlier:

> lsbom $(pkgutil --bom BoringWallpaper.pkg )
.    40755   0/0
./Library    40755   0/0
./Library/Desktop Pictures    40755   0/0
./Library/Desktop Pictures/BoringBlueDesktop.png    100644  0/0 17393342618871431

(You may see a ._BoringBlueDesktop.png file appear here. That is a resource fork file. Preview.app sometimes creates these for file previews. You can safely ignore those.)

There are two relevant things to notice here: the payload contains the intermediate folders /Library and /Library/Desktop Pictures, which means they will be created, should they not exist on the system yet. This is generally what we want to happen, but a good thing to keep in mind.

Also notice that pkgbuild set the owner and group ID for the folders and the image file to 0/0 or root:wheel. This is the default ownership for files installed by packages, which ensures that non-admin users cannot change, delete or overwrite the files. This is a useful default, but there are options to have more granular control.

pkgbuild will always preserve the file mode or access privileges. When you change the file mode in of the file in the payload folder before you run pkgbuild, the command line tool will use that for the payload and Bom. In this case, the 644 or r-wr--r-- file mode works quite well, but for a test, let’s change the mode to 444 (removing the write access for the owner) and re-run the pkgbuild command.

> chmod u-w payload/BoringBlueDesktop.png 
> pkgbuild --root payload --install-location / --identifier com.example.BoringWallpaper --version 1 BoringWallpaper.pkg
pkgbuild: Inferring bundle components from contents of payload
pkgbuild: Wrote package to BoringWallpaper.pkg
> lsbom $(pkgutil --bom BoringWallpaper.pkg )
.    40755   0/0
./Library    40755   0/0
./Library/Desktop Pictures    40755   0/0
./Library/Desktop Pictures/BoringBlueDesktop.png    100644  0/0 17393342618871431

Note that running the pkgbuild command again overwrote the previously generated pkg file with warning. This is generally not a problem, but something you need to be aware of.

We will want to change the image file in the following steps, so add the writable flag back:

> chmod u+w "payload/Library/Desktop Pictures/BoringBlueDesktop.png"

Handling extended attributes

In recent versions of macOS, pkgbuild will preserve extended file attributes in the payload.

This is a change in behavior to earlier versions of macOS, where you had to use the undocumented --preserve-xattr option to preserve extended attributes.

Most extended attributes contain metadata for Finder and Spotlight. For example, when you open the image file in Preview, you will get a com.apple.lastuseddate#PS extended attribute. You can use the -@ option of the ls command to see extended attributes:

> open "payload/Library/Desktop Pictures/BoringBlueDesktop.png"
> ls -l@ "payload/Library/Desktop Pictures"
total 3400
-rw-r--r--@ 1 armin  staff  1739334 Aug  1 14:03 BoringBlueDesktop.png
    com.apple.lastuseddate#PS        16 

You generally do not want to have extended attributes be part of your package payload. This is especially true of quarantine flags!

There are some exceptions. For example, signed shell scripts store the signature information in an extended attribute. In these cases you will have to carefully build your package creation workflow to ensure only the desired extended attributes are preserved in the package and installed to the target file system.

You can remove extended attributes recursively with the xattr command:

> xattr -cr payload

Then rebuild the package:

> pkgbuild --root payload --install-location / --identifier com.example.BoringWallpaper --version 1 BoringWallpaper.pkg
pkgbuild: Inferring bundle components from contents of payload
pkgbuild: Wrote package to BoringWallpaper.pkg

Creating a Build Script

The command line tools to create installer package files have a large number of options. Even in our simple example, pkgbuild requires several options and arguments. Each one needs to be entered correctly so the installer process does the right thing. An error in the identifier or a version number will result in unexpected behavior that may be very hard to track down. In addition, there are steps like running xattr -c that need to be performed before creating the package.

To avoid errors and simplify the process, we will create a shell script which runs the required commands with the correct options. The script will always repeat the commands with the proper arguments in the correct order, reducing the potential for errors. Updating the version is as simple as changing the variable in the script.

In your favored text editor create a file named buildBoringWallpaperPkg.sh and with the following code:

#!/bin/sh

pkgname="BoringWallpaper"
version="1.0"
install_location="/"
identifier="com.example.${pkgname}"

export PATH=/usr/bin:/bin:/usr/sbin:/sbin

projectfolder=$(dirname "$0")
payloadfolder="${projectfolder}/payload"

# recursively clear all extended attributes
xattr -cr "${payloadfolder}"

# build the component
pkgbuild --root "${payloadfolder}" \
         --identifier "${identifier}" \
         --version "${version}" \
         --install-location "${install_location}" \
         "${projectfolder}/${pkgname}-${version}.pkg"

The script first stores all the required pieces of information in variables. This way you can quickly find and change data in one place and do not have to search through the entire script.

The identifier variable is composed from our com.example reverse-DNS prefix and the pkgname variable set earlier.

Then the script sets the shell PATH variable, which is always a prudent step.

In the next line, the script determines the folder enclosing the script, by reading the $0 argument which contains the path to the script itself and applying the dirname command which will return the enclosing folder. This way we can use the projectfolder variable later to write the resulting pkg file into a fixed location (the project folder), instead of the current working directory.

Finally the pkgbuild command is assembled from all the variables.

The backslash \ in a shell script allows a command to continue in the next line. Instead of a command in a single very long line we can have one line per argument. This makes the script easier to read and update.

In Terminal, set the script file’s executable bit with

> chmod +x buildBoringWallpaperPkg.sh

Delete the original BoringWallpaper.pkg and run the build script.

> rm BoringWallpaper.pkg
> ./buildBoringWallpaperPkg.sh
pkgbuild: Inferring bundle components from contents of ./payload
pkgbuild: Wrote package to ./BoringWallpaper-1.0.pkg 

This will create a new package file named BoringWallpaper-1.0.pkg.

Now you do not have to remember every option exactly but instead can run ./buildBoringDesktopPkg.sh. If you need to change options like the version number, it is easy to do by changing a variable. Package creation is easy to repeat and if you use a version control system (e.g. git) changes to the script are tracked.

You have literally codified the package creation process. If you are working in a team, you can point to the script and say: “This is how we create this package!” and everyone who has access to the script can recreate the package creation workflow. They can also read the script and understand what is going to happen. A script like this does not replace the need for documentation, but is better than no documentation at all.

To be precise, the build script and the files and folder structure in the payload are required together for the package creation workflow. They should be kept and archived together.

Ideally together with documentation describing:

  • motivation/reason for building this package
  • where and how the package is intended to be used
  • whether the software installed requires certain configurations that are not provided by the pkg, such as licenses or default settings through a script or a configuration profile and where and how to obtain and set those
  • macOS and platforms (Intel or Apple silicon) the package was built on
  • macOS versions and platforms the package was tested on
  • where to obtain the resources in the payload, should they get lost or need to be updated
  • person(s) or team responsible for this package project
  • version history or change log
  • an archive of older versions of the pkg file
  • uninstall process or script
  • any other relevant links and history, for example problems and issues that lead to certain design choices

For developers, scripts like this can be part of an automated release or CI/CD workflow.

You do not have to include the version number in the package name, but it helps in many situations. It also helps when you build an archive or history of installers. You never know when you will need an older version of an installer. When vendors/developers provide the version number in their file names, it helps admins and users identify new or outdated versions.

You should (again) inspect the new package file you just created in Suspicious Package and using pkgutil.

Testing the Package

Finally, you can install this package on your test machine.

For this simple package, you can also use the Mac you are currently working on. With more complex packages, especially when we get into installation scripts and LaunchD configuration, a virtual machine or separate testing device is strongly recommended.

When you run the package in the Installer application, note that the dialogs are the defaults and very terse. System administrators will rarely build packages that are meant to be installed with the user interface, so this is not a problem. Most administrative package files will be installed by management systems in the background and never show the UI.

Developers can customize the user interface for installer packages with distribution files, we will get to in a future post.

You can also use the installer command to install the package:

> sudo installer -pkg BoringWallpaper-1.0.pkg -tgt / -verbose
installer: Package name is BoringWallpaper-1.0
installer: Installing at base path /
installer: The install was successful.

After installing, go to /Library/Desktop Pictures and look for the BoringBlueDesktop.png image file, then open System Settings and go “Wallpaper.” You will have to scroll down to the “Pictures” section and all the way to the right, but the picture will appear there!

You can also open Terminal and run the pkgutil commands to inspect what was installed: (replace com.example.* with your identifier)

> pkgutil --pkgs="com.example.*"         
com.example.BoringWallpaper

> pkgutil --info com.example.BoringWallpaper
package-id: com.example.BoringWallpaper
version: 1.0
volume: /
location: 
install-time: 1754058568

> pkgutil --files com.example.BoringWallpaper
BoringBlueDesktop.png

> pkgutil --file-info /Library/Desktop\ Pictures/BoringBlueDesktop.png                        
volume: /
path: /Library/Desktop Pictures/BoringBlueDesktop.png

pkgid: com.example.BoringWallpaper
pkg-version: 1.0
install-time: 1754058568
uid: 0
gid: 0
mode: 100644

If you created an installer package that attempted to install the image file in /System/Library/Desktop Pictures, it would fail. This directory is protected by two technologies, System Integrity Protection and the read-only sealed System volume. Figuring out the proper location to install management files is an important, but sometimes complicated task.

Removing the installed files

If you tested this package on your main Mac, you can remove the installed files with the following commands:

> sudo rm /Library/Desktop\ Pictures/BoringBlueDesktop.png
> sudo rmdir /Library/Desktop\ Pictures
> sudo pkgutil --forget com.example.BoringWallpaper

Note the rmdir command will error when there are other files remaining in it. This is intentional here, since we only want to remove the folder if it is empty and not remove files other than those we installed. The /Library folder is part of the base macOS installation, so we are not going to touch it.

Similar to the build script, it can be useful to maintain the un-install script while developing the package, especially for more complex installs. An un-install script for this project might look like this:

#!/bin/sh

# uninstall Boring Wallpaper

# reverts the installation of com.example.BoringWallpaper


# remove the file
rm -vf "/Library/Desktop Pictures/BoringBlueDesktop.png"

# remove folder
# fails when there are other files remaining inside
# we do not want to affect files we did not install
rmdir -v "/Library/Desktop Pictures"

# forget the pkg receipt
pkgutil --forget com.example.BoringWallpaper

Since the installed file and folder are owned by root, you need to run the entire script with root privileges or sudo:

> sudo ./uninstallBoringWallpaper.sh

Note that many device management services offer the option to run scripts on the Mac clients and they generally run the scripts with root privileges. Consult your management service’s documentation for details.

You have to remember to update the uninstall script when you change the payload and other settings in the package. This will be useful for your testing.

Developers or vendors can also provide the uninstall script to customers in case they need to uninstall the software. Administrators could use the uninstall script in a self service portal to allow end users to remove software they no longer require.

Note that software might create other files while it is running that are not part of the installer package. Use your judgment whether they need to be removed as part of the uninstall script. Some files might contain user data that should be preserved, even when the software is deleted.

Installing Packages

What is a package?

If you have been using a Mac, you will have encountered an installation package file or pkg.

Package files are used to install software and configuration files on your Mac.

Package files come in different flavors and formats, but they can be summarized to these relevant components:

  • a payload
  • installation scripts

A package file may have only a payload, only scripts, or both.

The payload is an archive of all the files that the package will install on the system. The package also contains a “bill of materials” (BOM) which lists where each file should be installed and what the file privileges or mode should be.

Installation scripts can be executed before and after the payload is installed.

Additionally, packages contain some metadata, which provides extra information about the package and its contents. They can also contain images and text files, such as license agreements that can customize the default Installer app interface.

Installer application

The most common way of installing a package file and start its installation process, is to open it with a double-click. This opens the default application for the pkg extension: Installer. The Installer app can be found in /System/Library/CoreServices/. However, you rarely need to open it directly. It is usually started indirectly by opening a package file.

After launch, the Installer app presents different panels to the user. The exact order and content of the panels depends on what the developer of the package configured. At the very least, it will show:

  • a short introduction
  • prompt to authenticate for administrative privileges
  • a progress bar of the installation
  • whether the installation succeeded or failed

A package may also show:

  • a custom background image
  • a detailed introduction
  • a license agreement that needs to be accepted
  • alternative installation location
  • options to select certain subsets of files and apps (components)
  • more custom steps implemented by the developer

Installer app can also list the contents of a package file without installing it first. You can choose ‘Show Files…’ (⌘I) from the ‘File’ menu to get a list of files in the payload. This list can be very extensive and there is a search field to filter the list.

If you want to see more than just the progress bar during the installation, you can select “Installer Log” from the “Window” menu (⌘L) and then increase the output to “Show all Errors and Logs” (⌘3).

You can also review the installation log afterwards, by opening the Console application from Utilities and choosing “install.log” under “Log Reports.” You can also look at /var/log/install.log directly.

This log is notoriously verbose. There will be many entries for each installation and some related system services like software update will log here, too.

Security

Packages can place files and executables in privileged locations in the file system. When you open a package file with the installer application, it will usually prompt for administrator privileges.

In the early days of Mac OS X, package installers had no limitations at all. However, these days, digital security and privacy are important features and criteria for platforms and Apple has implemented several features in macOS which set strong limits on what third-party package installers can do.

Most importantly, System Integrity Protection (SIP, introduced in OS X Yosemite 10.11) and the Signed System Volume (introduced in macOS Catalina 10.15) prevent third-party packages from affecting system files.

Even with these protections in place, packages still provide many options for abuse. Packages are very useful to install legitimate software but can also be used to install and persist malicious tools.

File Quarantine

File quarantine does not directly limit the abilities of package installers, but it is important to understand how it works together with other security features in macOS.

Quarantine flags were introduced in Mac OS X as early as version 10.4 (Tiger) but were not actually used for much until later in 10.5 (Leopard).

When a file arrives on your Mac from an external source, such as a download in a web browser, an email attachment, or an external drive, the process that downloads or copies generally adds a quarantine flag. The quarantine flag is added in an extended attribute to the file.

Note: the examples here reference desktoppr-0.5-218.pkg, the installer for a open source command line tool I wrote. You can download its installer pkg from the GitHub repository. The version and build number might be different.

After the download, you can see some of the metadata added to the download the Finder info window. Select the downloaded pkg file and choose ‘Get Info’ (⌘I) from the context or ‘File’ Menu. In the Info window that appears, you need to expand the ‘More Info’ section, where you will see the URL it was downloaded from. There might be more than one URL if the browser was redirected.

You can get more information in the command line using the xattr command line tool: (xattr stands for ‘extended attribute’, it is often pronounced like ‘shatter’)

> xattr desktoppr-0.5-218.pkg         
com.apple.metadata:kMDItemDownloadedDate
com.apple.metadata:kMDItemWhereFroms
com.apple.quarantine

The quarantine flag has the attribute name com.apple.quarantine. You can also use xattr to show the contents of the extended attribute:

> xattr -p com.apple.quarantine desktoppr-0.5-218.pkg
0083;6842fcbe;Safari;A2964F09-ACDF-430F-8CFF-48BD75C464CD

The contents are (in order, separated by semi-colons):

  • a hexadecimal flag number: always 0083
  • a hexadecimal timestamp: e.g. 6842fcbe
  • the process that created the file: e.g. Safari
  • a universal identifier (UUID)

You can convert the hexadecimal timestamp into a human readable time with

> date -jf %s $((16#6842fcbe)) 
Fri Jun  6 16:35:42 CEST 2025

There are two more extended attributes attached to the downloaded file that are interesting. The awkwardly named com.apple.metadata:kMDItemDownloadedDate and com.apple.metadata:kMDItemWhereFroms contain the download date and the web addresses the file was downloaded from respectively. When you look at them withxattr, you see the data is stored in a binary property list format.

> xattr -p com.apple.metadata:kMDItemDownloadedDate desktoppr-0.5-218.pkg
bplist00?3A????r

To show this in a human readable format, you have to pipe the output through xxd and plutil:

> xattr -x -p com.apple.metadata:kMDItemDownloadedDate desktoppr-0.5-218.pkg | xxd -r -p | plutil -p -
[
  0 => 2025-06-06 14:35:42 +0000
]

The name of the extended attribute gives us a hint that the information is also accessible through the file’s metadata, which is a bit easier:

> mdls -name kMDItemDownloadedDate desktoppr-0.5-218.pkg
kMDItemDownloadedDate = (
    "2025-06-06 14:35:42 +0000"
)

The named kMDItemWhereFroms attribute contains the URLs the file was downloaded from. It might be more than one URL because of redirections.

The URLs for GitHub downloads tend to be very long. After manually downloading Firefox, I see these URLs:

> mdls -name kMDItemWhereFroms Firefox\ 139.0.1.dmg 
kMDItemWhereFroms = (
    "https://download-installer.cdn.mozilla.net/pub/firefox/releases/139.0.1/mac/en-US/Firefox%20139.0.1.dmg",
    "https://www.mozilla.org/"
)

It is important to remember that not all processes add quarantine flags to downloaded or copied files. As a general rule, applications with a user interface add quarantine files and command line tools and background processes do not. But there may be exceptions either way.

For example, when you download a file using the curl command line tool, it will have no quarantine flag or other metadata extended attributes. This is might be exploited by malicious software.

The quarantine flag serves as a signal to the system that this file or archive needs to be checked before opening or running. The part of the system that performs this check is called Gatekeeper.

Gatekeeper

Gatekeeper was introduced to macOS in OS X Lion (10.7) in 2011. Gatekeeper will verify the integrity, signature and notarization status of an app or executable before opening it.

A package installer file can be:

  • not signed at all
  • signed, but not notarized
  • signed and notarized

Gatekeeper works somewhat differently for installer packages compared to applications that you download in disk image (dmg) or other archive formats (e.g. zip). When you copy an application out of a disk image into the Applications folder or unarchive it from an archive, the system transfers the quarantine flag and metadata to the application bundle on the local disk. When you then open the app for the first time, the presence of the quarantine flag signals Gatekeeper to verify the integrity of the application using the signature and verify the notarization status. You can see the dialogs the system might present in this Apple support document.

The signature verifies the integrity and the source of an application or installer package. With an intact signature, you can be certain the package file has not been modified since it was signed. If the signature uses an Apple Developer ID, you can be fairly certain that the package was signed by that developer, or someone from that organization. There have been cases where Apple Developer ID certificates were stolen, but Apple will usually invalidate those fairly quickly.

Notarization is an extra step where the developer uploads the signed package file to Apple’s notarization servers. Apple then scans the package for certain security features and whether known malware signatures are present. Apple then adds the package file’s hash to their notarization database. The developer can also ‘staple a ticket’ to the package file, so that Gatekeeper doesn’t have to reach out to Apple’s servers on the check.

When the Gatekeeper verification of the signature and notarization status succeeds, the user gets a prompt to confirm that they want to launch the application they just downloaded.

When either verification fails, most commonly because the app is not notarized, the user gets a different, quite scary, prompt, stating that the system cannot verify the package is free of malware. You will get the same dialog when the package is not signed at all.

While generally, all software developers should sign and notarize their packages, this is not always the case. Open source projects, often have neither the financial nor logistical means to obtain an Apple Developer account, which provides the required Apple Developer ID certificates.

Bypassing Gatekeeper

Before you choose to install a package file which is not validly signed or notarized, you should first be certain the source is trustworthy. Then you should probably inspect the package using the tools in this post to verify that it only installs what it is supposed to, before actually installing it.

After attempting to open the pkg file with the Installer app by double-clicking and receiving the dialog that it could not be verified, click ‘Done.’ Then navigate to the ‘Privacy & Security’ pane in System Settings. Under the ‘Security’ section, you will see a message that the package ‘was blocked to protect your Mac’ with a button to ‘Open Anyway.’

This extra option will disappear after a few minutes.

You can also remove the quarantine flag using the command line.

> xattr -d com.apple.quarantine desktoppr-0.1.pkg

Without the quarantine flag, the Gatekeeper verification will not be triggered when the file is opened.

Packages installed by a device management service are not checked by Gatekeeper and do not need to be notarized. With some services, the packages may need to be signed, but not necessarily with an Apple Developer ID. Consult the documentation of your device management service for details.

Installer command line tool

You can also install package files from the command line using the installer tool.

To install a package file on the current system volume, use the installer command like this

> sudo installer -package desktoppr-0.5-218.pkg -target /

There are shorter flags for both these options:

> sudo installer -pkg desktoppr-0.5-218.pkg -tgt /

Installing a package with the installer command will not enforce a restart or logout, whether the package requires one or not. You will have to perform or schedule the reboot manually.

Installing with installer will also not trigger GateKeeper checks, whether the quarantine flag is set or not.

The -verbose flag will increase the output of the installer tool which can help when you need to analyze problems. The installer process will also log to /var/log/install.log so you can also monitor or review the installation log in the Console app.

Apple Platform updates for July 2025

I usually post this collection of links as part of the weekly MacAdmins.news summary, but that is currently on a slower bi-weekly “Summer Camp” schedule. So I am posting this here for a change and will link in the issue next week.

The dot-six updates, as is common are mostly bug fixes and security patches. The enterprise notes have a bit of relevant information, especially for macOS 15.6. (More so than the general “What’s new in” articles.

And with this, we say good-bye to macOS Ventura 13. Barring some terrible security vulnerability, this will be the last update for the macOS release with the poppy. (One of my favorite default desktop pictures.)

macOS

iOS and iPadOS

Other Platforms

Updates: Setup Manager and utiluti

Setup Manager 1.3

We have released Setup Manager 1.3 today. You can see the release notes and download the pkg installer here.

Most of the changes to Setup Manager in the update do not change the workflow directly. The focus for this update was to improve logging and information provided for trouble-shooting.

With the 1.3 update, Setup Manager provides richer logging information. You will find some entries in the Setup Manager log that were not initiated by the Setup Manager workflow, but are still very relevant to troubleshooting the enrollment workflow. You can see all installation packages that are installed during the enrollment, as well as network changes. This allows an admin to see when managed App Store installations or other installations initiated from the MDM or Jamf App Installers are happening in the enrollment workflow.

These can be very helpful to determine what might be delaying or interrupting certain other installations.

When we started building the “enrollment tool we wanted to use ourselves” more than two years ago, we chose to build a full application, rather than a script-based solution which remote controls some interface. One of the immediate benefits is that we could make the user interface richer and more specialized. Localizing the app into different languages was easier, too. Setup Manager adds Polish localization, bringing the total number of languages to ten!

(We use the help of volunteers from the community to localize to other languages, if you want to help localize Setup Manager into your language, please contact me.)

There was another goal, which took a bit longer to realize.

Swift apps allow us to dive deeper into the capabilities and information available in the operating system. A full blown app is also more capable at analyzing and displaying multiple sources of information at the same time. For example, Setup Manager will display a big warning when the battery level drops below a critical threshold.

These kinds of workflows and user interfaces would be nearly impossible or, at the very least, extremely complex to build and maintain with shell scripts. In this case, Setup Manager is monitoring and parsing other log files and summarizing them down to some important events in the background, while it is working through its main purpose of running through the action list from the profile.

This feature will not be seen by most users or even techs who are sitting in front of the Mac, waiting for the base installation to finish. But when you are trouble shooting problems during your enrollment workflow, these extra log entries can be very insightful. Even during testing, it unveiled some surprises in our testing environments.

We hope you like the new features. But, we are also not done yet and have plenty more ideas planned for Setup Manager!

utiluti 1.2

Since we are talking updates, I have also released an update to my CLI tool to set default apps for urls and file types (uniform type identifiers/UTI). utiluti 1.2 adds a manage verb which can read a list of default app assignments from plist files or a configuration profile. You can see the documentation for the new manage verb here and download the latest pkg installer here.

This allows you to define lists of default apps and push them with your device management system. Then you can run utiluti from a script in the same management system. This should greatly simplify managing default apps.

Note, that while you can set the default browser with utiluti, whether you are using the manage option or not, the system will prompt the user to confirm the new default browser. For this use case, you will want to put the utiluti command in a context where the user is prepared and ready for that extra dialog (such as a Self Service app). There are other tools, such as Graham Gilbert’s make-default CLI tool, which bypass the system dialog. In my experience, tools like this work well in fairly clean setup and require a logout or reboot after the change. This might fit your workflow, but you need to test.

I hope utiluti will find a place in your MacAdmin’s toolbox!

New tool updated: utiluti v1.1

A few weeks back I introduced a new tool: utiluti, which sets default apps for url schemes and file types.

Not standing still, I have updated the tool with new verbs to list url schemes and file types that a given app can handle, as well as inspect and set default apps set for specific files.

You can see the Readme and download the latest release on the GitHub repo.

Installomator v10.8

Further chipping away at the backlog of new and updated with merged 200 PRs merged or closed.

The new PR templates and automations are proving to be a big help! Many thanks Bart for working on these and all the maintainers for staying on top of most things.

This release brings Installomator to 1025 (!) labels!

Many thanks to all the contributors, this tool wouldn’t exist without you!

You can find the detailed release notes and the pkg on the repo!

New tool: utiluti sets default apps

A while back I wrote a post on the Jamf Tech Thoughts blog about managing the default browser on macOS. In that post I introduced a script using JXA to set the default application for a given url scheme. (like http, mailto, ssh etc.) The beauty of using JXA/osascript is that it doesn’t require the installation of an extra tool.

However, there was a follow-up comment asking about default apps for file types, i.e. which app will open PDF files or files with the .sh file extension. Unfortunately, Apple has not bridged those AppKit APIs to AppleScript/JXA, which means it is not possible to use them in a script without dependencies.

Back then, I started working on a command line tool which uses those APIs. I didn’t really plan to publish it, since there were established tools, like duti, cdef and SwiftDefaultApp that provided the functionality. It was a chance to experiment and learn more about Swift Argument Parser. Then life and work happened and other projects required more attention.

A recent discussion on the Mac Admins Slack reminded me of this. Also, none of the above mentioned tools have been updated in the past years. As far as I can tell, none of them have been compiled for the Apple silicon platform. They don’t provide installation pkgs either, which complicates their distribution in a managed deployment.

So, I dusted off the project, cleaned it up a bit, and added a ReadMe file and a signed and notarized installation pkg. The tool is called utiluti (I am a bit proud of that name).

You can use utiluti to set the default app for an url scheme:

$ utiluti url set mailto com.microsoft.Outlook
set com.microsoft.Outlook for mailto

or to set the default app to open a uniform type identifier (UTI):

$ utiluti type set public.plain-text com.barebones.bbedit
set com.barebones.bbedit for public.plain-text

There are bunch of other options, you can read the details in the ReadMe or in the command line with utiluti help.

The functionality is quite basic, but please provide feedback if there are features you’d like to have added.

Installomator v10.7

Chipping away at the backlog of PRs and issue, we have released a new version of Installomator today.

Main focus was on releasing a whole bunch of new and updated labels. But the maintainer team has also started work on implementing to templates for issues and PRs and some automation for testing. This should help a lot with the effort to keep up with new issues and PRs going forward.

Many thanks to all the contributors and maintainers for the hard work that went into this!

You can find [the detailed release notes and the downloads on the repo!](https://github.com/Installomator/Installomator/releases/tag/v10.7)