AppleScript on macOS is a useful tool for pro users and administrators alike. Even though it probably is not (and shouldn’t be) the first tool of choice for many tasks, there are some tasks that AppleScript makes very simple. Because of this it should be a part of your ‘MacAdmin Toolbelt.’
AppleScript’s strength lies in inter-application communication. With AppleEvents (or AppleScript commands) you can often retrieve valuable information from other applications that would be difficult or even impossible, to get any other way. With AppleScript, you may even be able to create and change data in the target applications.
If you are in any way security and privacy minded this should raise your hairs. Up to macOS 10.13 High Sierra, any non-sandboxed app could use AppleScript and AppleEvents to gather all kinds of personal and private data from various script-enabled apps and services. It could even use script-enabled apps like Mail to create and send email in your name.
Since macOS Mojave, the Security and Privacy controls restricts sending and receiving AppleEvents. A given process can only send events to a different process with user approval. Users can manage the inter-application approvals in the Privacy tab of the Security & Privacy preference pane.
MacAdmins have the option of pre-approving inter-application events with a PPPC (Privacy Preferences Policy Control) configuration profile that is pushed from a DEP-enrolled or user-approved MDM.
Privacy approval
You can trigger the security approval from Terminal when you send an event from the shell to another process with osascript
:
> osascript -e 'tell application "Finder" to get POSIX path of ((target of Finder window 1) as alias)'
When you run this command from Terminal, you will likely get this prompt:
You will not get this prompt when you have approved or rejected the Terminal app to send events to this particular target application before. You can check the permissions granted by the user in the Automation section of Privacy tab in the Security & Privacy pane of System Preferences.
For any given source/target application combination, the prompt will only be shown once. When the user approves the privilege (“OK” button), future events will just be allowed.
When the user rejects the connection (“Don’t Allow” button), this event and future events will be rejected without further prompts. The osascript
will fail and the AppleScript will return an error –1743.
> osascript -e 'tell application "Finder" to get POSIX path of ((target of Finder window 1) as alias)'
79:84: execution error: Not authorized to send Apple events to Finder. (-1743)
If you want to get the approval dialogs again, you can reset the state of the source application (Terminal) with the tccutil
command:
> tccutil reset AppleEvents com.apple.Terminal
This will remove the Terminal application and all target applications for it from the Automation (AppleEvents
) area in the Privacy pane and show dialogs for every new request going forward. This can be very useful during testing.
Dealing with rejection
You should write your code in a ways that it fails gracefully when access is not granted. in this case osascript
will return an error:
if ! osascript -e ' tell app "Finder" to return POSIX path of ((target of Finder window 1) as alias)'
then
echo "osascript encountered an error"
exit 1
fi
However, osascript
will return errors for all kind of failures with no easy way to distinguish between them. As an example, the above will also fail when there are no Finder windows open.
If you want to distinguish AppleScript errors, you need to do so in the the AppleScript code:
if ! osascript -s o <<EndOfScript
tell application "Finder"
try
set c to (count of Finder windows)
on error message number -1743
error "Privacy settings prevent access to Finder"
end try
if c is 0 then
return POSIX path of (desktop as alias)
else
return POSIX path of ((target of Finder window 1) as alias)
end if
end tell
EndOfScript
then
echo "osascript failed"
fi
Note: the
-s o
option ofosascript
makes it print AppleScript errors to standard out rather than standard error, which can be useful to find the errors in logs of management systems.Note 2: when you are running
osascript
from management and installation scripts (which run as the root user) you need to run them as the current user to avoid problems.
Avoiding Privacy prompts
So, we know of one way to deal with the privacy prompts. Ideally, you would want to avoid them entirely. While this is not always possible, there are a few strategies that can work.
Don’t send to other Processes
In past versions of Mac OS X (I use this name intentionally, it’s that long ago.), scripts that showed dialogs might not display on the highest window layer. In other words, the dialog was lost behind the currently active windows. To avoid “lost” dialogs, it became best practice to send the display dialog
command (and similar) to a process that had just received an activate
command as well:
tell application "Finder"
activate
display dialog "Hello, World!"
end tell
As an alternative for Finder, the System Events process is often used as well. Jamf MacAdmins often used “Self Service.” This had the added bonus, that the dialog looks as if it comes from the Finder or Self Service, including the bouncing dock icon.
Over time, even though the underlying problem with hidden dialog has been fixed, this practice has persisted. You often even see AppleScript code use this with commands other than user interaction, where it wouldn’t have made sense in the first place. With the privacy restrictions in macOS Mojave, this practice has become actively trouble some, as you are sending the display dialog
(or other) command to a separate process. The process running this script will require approval to send events to “System Events.”
osascript <<EndOfScript
tell application "System Events"
activate
display dialog "Hello, World!"
end tell
EndOfScript
In current versions of macOS, you can just use display dialog
and may other commands without an enclosing tell
block. Since your AppleScript code isn’t sending events to another process, no privacy approval is provided. This code has the same effect as above, but does not trigger an approval request.
osascript <<EndOfScript
display dialog "Hello, World!"
EndOfScript
To determine whether an AppleScript command requires a tell
block, you have to check where it is coming from. Many AppleScript commands that are useful to MacAdmins are contained in the ‘StandardAdditions’ scripting extension. Scripting extensions, as the name implies, extend the functionality of AppleScript without requiring their own process.
The useful commands in the Standard Additions extension include:
- user interaction:
choose file/folder/from list
,display dialog/alert/notification
- file commands:
mount volume
- clipboard commands:
get the clipboard
,set the clipboard to
- sound control:
set volume
,get volume settings
system info
When your script uses only these commands, make sure they are not contained in tell
blocks. This will avoid unnecessary prompts for access approval.
Exempt AppleScript commands
Some AppleScript commands are treated differently and will not trigger privacy approval:
activate
: launch application and/or bring to frontopen
: open a fileopen location
: open a URLquit
: quit the application
For example, this will work without requiring approval:
osascript <<EndOfScript
tell application "Firefox"
open location "https://scriptingosx.com"
end
EndOfScript
Use non-AppleScript alternatives
Sometimes, similar effects to an AppleScript can be achieved through other means. This can be difficult to figure out and implement.
As an example, I used this AppleScript command frequently for setup before Mojave:
tell application "Finder" to set desktop picture to POSIX file "/Library/Desktop Pictures/BoringBlueDesktop.png"
While Mojave was in the beta and it wasn’t really clear if or how the PPPC exemptions could be managed, I looked for a different means. I discovered Cocoa functions to read and change the desktop picture without triggering PPPC, and built a small command line tool out of that: desktoppr
.
The downside of this approach is that you know have to install and/or manage a command line tool on the clients where you want to use it. There are different strategies for this, but it is extra effort compared to “just” running an AppleScript.
Build PPPC profiles to pre-approve AppleEvents
Even after you have considered the above options to avoid sending AppleEvents to another process, there will still be several situations where it is necessary. For situations where a MacAdmin needs to run a script on several dozens, hundreds, or even thousands of Macs, user-approval is simply not a feasible option.
MacAdmins can pre-approve AppleEvents (and most other privacy areas) between certain processes with a Privacy Preferences Policy Control (PPPC) configuration profile. PPPC profiles can only be managed when pushed from a user-approved or automatically enrolled MDM.
You can build such a profile manually, but it is much easier to use a tool to build these:
Your MDM solution might have a specific tool or web interface for this, consult the documentation or ask you vendor.
There is one big requirement here, though: only applications and tools that are signed with a valid Apple Developer ID can be pre-approved this way, as the signature is used to identify and verify the binary.
Determining the process that needs approval
While you can sign shell scripts and other scripts this is often not necessary. As we have seen earlier, when we ran our script from Terminal, it wasn’t the script that requested approval but the Terminal application. When your scripts run from a management system or another tool, it may not be easy to determine which process exactly needs approval.
The most practical approach to determine this, is to log the output of the ’Transparency, Consent, and Control” system (tcc) and look which process is sending the requests.
First, either use a clean test system, or reset the approvals for the processes that you suspect may be involved with tccutil
.
Then open a separate Terminal window and run this command which will show a stream of log entries from the tcc process:
> log stream --debug --predicate 'subsystem == "com.apple.TCC" AND eventMessage BEGINSWITH "AttributionChain"'
There will be a lot of noise in this output.
Then run the script in question, the way you are planning to run it during deployment. If you are planning to run the script from a management system, then do that right now. You will get a lot output in the stream above.
Even when you don’t have a good idea what the parent process is going to be, you can filter the output for osascript
since this is usually the intermediary tool used.
In my example I found several entries similar to this:
0 tccd: [com.apple.TCC:access] AttributionChain: RESP:{ID: com.barebones.bbedit, PID[1179], auid: 501, euid: 501, responsible path: '/Applications/BBEdit.app/Contents/MacOS/BBEdit', binary path: '/Applications/BBEdit.app/Contents/MacOS/BBEdit'}, ACC:{ID: com.apple.osascript, PID[18756], auid: 501, euid: 501, binary path: '/usr/bin/osascript'}, REQ:{ID: com.apple.appleeventsd, PID[577], auid: 55, euid: 55, binary path: '/System/Library/CoreServices/appleeventsd'}
The important information here is the responsible path
which give me the binary and the enclosing application that tcc considers ‘responsible.’ This is the application you need to approve.
When you are running your scripts from a management system, your MDM vendor/provider should already have documentation for this, to save you all this hassle.
With all this information, you can build the PPPC profile with one of the above tools, upload it to your MDM and push it to the clients before the deployment scripts run.
Conclusion
While the added privacy around AppleEvents is welcome, it does add several hurdles to automated administration workflows.
There are some strategies you can use to avoid AppleScripts triggering the privacy controls. When these are not sufficient, you have to build a PPPC profile to pre-approve the parent process.