Use docklib to manage macOS docks

docklib instead of dockutil

I have a few posts about using dockutil to manage the macOS dock. dockutil is still a valid and working project, but I'm starting to migrate my scripts to docklib instead.

Installing docklib

The installation instructions for docklib say you can put the file in the same directory as the scripts that invoke it or you can put it "in your Python path." I'd recommend just grabbing the docklib .pkg from the releases page or using the AutoPkg docklib recipes to download it. The .pkg puts in /Library/Python/2.7/site-packages/

Using docklib with Outset

docklib can be used in an Outset login-once or login-every script. There is no need to explicitly put in a delay to wait for the initial dock to appear before running your script. There is also no need, if you're specifying a dock (rather than modifying an existing one) to remove the default applications Apple puts on the dock. If you're specifying a dock, just say what you want to add. Use this suggested template:

import os
from docklib import Dock
tech_dock = [
   '/Applications/Managed Software',
dock = Dock()
dock.items['persistent-apps'] = []
for item in tech_dock:
   if os.path.exists(item):
      item = dock.makeDockAppEntry(item)

Checking if an item exists before removing/adding via docklib?

Here's an example of checking for something's existence on the right side of the dock before adding it. To check on the left side, it's a very similar process, except you just replace


For example, this will add Microsoft Word only if it's not in the dock already:

from docklib import Dock
dock = Dock()
if dock.findExistingLabel('Microsoft Word', section='persistent-apps') == -1:
   item = dock.makeDockAppEntry('/Applications/Microsoft')

If you add an item using docklib that already exists in the dock, a second instance of it will be added to the dock, so you definitely should check for the existence of the item first.

However, if you want to remove an item, just use the standard removal procedure:

from docklib import Dock
dock = Dock()
dock.removeDockEntry('Microsoft Word')
If the item isn't in the dock when you try to remove it, docklib won't give any error or warning.

Updating MS Office dock icons from 2011 to 2016 using dockutil

Managing client machines while also giving your users freedom to customize their machines as they want can be a bit tricky. On the one hand, you want to automate things as much as possible so users don't have to be bothered with too many update prompts and other maintenance nuisances. On the other hand, you don't want to automate things in a way that will confuse your users.

Jamie (our Dir. of IT) and I had a discussion about moving people from Microsoft Office 2011 to Microsoft Office 2016 and what that would look like. We didn't want to just uninstall Office 2011 right away, especially since it's the only thing MathType will reliably work with (in February, 2016, Design Science announced compatibility with Office 2016 for Windows, with a note that compatibility with Office 2016 for Mac would be coming "soon"—still hasn't come over a year later, as of this writing). And, even though installing Office 2016 side by side with Office 2011 makes 2016 the default for Office files, we wanted to update the Dock icons, so people would launch Office 2016 applications instead of Office 2011 ones.

These were the situations we thought we'd encounter:

  1. User has MathType installed. If that's the case, we don't want to touch the Dock icons. We have only a handful of MathType users, and most of them have already installed Office 2016 (previously an optional install through Munki's Managed Software Center). Only one user asked about how to change the default application to be Office 2011's instead of Office 2016's.
  2. User has only Office 2016 icons in the Dock. Nothing to do in this scenario, because everything's cool already.
  3. User has a mix of Office 2016 and Office 2011 icons in the Dock. If both Word 2011 and Word 2016 are in the Dock, we're going to assume the user wants it that way, and we aren't going to mess with it. But if Excel 2011 and Excel 2016 are in the Dock but only Word 2011 is in the Dock, we want to switch that up to be Word 2016.
  4. User has only Office 2011 icons in the Dock. If this isn't a MathType user, let's switch these all up for Office 2016.
  5. User has no Office icons in the Dock. Leave it alone. If the user doesn't want shortcuts to Office, don't put any in there.

The tricky thing about changing up Office icons in the Dock is that dockutil goes by name or bundleid to add, and both the name and the bundleid is the same for Office 2011 and Office 2016 applications.

So I wrote up a script that checks based on the dockutil --list output to see if the Dock icon is for 2011 or not. It may not work exactly for your organization, but you can see the logic in there, and it's easily tweakable.

Using an Outset boot-every script to add default applications via Munki

In Bash script to add optional installs for Munki, I introduced a script that uses PlistBuddy to add optional install items to the client machine's SelfServeManifest.

I thought at first I could use that as a boot-once script for Outset, but it seemed the script ran too early (actual first boot) and then didn't actually write the values it should.

As a workaround, I've put the script in as an Outset boot-every with a check to see if one of the optional items is already in the Munki install log. Here's an example:


# See if this has ever run before... have to check the log, because Outset will delete the file once run. We don't want this to re-run if we update the pkg version
alreadyRun=$(cat /Library/Managed\ Installs/Logs/Install.log | grep "Firefox")

if [ -z "$alreadyRun" ]; then

# Self-serve manifest location
manifestLocation='/Library/Managed Installs/manifests/SelfServeManifest'

# PlistBuddy full path

# Add in "optional" default software

# Check to see if the file exists. If it doesn't, you may have to create it with an empty array; otherwise,
if [ ! -f "$manifestLocation" ]; then
sudo "$plistBuddy" -c "Add :managed_installs array" "$manifestLocation"

for packageName in "${optionalDefaults[@]}"
# Check it's not already in there
alreadyExists=$("$plistBuddy" -c "Print: managed_installs" "$manifestLocation" | grep "$packageName")

# Single quote expansion of variables gets messy in bash, so we're going to pre-double-quote the single-quotes on the package name

if [ -z "$alreadyExists" ]; then
sudo "$plistBuddy" -c "Add :managed_installs: string $alteredPackageName" "$manifestLocation"

So this basically checks for Firefox. If Firefox (one of the default optional installs) is in the install log, it won't run again.

Using Dockutil with Outset to set user Docks on a Mac

Many people will find Dock Master a good tool to distributing docks to users. If you would prefer to use Dockutil with Outset and a login-once (or login-every) script, you may run into this issue:
Items are added but not always removed

Workaround #1

kcrawford (developer of Dockutil) has a workaround for this:

# Wait for a default dock
while ! /usr/local/bin/dockutil --list | grep Messages
/bin/sleep 1
# actual dockutil dock modifications here
# /usr/local/bin/dockutil --add ...

I've found that (at least in El Capitan), you can do fractions of seconds in sleep, so

/bin/sleep .25
works, and I'd recommend it. Either way, you may still have to wait a little bit before the Dock refreshes.

Workaround #2

I did quite a bit of experimenting (with the latest Dockutil and the latest El Capitan), and the way I got a login script with Dockutil to work is to

/usr/local/bin/dockutil --remove all
remove everything on the Dock, and then do individual additions
/usr/local/bin/dockutil --add '/Applications/' --no-restart
until you get to the last item and then do
/usr/local/bin/dockutil --add '/Applications/' --no-restart
This will refresh the Dock twice, but that seems to be what works. If you do a --no-restart after the first command, you may not get everything actually removed.

AutoDMG / Outset / Munki bootstrap workflow

I wanted to create a workflow that involved pretty much just imaging a new machine with a thin image and then having the image itself pull updates. Sounds simple, but I had to do quite a bit of experimenting to figure out the exact flow.

What to include with AutoDMG

Include in the AutoDMG-created image only CreateUserPkg (for one default user), Outset (for boot and login scripts), the latest Munki tools, and a special ".pkg" that puts some scripts in place to run at boot.

The special .pkg

In addition to distributing various payloads, it's key that the special .pkg have a postinstall script that runs

sudo touch "$3"/var/db/.AppleSetupDone
This cannot be an Outset script. It has to be part of the AutoDMG-created never-booted image, because if you boot the previously-never-booted image without the .AppleSetupDone file in place, you'll be prompted to do all the Mac setup stuff (e.g., create a user, select the time zone, connect to a wireless network manually) at first boot.

One of the payloads should be a script that goes into the /usr/local/outset/boot-every directory, because Outset won't run boot-once scripts unless there's a network connection by default—you can change the preferences .plist and deploy it, but I find it easier to just use a boot-every script. This script will do several things:

  • Check for a Munki preferences file. If the file exists, self-delete (otherwise the script will run at every boot).
  • Create Munki preferences.
  • Create the Munki bootstrap file.
  • Connect to a wireless network to pull in updates.
  • Reboot after waiting a minute (just to give a little time for the wireless connection to finish).***
*** In real-world testing, if you put in your script to wait one minute before shutting down, it may sometimes take more than one minute for the reboot to happen. In a recent test I did, it took about four minutes from first boot for the next reboot to happen. And then the reboot after that (the one that triggered the Munki bootstrap) took about 90 seconds.

After that, the Munki bootstrap file should take care of any subsequent reboots and updates until the machine is fully updated.

Run scripts at logout using Offset

There is an excellent open source (Apache-licensed) tool called Outset that will run scripts for Mac computers once at boot, every boot, once at login, every login, or on demand. You can add and remove ignored users (users you don't want to run those scripts for). And all you have to do, once Outset is installed, is drop the scripts into the appropriate folders (e.g., /usr/local/outset/boot-every) and make sure they have the right ownership and permissions.

I've been working with chilcote (the original developer of Outset) to see if it made sense to bring logout scripts into Outset. After much discussion and testing, chilcote ultimately decided it was outside the scope of what he wanted for Outset, but he encouraged me to make a spinoff called Offset.

After a lot of tweaking and testing and re-testing, I have a first working version (would still love testers!) of Offset, which will run scripts at logout.

Once Offset is installed, just put your scripts into /usr/local/offset/logout-every and make sure they have the correct ownership (root:wheel) and permissions (755 for scripts, 644 for packages).

Check out the README file for lots more detailed information, including why I ultimately went with the login window launch agent instead of two other methods (login launch agent that runs scripts when interrupted or logout hook).

Using Outset to deploy scripts to Mac clients

Why this guide?

Okay, this isn't terribly groundbreaking stuff for seasoned Mac Admins, but for the beginners and intermediates among us, I'm trying to write the layperson's guide to Outset.

What is Outset?

Outset is basically a set of launch daemons (to run at boot) and launch agents (to run at user login) that will run a bunch of scripts placed in a variety of folders.

Without Outset (or something like it), if you had run-at-boot scripts and run-at-login scripts, you would have to make separate launch daemons and launch agents, respectively, for each script (and then write logging into each script).

With Outset, all you have to do is put the scripts in the right folders, and they'll run without their own individual launch daemons / agents, and their runs will be logged without you scripting the logging for each individual one.

How do you use Outset?

Download the latest Outset release and install the .pkg on a test client.

You should see within the /usr/local/outset directory, a series of folders. Scripts you put in these subfolders...

  • boot-once
  • boot-every
  • login-once
  • login-every
  • on-demand
... will run based on what type of folder you put the scripts in. For example, if you want a script to run every time the computer boots up, put the script in boot-every. If you want a script to run every time a user logs in, put the script in login-every.

Apparently, at one time, the scripts had to have specific extensions in order to be run. Now they can be any extension (or no extension), as long as they're executable. And the permissions on (these root-owned) scripts should be 755 (the permissions on .pkg files should be 644).

You can then use something like Packages to create several .pkg files to distribute out payloads of scripts to those folders. And, if you're using Munki, you can make each .pkg require Outset.

You can find logs in /var/log/outset.log (for boot- scripts) and ~/Library/Logs/outset.log (for login- scripts).