Why Won't launchd Run My Script?
Perhaps you’ve had this problem before: you’ve written a fun little Flask app that runs fine in Terminal, and now you want make it a daemon.
Being a good Mac power user, you’ve written up a LaunchAgent and stashed it in ~/Library/LaunchAgents
for launchd to find. Then, as soon as you try to launchctl load
your new service, it completely zonks out.
You’ve already chomod
‘ed everything that needs chomod
ding but all you’re getting back are vague “operation not permitted” errors. What the heck?
May I suggest that your script is failing because of TCC (Transparency, Consent and Control). That’s the macOS subsystem (introduced in Mavericks) that controls access to the microphone, camera, Documents folder, and so on. If you’ve ever seen a dialogue like this, you’ve met TCC:
My homemade LaunchAgents often break because they need to read and write data from my Documents folder, which TCC dutifully blocks because I’ve never actually granted the required permission. The scripts work in Terminal because I already gave Terminal access to my Documents folder years ago.
TCC doesn’t offer the same nag-o-gram mechanism for granting permissions to our scripts, so we need to work around it.
Solution 1: fdautil
launchd
is not the most user-friendly piece of software in the world, so I use a GUI wrapper called LaunchControl1. The developers of LaunchControl have kindly provided a wrapper utility called fdautil
that provides full disk access to any script of your choosing. It’s bit like borrowing a friend’s Costco membership.
LaunchControl users are prompted to grant full disk access to fdautil
when they first install the app. Then, they can insert this snippet into any LaunchAgent of their choosing:
/usr/local/bin/fdautil exec /path/to/my/script.sh
This method is crude, but effective. I can think of two issues that might hold you back from using it…
- Security: giving full disk access to all of my random little scripts is probably unwise
- Cost: LaunchControl (and therefore fdautil) costs money
Solution 2: Script Editor
Script Editor has a little-used feature where you can create little Mac apps from your AppleScripts. If you create a wrapper app around your daemon’s launch command, the applet can then interact with TCC to get the permissions it needs, including GUI prompts for filesystem access.
Just create an app with this template…
do shell script "sh /path/to/my/script.sh"
Then click to File > Export… and select “Application” as the file format.
When putting together your new LaunchAgent, you’ll need to directly reference the applet
executable embedded within the new Wrapper.app
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>science.nunn.wrapperdemo</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/project/Wrapper.app/Contents/MacOS/applet</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/path/to/project/science.nunn.wrapperdemo.stderr</string>
<key>StandardOutPath</key>
<string>/path/to/project/science.nunn.wrapperdemo.stdout</string>
<key>WorkingDirectory</key>
<string>/path/to/project/</string>
</dict>
</plist>
When your LaunchAgent runs Wrapper.app
for the first time, you’ll be prompted by TCC to grant any permissions it needs:
Et voilà. Your script is free to roam your Documents folder as much as it likes.
Solution 3: Keyboard Maestro
When you need a daemon (e.g. a web app), launchd
is your friend. When you just need to run a script every couple of hours, it’s hard to beat the simplicity, reliability and maintainability of Keyboard Maestro:
TCC will annoy you (once) for any permissions Keyboard Maestro needs to get the job done, and then you’re set for life. The Russians used a pencil, they say2.
Further Reading
- Apple’s current documentation, which fails to warn the user about any of the above-mentioned TCC headaches
- Apple’s older and much more comprehensive documentation which suffers from the same issue as its modern counterpart
- The nitpicky difference between daemons and services
- The manual entry for launchd