Recently I needed to perform a time-honored ritual of the modern Mac client systems engineer: customizing the items that would appear in the macOS Dock when users first log in to a newly-provisioned Mac. Numerous examples and resources exist on the topic, and I’m the maintainer of a framework for performing the task. Should be a walk in the park, right?
Wrong!
I stumbled onto a couple interesting roadblocks, and I might not be the first or last to do so. To help others (and my future self) maximize the resilience and longevity of their own docklib scripts, I’ll share how I overcame these roadblocks, and how I think about creating Dock scripts in general.
Python knowledge helpful but not required
In this post I'll use docklib to write a Dock configuration script in Python. I'll also include comments that can help you follow along if you aren't familiar with Python. (I often find reading other people's scripts quite helpful when learning a language.)
Contents
I acknowledge that the full process of managing your fleet’s Dock configuration includes some things that aren’t covered by this post. For example, I won’t go into details here on deploying Outset or a Python runtime, nor will I cover how to deploy your Dock script using Munki or Jamf. This post focuses on the design and resilience of your Dock script itself.
- Define the goal
- Select a tool
- Create a test environment
- Specify Dock contents
- Skip missing apps
- Avoid monolingual assumptions
- Define the scope and conditions
- Consider user context
- Enable idempotence
- Prevent race conditions
- Make backups
- Simplify troubleshooting
- Tying it all together
Define the goal
Before diving into coding a Dock script, one question should always be answered first: Will your management of Dock configurations provide a clear benefit to your users? If the answer to this question is no, then stop here. (Congratulations, you’ve just saved yourself and your colleagues a fair bit of time.)
Apple’s selection of items included in default macOS Docks is far from unreasonable. All the basics are included: a good web browser, many productivity apps, media storage and entertainment, system settings, and avenues for finding more apps. When choosing to manage the Dock (or any other manageable setting), IT administrators should be confident they are improving the user experience, not simply codifying their own personal preferences.
The most persuasive arguments I’ve seen to justify Dock scripts are:
- minimizing new employee toil on day one by making it easy to find essential apps
- reducing confusion and support calls by gently discouraging use of unsupported built-in apps
Once you’re confident that managing the Dock is the right way forward, then it’s time to consider which tool you’ll use.
Select a tool
At its most basic, the Dock configuration file is just a property list (plist), and macOS contains several built-in tools meant to create and modify plist files (including but not limited to plutil
, defaults
, and PlistBuddy
). However, the Dock plist in particular contains complex nested data structures that benefit from a layer of abstraction when handling programmatically. Open-source tools have been created to provide this layer of abstraction.
One such tool many Mac admins (including myself) have relied on for years is dockutil, which provides shell-like commands for Dock management.
For Mac admins who do most of their scripting in Python, the docklib module aims to be a familiar, flexible, and Pythonic way to express the desired state of the Dock. For the purposes of this exercise I’ll be using Python/docklib, but all of the concepts below could be converted for shell/dockutil.
It’s also possible to manage the Dock configuration using MDM profiles or configuration management tools like Chef, but I won’t go into those methods here.
Create a test environment
Before you start creating your script, it’s a good idea to create an environment you can use to test incrementally and iterate your changes in isolation. While it’s certainly possible to do this testing on your daily driver Mac, you may find that using a virtual machine or test Mac gives you extra confidence and allows you to replicate the first login experience more closely.
You’ll need to ensure your test Mac has Python 3 and the docklib module installed. Numerous ways exist to meet this requirement, but my go-to advice for most admins is to install the MacAdmins Python “recommended” package, which includes docklib. With this package installed on your test Mac, you can use the symlink at /usr/local/bin/managed_python3
as your Python 3 interpreter.
Let’s run through a rudimentary test of our ability to back up, make a Dock change, and restore.
-
Back up your test Mac’s Dock configuration with this Terminal command:
cp ~/Library/Preferences/com.apple.dock.plist /tmp/com.apple.dock.backup.plist
-
Create a bare-bones Python script with the following contents (adjust your interpreter path, if you aren’t using MacAdmins Python). For convenience I suggest saving the file to
~/Desktop/dock_script.py
.1 2 3 4 5 6 7
#!/usr/local/bin/managed_python3 from docklib import Dock dock = Dock() item = dock.makeDockAppEntry("/System/Applications/Chess.app") dock.items["persistent-apps"].append(item) dock.save()
-
Make the script executable:
chmod +x ~/Desktop/dock_script.py
-
Run the script:
~/Desktop/dock_script.py
-
Verify the Chess app was successfully added to your Dock.
-
Revert the Dock configuration and relaunch the Dock:
cp /tmp/com.apple.dock.backup.plist ~/Library/Preferences/com.apple.dock.plist killall cfprefsd Dock
-
Verify your Dock config is back to its original state, without the Chess app.
If that worked as expected, you can use steps 4 through 7 above to repeatedly test your Dock script as you iterate and improve it, restoring your Dock to its previous state each time.
Now you’re ready to dive into the script itself.
Specify Dock contents
The most obvious question: What items do you want in your users’ initial Dock? Digging into your answer’s details can reveal a surprising amount of nuance. How you define the Dock contents will determine how static or dynamic your script is and how future macOS changes are handled.
Example: Static list of apps
On the static end of the spectrum, you can define a comprehensive list of the apps you want in the Dock (desired_apps
below). The primary benefit of this method is predictability. You’ll get the apps you specify, in the order you specify — and nothing else.
|
|
Only persistent-apps
is defined above, but you can define a list of persistent-others
too if desired.
Example: Dynamic adds, removes, and replacements
On the dynamic end of the spectrum would be a script that defines the granular changes needed to bring the Dock to your desired config, rather than defining the config wholesale. The example below uses a dictionary (app_changes
) to define the specific additions, removals, and replacements needed:
|
|
Although you’re giving up some control of the items’ order, the main advantage here is that you’re only making the changes you need. Things you don’t care about in the default Dock are left alone rather than wiped out.
To that end: Do you want Apple’s newly featured apps to be easily discoverable by your users? New major versions of macOS often have new featured applications in the Dock. (Previous examples of such apps have been News, Podcasts, Maps, and TV.) The dynamic example above will allow these featured applications to remain in the Dock untouched.
Dock fixup plist
The once-per-user adjustment to the Dock that occurs when logging in to a newly installed or upgraded major macOS version is called a fixup. The plist file that defines the fixup items for the current macOS version can be found here:/System/Library/CoreServices/Dock.app/Contents/Resources/com.apple.dockfixup.plist
Skip missing apps
Another question: Will all these apps be installed by the time your Dock script runs? Usually, the answer is yes, especially if your provisioning process uses Munki’s bootstrap mode or a DEPNotify workflow to trigger your initial software installations prior to first login.
But if the apps don’t land on disk for some reason, you may end up with question marks in the Dock for any missing items. These question marks will fix themselves if the referenced app is subsequently installed, and the icon will update when clicked (or when the Dock restarts via a logout, restart, or killall Dock
command) — but that’s not a great user experience.
If there’s any doubt as to whether your defined apps will be installed at runtime, you can add some resilience to your script by using os.path.isdir
to check for the existence of each app and only add the ones that exist on disk, shown in lines 22-23 below.
Example: Static list of installed apps
|
|
For extra Python flavor, that for
loop can be compressed into a list comprehension, as long as you don’t find those unreadable:
|
|
Avoid monolingual assumptions
Another very important consideration: What language will your users be using when your Dock script runs? During development of a recent Dock script, a colleague made me aware of some English-centric assumptions that I had baked into my script (which were also present in docklib itself).
Specifically: using a Dock item’s “label” to perform find/replace/remove operations will be unreliable if the logged-in user isn’t using the language you’re writing for. To illustrate, here’s the Messages app label in Big Sur when Spanish is selected as the system language:
If we take a look at the (truncated) plist data available for the Messages Dock item, we can see that although the label has been localized for the selected language, the filesystem path (represented by _CFURLString
) remains unchanged:
|
|
Consequently, I’ve made some changes to docklib as of version 1.3.0 that help address this. The bottom line: functions that previously depended solely on item labels now (by default) take _CFURLString
into account first.
If you’re delving into the persistent-apps
items yourself rather than using the docklib functions, just remember not to rely on item["tile-data"]["file-label"]
.
Define the scope and conditions
Next: What machines/users are in-scope for your Dock script? Your endpoint management or MDM tool can likely be used to target a specific subset of Macs, or Macs at a specific point in their lifecycle. Additionally, if you have a central directory system you can also leverage identity data for scoping.
Regardless of how the scope is determined by endpoint management tools, I like to build some basic scope limitations into the script itself. Even though this creates some redundancy, the habit has proven useful in limiting damage from upstream misconfigurations.
Example: Limit to certain hostname patterns
If your endpoint management tool applies a predictable hostname scheme to provisioned Macs, it’s possible to leverage this information to elect in or out of the Dock configuration at script runtime. Here’s one way to do that:
|
|
Example: Conditional Dock items based on hostname
Do you need to apply different Docks to specific sets of Macs? Creating two or more slightly-different Dock scripts and scoping them “just so” on your endpoint management tool can be tedious and difficult to audit. An alternative (or complementary) solution would be to build the conditions into the script’s logic.
Building on the example above, here’s a script that would apply Docks based on computer hostname:
|
|
Example: Conditional Dock items based on username
Do you need to apply different Docks to specific users? Although the practice is falling out of favor for very good reasons, many organizations still maintain a local administrator account for IT support technicians. Such an account might benefit from troubleshooting tools like Activity Monitor, Disk Utility, and Terminal in its Dock. Similar customizations may be beneficial for other single-purpose logins like software builders, audio-video, and wall dashboards.
This example applies different Dock configurations depending on the current user:
|
|
Consider user context
Speaking of logged-in users: How will I ensure my script executes in the proper context? Scripts that modify the Dock typically need to run in the context of the logged-in user, but most endpoint management systems run scripts in root context by default.
The details of executing Dock scripts in user context are outside the scope of this post, but three possible methods to evaluate include:
- Outset, a tool purpose-built for running scripts at login in user context
- Leverage
sudo
/launchctl
(see Armin Briegel’s post “Running a Command as Another User”) - Create and deploy your own LaunchAgent that triggers your Dock script
Enable idempotence
Do you need your script to be able to run multiple times without adverse effects? Depending on the method you use to deploy your script, you may need to enable your script to run at every login rather than just the first or next login for each user.
When writing Dock scripts I aim to make them as idempotent as possible, for two main reasons. First, it’s simpler to create a script capable of running at every login, versus creating a robust solution for tracking which users have already run the Dock script. Second, I just sleep better at night if I know the script I’m deploying is unlikely to result in any user configurations being overwritten.
In the past, I would simulate idempotence by having the script use the presence of a “flag” file or a preference key to determine whether to proceed with making changes. However, I encountered various problems with this approach and found it cumbersome to test effectively.
Recently I’ve switched to a more thorough introspection of the current Dock contents to determine whether it’s customized or still unmodified from the macOS default, as seen in the example below.
Example: Only modify uncustomized Docks
This script stores a list of all the apps that have been present in modern macOS default Docks. If the current Dock consists solely of items in that list, the Dock is probably safe to alter in my estimation.
|
|
Of course, some holes exist in this logic: What if somebody genuinely only uses Apple’s first-party apps for everything, and their customized Dock is very close to the default? What if somebody removes every app from their Dock, preferring it to be empty? These edge cases can be handled in the code if desired, but so far I’ve chosen not to.
Additionally, the above approach requires a bit of maintenance: administrators to pay attention to new macOS releases and add new default apps to the apple_default_apps
list as needed.
I’d like to call attention to lines 25-28 of the above code for a moment:
|
|
I’m taking my own advice here by not relying on the item’s label as an accurate point for comparison to our “Apple default apps” list. This allows the is_default
function to work as expected regardless of the user’s selected language.
Prevent race conditions
I’ve occasionally encountered a race condition with docklib scripts wherein the script executes before the Dock itself has launched. On a user’s first login, this results in a situation where docklib tries to read a preference that doesn’t yet exist, causing the script to fail.
The way dockutil worked around this issue (when it was written in Python) was by waiting for the mod-count
of the dock to be greater than 1. Docklib takes a more hands-off approach, choosing to leave this particular workaround up to admins’ discretion instead.
I prefer to build in a loop that waits for the Dock process to run before continuing.
|
|
Others have come up with creative solutions to this issue as well:
- Waiting for both the Finder app and the Dock app, as shown here
- Using AppKit’s
NSRunningApplication
to perform the check, as shown here - Using
killall -s
, a “no-signal” kill command, to perform the check, as shown here
No matter which method you use, the end result should be that the Dock modifications are more reliable, especially on slower Macs where the Dock may take a few seconds to launch.
Make backups
Is there any chance you’d need to revert the changes your script makes to users’ Docks? To be safe, you can implement a function that performs a backup of your Dock plist prior to saving a new one, like so:
|
|
If a restore is needed, the appropriate plist could be moved from ~/Library/PretendCo/backup/com.apple.dock (<datestamp>).plist
to ~/Library/Preferences/com.apple.dock.plist
.
Simplify troubleshooting
In all the examples above, I’ve tried to consistently leave comments that explain what each section of code is intended to do. You’d be well-served to do the same — your future self will thank you, not to mention your colleagues who may need to tweak your script down the road.
For more complex scripts that include functions for performing specific tasks, include a docstring to explain what each function does.
In addition to comments and docstrings, log output can be tremendously useful. One option is to create a function for generating log messages with timestamps, as shown below. Call this function whenever the script performs a notable action.
|
|
Note that Python contains a logging
module that you can leverage if you prefer. This function allows you to set the level of a message, format output consistently, and other conveniences.
|
|
If you use a custom LaunchAgent to trigger your Dock script, you can define the agent’s StandardErrorPath
and StandardOutPath
to determine where on disk this output is stored. (Be sure the user running the script has permission to write to the output location.) If you’re using Outset, the output for user-context scripts is stored in ~/Library/Logs/outset.log
.
Tying it all together
At last, here’s a fully-featured example docklib script that incorporates many of the resilience tips demonstrated above. Feel free to use this script as a launching-off point for your own Dock customization adventures.
Once your script is working as expected on your test Mac, the next step would be to test and deploy it (along with any required frameworks like Outset and Python 3) on your endpoint Macs. I’ve written another post that describes that process.