Schedule with launchd

Control environment and output

Give a scheduled job absolute paths, a working directory, and dedicated output files instead of depending on Terminal configuration.

A LaunchAgent does not inherit your .zshrc. Commands that work in Terminal can fail on the schedule because PATH, working directory, or variables differ. This one fact explains most “works when I run it, fails on the schedule” reports.

The PATH is short

The PATH a launchd job receives is this:

/usr/bin:/bin:/usr/sbin:/sbin

No /opt/homebrew/bin. No ~/bin. A script that calls jq or anything else Homebrew installed dies with command not found.

Two fixes. Call tools by absolute path, like /opt/homebrew/bin/jq. Or set PATH explicitly at the top of the script. I prefer the second, because the PATH then lives in version control next to the code that depends on it.

Working directory and output

The working directory isn’t your project folder either. Set it, and give the job somewhere to write:

<key>WorkingDirectory</key>
<string>/Users/flavio</string>
<key>StandardOutPath</key>
<string>/Users/flavio/Library/Logs/screenshot-sorter.log</string>
<key>StandardErrorPath</key>
<string>/Users/flavio/Library/Logs/screenshot-sorter.err.log</string>

The two output paths are your only window into a job that runs with no terminal attached. launchd appends everything the job prints to those files.

One caveat. launchd does not create the directory tree for you. Create the parent directories before loading the job, or the output silently goes nowhere. ~/Library/Logs exists on every Mac, which is one reason I put logs there.

See what the job sees

When a job misbehaves and you suspect the environment, make the job show you:

/usr/bin/env > /Users/flavio/Library/Logs/sorter-env.txt

Put that line at the top of the script. Run the job once. Compare the dump with env in your terminal. The difference is usually the whole answer, and it’s usually PATH.

Keep secrets out

A plist is a plain file on disk. Log files get zipped into bug reports. Neither is a place for a token.

Read secrets at runtime through a protected mechanism. For agents, the user Keychain works, because they run in your session:

token=$(security find-generic-password -s acme-api -w)

And avoid shell tracing. set -x prints every expanded command, secrets included, straight into the error log. If you need it for debugging, add it, look, and remove it before the job goes back on the schedule.

Try this: add the env dump line to your script, load the job in the next lesson, and read the file. Count how many entries from your terminal env are missing.

Lesson completed