Sam's Website Propeller Hat Icon

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 chomodding 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:

A screenshot of TCC on macOS Sonoma asking if BBEdit can access the Desktop folder.

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.

A screenshot of System Settings on macOS Sonoma showing the "Full Disk Access" preference pane. LaunchControl is shown in the list of apps with Full Disk Access.

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…

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.

A screenshot of System Settings on macOS Sonoma showing the "Full Disk Access" preference pane. LaunchControl is shown in the list of apps with Full Disk Access.

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:

A screenshot of macOS Sonoma showing a TCC prompt. The prompt reads 'Wrapper would like to access files in your Desktop folder.'

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:

A screenshot of a demo Keyboard Maestro wrapper that periodically runs a script file.

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

  1. There’s another popular launchd GUI called Lingon. I’ve never used it, but it looks nice. 

  2. I know, I know.