Terminal Secure Keyboard Entry and sudo with Touch ID on macOS

Posted on December 2, 2023 by alanysiu

At one point or another, you may have discovered that you can enable Touch ID for sudo in the macOS terminal. You used to have to manually edit the /etc/pam.d/sudo file after every macOS update to re-enable it, but now you can keep it enabled permanently.

When you have Touch ID enabled for sudo, you get this cool little prompt for your fingerprint when you use a sudo command:

But if you have Secure Keyboard Entry enabled…

… you may notice that the Touch ID prompt shows up but is greyed out…

… and you’ll have to click on it with your mouse in order for Touch ID to work for sudo

So, if that annoys you, you may want to uncheck Secure Keyboard Entry (either temporarily or permanently).


launchctl “new” subcommand basics for macOS

Posted on November 15, 2023 by alanysiu


For services running in the background (or foreground), macOS uses launchd (think cron jobs on Linux or scheduled tasks on Windows).

Legacy Subcommands

If you’ve been managing Macs for a while, you may be familiar with a particular syntax for loading launchd.

For launch agents (usually run as user), you may typically have launched them with

launchctl load /Library/LaunchAgents/com.domainname.app.plist

For launch daemons (usually run as root), you may typically have launched them with

sudo launchctl load /Library/LaunchDaemons/com.domainname.app.plist

If you read the manual for launchctl (man launchctl), you’ll see load and unload listed as legacy subcommands:

Legacy subcommands should still work. I don’t believe they’re fully deprecated (at least as of this writing).

“New” subcommands

But there are now subcommands that aren’t legacy:

For a super-comprehensive breakdown on what all these “new” subcommands are, you may want to check out Babo D’s Launchctl 2.0 Syntax post (from 2016!), but I thought I’d just do a quick breakdown here of the basics.

launchctl subcommand basics

Don’t come for me. As I mentioned above, there are far more comprehensive breakdowns of launchctl subcommands. This is just the basics. Not much nuance. Just how do you do it if you’ve been doing it the “legacy” way this whole time.

Listing with launchctl

With legacy subcommands, to get launch agents, you’d run

launchctl list

and that would give you a list of launch agents that are running.

With the “new” subcommands, you would do something like

launchctl print gui/501

Well, that’s assuming your uid is 501, which it may not be. If you want to find your uid, you can use id -u. If you want to find the uid of a specific other user account, you can use id -u username. You could also find the uids of a bunch of users by running dscl . -list /Users UniqueID | grep -v _

With legacy subcommands, to get launch daemons, you’d run

sudo launchctl list

or run launchctl list as root (e.g., via Munki or Jamf).

With the “new” subcommands, you would do

launchctl print system

The nice thing about doing that is you’re specifying system, so it doesn’t matter if you run it as root or as user—you’ll just get the system (root-level) launchd.

With either type of listing type, you won’t just get a list of process IDs and labels (you will still get those under services), but you’ll also get a list of what are called disabled services, which is a bit confusing, because that list will include both disabled and enabled services.

So, for example, for Nudge, you might see something like

0 0 com.github.macadmins.Nudge

under services but then something like

"com.github.macadmins.Nudge" => enabled

under disabled services.

You’ll probably find ways to parse the output, but just be aware there is this note in man launchctl:

IMPORTANT: This output is NOT API in any sense at all. Do NOT
rely on the structure or information emitted for ANY reason. It
may change from release to release without warning.

Launching launch agents

Instead of running

launchctl load /Library/LaunchAgents/com.github.macadmins.Nudge.plist

you’d run something like

launchctl bootstrap gui/501 /Library/LaunchAgents/com.github.macadmins.Nudge.plist

Launching launch daemons

Instead of running

sudo launchctl load /Library/LaunchDaemons/com.github.macadmins.Nudge.logger.plist

you’d run something like

sudo launchctl bootstrap system /Library/LaunchDaemons/com.github.macadmins.Nudge.logger.plist

Obviously, if you’re running this in a script from a root-run management tool (e.g., Munki or Jamf), you wouldn’t preface commands with sudo.

Unloading launch agents

Instead of running

launchctl unload /Library/LaunchAgents/com.github.macadmins.Nudge.plist

you’d run something like

launchctl bootout gui/501/com.github.macadmins.Nudge

Unloading launch daemons

Instead of running

sudo launchctl unload /Library/LaunchDaemons/com.github.macadmins.Nudge.logger.plist

you’d run something like

sudo launchctl bootout system/com.github.macadmins.Nudge.Logger

Reminder: don’t preface with sudo in root-run scripts.

Disabling a launch daemon or agent

Instead of bootout, you would use disable to disable a launch daemon or agent. Example:

sudo launchctl disable system/com.github.macadmins.Nudge.Logger

Just be careful with this. If you disable it, you can’t then just bootstrap it right afterwards:

sudo launchctl bootstrap system /Library/LaunchDaemons/com.github.macadmins.Nudge.logger.plist
Bootstrap failed: 5: Input/output error

First, you want to re-enable it:

sudo launchctl enable system/com.github.macadmins.Nudge.Logger

Then you can bootstrap it:

sudo launchctl bootstrap system /Library/LaunchDaemons/com.github.macadmins.Nudge.logger.plist

That’s all I’ve got for now. As I mentioned earlier, you can read a more comprehensive breakdown at Launchctl 2.0 Syntax.


Using Touch ID for sudo on macOS… even after installing an OS update

Posted on November 8, 2023 by alanysiu

For a while, you were able to use Touch ID for sudo on macOS by editing the /etc/pam.d/sudo file to have a line like

auth sufficient pam_tid.so

In previous macOS versions, that file would get overwritten when you updated (say, from 13.6 to 13.6.1), but in macOS 14 (Sonoma) and supposedly in future versions, you can now have that persist by setting up a /etc/pam.d/sudo_local file with these contents (only three lines, even if it wraps):

# sudo_local: local config file which survives system update and is included for sudo
# uncomment following line to enable Touch ID for sudo
auth sufficient pam_tid.so

And then making sure the /etc/pam.d/sudo file contains this line:

auth include sudo_local

After that, if you update from macOS 14.1 to 14.1.1 or 14.2 or 14.3, you should still be able to use Touch ID for sudo commands without having to manually edit the /etc/pam.d/sudo file every time.


Comparison macOS versions using Python

Posted on October 7, 2023 by alanysiu

At some point in a Python script, you may want to compare macOS versions (or, really, any software versions) to each other.

Back in the day, you could use Python’s LooseVersion from distutils.version, but that’s now deprecated:

>>> from distutils.version import LooseVersion
>>> LooseVersion('14.0.0') > LooseVersion('14.0')
>>> LooseVersion('14.0.0') == LooseVersion('14.0')
>>> LooseVersion('14.0') > LooseVersion('13.5.2')
>>> LooseVersion('13.4.1 (c)') > LooseVersion('13.4.1 (a)')

The non-deprecated Version can run into issues, though, for Rapid Security Responses (thanks to @elios on the MacAdmins Slack for pointing this out):

>>> from packaging.version import Version
>>> Version('14.0.0') > Version('14.0')
>>> Version('14.0.0') == Version('14.0')
>>> Version('14.0') > Version('13.5.2')
>>> Version('13.4.1 (c)') > Version('13.4.1 (a)')
Traceback (most recent call last):
File "<stdin>", line 1, in
File "/Library/ManagedFrameworks/Python/Python3.framework/Versions/3.11/lib/python3.11/site-packages/packaging/version.py", line 197, in __init__
raise InvalidVersion(f"Invalid version: '{version}'")
packaging.version.InvalidVersion: Invalid version: '13.4.1 (c)'

If you have Munki available, Munki’s MunkiLooseVersion works:

>>> import sys
>>> sys.path.insert(0, '/usr/local/munki')
>>> from munkilib.pkgutils import MunkiLooseVersion
>>> MunkiLooseVersion('14.0.0') > MunkiLooseVersion('14.0')
>>> MunkiLooseVersion('14.0.0') == MunkiLooseVersion('14.0')
>>> MunkiLooseVersion('14.0') > MunkiLooseVersion('13.5.2')
>>> MunkiLooseVersion('13.4.1 (c)') > MunkiLooseVersion('13.4.1 (a)')

I would still recommend MunkiLooseVersion if you have it available, as it can handle some fairly complex versioning (even beyond macOS versions).

If you don’t have Munki available, Python’s own built-in comparisons (at least for macOS versions) oddly seem to work better than Version from packaging.version:

>>> '14.0.0' > '14.0'
>>> '14.0.0' == '14.0'
>>> '14.0' > '13.5.2'
>>> '13.4.1 (c)' > '13.4.1 (a)'


UseAdditionalHttpHeaders preference for MunkiReport 5.8.0

Posted on September 13, 2023 by alanysiu

Extra preference to consider

If you use authorization headers for your Mac clients to communicate with your MunkiReport server and are upgrading from MunkiReport 5.7.1 (which still uses Python 2) to MunkiReport 5.8.0 (which uses Python 3), be aware that there is an additional preference to set if you want your headers to be used: UseAdditionalHttpHeaders

You can see in the 5.7.1 code for reportcommon.py that the UseAdditionalHttpHeaders key isn’t used.

And then you can see in the 5.8.0 code for reportcommon.py that the UseAdditionalHttpHeaders key is now used.

Note about Python3 branch for modules

If you’re updating your MunkiReport modules, you’ll want to use the mr5-python3 branch until MunkiReport 6 comes out:


Special thanks to John Eberle at the MacAdmins Slack for that tip about which module branch to use.


Notes on connecting a Cloud Function to Cloud SQL

Posted on July 24, 2023 by alanysiu


I’m not a Google Cloud Platform expert, and this isn’t a tutorial. I’m just hoping that if people run into the same errors I ran into and search for those exact error messages, that they’ll find this blog post, and maybe it’ll be helpful to them.

What I found

Making the connection

Google’s documentation on connecting a Cloud Function to Cloud SQL isn’t super straightforward. When I tried to implement their examples, I came across all sorts of errors like:

sqlalchemy.exc.InterfaceError: (pg8000.exceptions.InterfaceError) communication error
(Background on this error at: https://sqlalche.me/e/20/rvf5)


File "/workspace/main.py", line 58, in postgres_connect sqlalchemy.engine.url.URL.create(AttributeError: type object 'URL' has no attribute 'create'"

I oddly found that using cloud-sql-proxy would allow the “Cloud” Function to run properly locally (even without a host specified—I guess because the assumption is that the host is, and that’s what the cloud-sql-proxy connects you to?).

I ran into issues connecting with Unix sockets (and it wasn’t because the socket path length was longer than 108 characters, which the documentation already warns about).

Ultimately, (at the urging of a colleague) I ditched trying to use Unix connectors, and I went with the Cloud SQL connector approach outlined in Connect from Cloud Functions, and that worked.

The requirements.txt file needed:


Using the connection

I didn’t know a ton about SQL Alchemy, so I had to do a bit of searching around and trial-and-error to get a useful connection.

So, first of all, you get the pool from the code Google provides you. What do you do with the pool?

I assigned it to a variable called engine, but then I had to run conn = engine.connect() in order to make the connection useful.

Even then, it wasn’t exactly clear how to make an actual SQL query. If you have a SQL query (e.g., SELECT field1, field2, field3 FROM table WHERE somecriteria = somevalue;) and put that into a variable like query, you can’t just run conn.execute(query) on your query, because it will complain about it not being able to figure out the text. So you actually have to use query = sqlalchemy.text(original_query) before you can run something like output = connect.execute(query). After that, the output should be an easy list to iterate through.


How to use Google Apps Script to get data from a connected data sheet

Posted on May 13, 2023 by alanysiu

Regular sheets are straightforward

For a regular Google Sheets spreadsheet, it’s fairly easy to use Google Apps Script to get the data contained on a worksheet:

    var spreadsheet = SpreadsheetApp.getActive();
    var regularsheet = spreadsheet.getSheets()[0];
    var range = regularsheet.getDataRange();
    var values = range.getValues();
    for ( j = 0; j < values.length; j++ ){
        Logger.log(values[j][0] + ': ' + values[j][1]);

The nice thing about the getDataRange() function is that it just automatically grabs all the cells have data in them. You don’t have to specify to start at this column or end at this particular row.

Connected sheets

You can connect a data source as another sheet.

Can’t treat connected sheet as regular sheet

In theory, Google provides a function called asSheet() that supposedly allows you to process a data source sheet as a regular sheet.

That doesn’t work, though.

Get values by column on connected sheet

You can get the connected sheet data by column name. That’s not optimal, but it does appear to work:

    var spreadsheet = SpreadsheetApp.getActive();
    var targetsheet = spreadsheet.getSheets()[0];
    var connectedsheet = spreadsheet.getSheets()[1].asDataSourceSheet();
    var values = connectedsheet.getSheetValues("name");
    for ( j = 0; j < values.length; j++ ){
        Logger.log(String(j) + ": " + values[j]);
        if ( values[j] == "Mary" ){
                  targetsheet.appendRow([j, values[j]]);


Using Munki to “nudge” for Rapid Security Response updates (like 13.3.1 (a))

Posted on May 4, 2023 by alanysiu

For many MacAdmins, Nudge has been an amazing tool for bothering users to update their Macs to the latest patch. Apple threw a wrench in things by the way it implemented the 13.3.1 (a) Rapid Security Response update, so Nudge doesn’t currently (as of this writing) support Rapid Security Response updates.

There would be challenges to the user experience even if Nudge did implement support for Rapid Security Responses (at least the way Apple’s implemented them so far).

First of all, the 13.3.1 (a) update is available for only 13.3.1, so two (disruptive) reboots would be necessary for users on older versions. If you have users on 13.2.1 or 13.3, they’ll have to update to 13.3.1 first (that requires a reboot), and then they’ll have to update again to 13.3.1 (a) (another reboot).

Apart from the reboots being disruptive, they’re also a terrible user experience. Since 13.3.1 (a) won’t appear at all for users on older versions of macOS Ventura, if you Nudge people to update to 13.3.1 (a), they’ll see only 13.3.1, install the update, think they’ve updated, and then get confused as to why they’re being Nudge’d again for 13.3.1 (a).

On top of the user experience being terrible, 13.3.1 (a) isn’t even an actual version. Let’s take a look at how Apple sees this update:

ProductName: macOS
ProductVersion: 13.3.1
ProductVersionExtra: (a)
BuildVersion: 22E772610a

As you can see, the product version is not 13.3.1 (a) but still 13.3.1, and to get to the (a) part, you need to look at this new key, ProductVersionExtra, which is not part of the major OS version (13), the minor OS version (3), or the patch version (1). The “extra” part is its own thing.

If you have Munki, which doesn’t really support Apple updates, because Apple doesn’t properly support script-installing Apple updates, you can’t natively enforce this update. You can’t even stage a full installer, as Apple hasn’t released a full installer for 13.3.1 (a), only for 13.3.1.

You can, however, use a Munki nopkg to act like Nudge to prompt users on 13.3.1 to update to 13.3.1 (a). Here’s a sample Rapid Security Response nopkg (feel free to tweak further if necessary for your environment).


Extracting icons from Assets.car on macOS

Posted on April 24, 2023 by alanysiu


Thanks to Karen Garner for bringing this problem to my attention, and thanks to Mike Lynn for showing the solution. I’m just expanding on the solution a bit using a specific example.


Sometimes, when you look in an app bundle on macOS, you see an .icns file in Contents/Resources, and you can use that or convert it to a .png.

For some apps, instead of an .icns file, you get an Assets.car file instead. There doesn’t appear to be an easy, native-to-macOS, point-and-click way to extract icons from the Assets.car file (even though some third-party utilities exist to do so). However, macOS does have some built-in command-line utilities that can help you get the icons out.


For this example, we’ll look at the System Settings app bundle. First, you can see in the Info.plist file what name the icon is using:

defaults read /System/Applications/System\ Settings.app/Contents/Info CFBundleIconName

That should display something like:


You can then see a bit more detail about what’s in the Assets.car file (including all the image and icon files associated with PrefApp):

assetutil -I /System/Applications/System\ Settings.app/Contents/Resources/Assets.car

Icon file

If you want to get an .icns file, you can specify so:

iconutil -c icns /System/Applications/System\ Settings.app/Contents/Resources/Assets.car PrefApp -o ~/Desktop/System\ Settings.icns

Icon Set

If you want to get an iconset folder, you can specify that instead:

iconutil -c iconset /System/Applications/System\ Settings.app/Contents/Resources/Assets.car PrefApp -o ~/Desktop/System\ Settings.iconset


Testing a local timezone deadline in Nudge

Posted on March 27, 2023 by alanysiu

In the past year, there have been several requests to have Nudge support local time zones instead of UTC:

Turns out, as Kevin M. Cox explains in Nudge deadlines in local timezones, the functionality has been there all along, just previously not well documented or used.

I tested this out myself, and it really is just a matter of changing <date> to <string>.

With a string deadline in my .mobileconfig profile, I still had a one-day grace period (I’m in Pacific time in the U.S.):


With a date deadline in my .mobileconfig profile, the deadline had already passed: