Your first Mac shell script (zsh), and running it automatically with launchd
Write a script in Terminal, make it executable, put it on your PATH, and schedule it with a LaunchAgent, the Mac's built-in alternative to cron.
Apple’s support page says Macs have used zsh as the default login and interactive shell since macOS 10.15 Catalina; bash was the default in 10.14 and earlier. Most simple scripts work in either.
1. Write it
In Terminal:
mkdir -p ~/Scripts
nano ~/Scripts/hello.sh
Paste this, then press Ctrl-O, Return, Ctrl-X to save and quit:
#!/bin/zsh
# hello.sh - greet, show free disk space, list the 5 newest Downloads
echo "Hello from zsh! It's $(date '+%A %d %B, %H:%M')."
df -h / | tail -1 | awk '{print "Free space on the startup disk: " $4}'
ls -t ~/Downloads | head -5
The first line, #!/bin/zsh, is the shebang. It tells macOS which program should run the file.
2. Make it executable and run it
chmod +x ~/Scripts/hello.sh
~/Scripts/hello.sh
chmod +x marks the file as runnable. Without it you’ll get permission denied.
To run it by name from anywhere, add the folder to your PATH:
echo 'export PATH="$HOME/Scripts:$PATH"' >> ~/.zshrc
source ~/.zshrc
hello.sh
3. Double-clickable version
Rename a copy to hello.command. Double-clicking a .command file in Finder opens Terminal and runs it. It still needs chmod +x.
4. Run it automatically with launchd
macOS uses launchd for scheduled and background jobs. Apple’s guide describes where job files go:
| Folder | What goes there |
|---|---|
~/Library/LaunchAgents | Agents for your account only. Apple describes a user agent as specific to one logged-in user, running only while that user is logged in. This is what you usually want. |
/Library/LaunchAgents | Agents loaded for every user who logs in. Files here must be owned by root, so you need admin rights to install them. |
/Library/LaunchDaemons | System-wide daemons, loaded when the Mac starts up rather than at login. Also root-owned. |
Create ~/Library/LaunchAgents/com.example.hello.plist:
<?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>Label</key><string>com.example.hello</string>
<key>ProgramArguments</key>
<array><string>/Users/YOUR-USER/Scripts/hello.sh</string></array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<key>StandardOutPath</key><string>/tmp/hello.log</string>
<key>StandardErrorPath</key><string>/tmp/hello.err</string>
</dict>
</plist>
Then load it and test it:
plutil -lint ~/Library/LaunchAgents/com.example.hello.plist # checks the file for typos
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.hello.plist
launchctl kickstart gui/$(id -u)/com.example.hello # run it right now
cat /tmp/hello.log
To unload it: launchctl bootout gui/$(id -u)/com.example.hello.
Other triggers (the first three are covered in Apple’s guide; all four are in man launchd.plist):
StartInterval: every N seconds.StartCalendarInterval: a cron-style time. Any key you leave out means “every”.WatchPaths: run whenever a file or folder changes.RunAtLoad: run when the job is loaded, i.e. at login for an agent.
Gotcha: launchd doesn’t load your
~/.zshrc, so your PATH and aliases aren’t there. Use full paths inside scheduled scripts (/usr/bin/rsync,/opt/homebrew/bin/...). If a script needs files in protected folders like Desktop or Documents, macOS may block it until you grant access under System Settings → Privacy & Security.
Next steps
- A full real-world LaunchAgent that keeps NAS shares mounted: Auto-mount NAS shares on a Mac.
- More shell, AppleScript and Shortcuts links: Scripting & Automation.
Sources
- Use zsh as the default shell on your Mac — by Apple Support
- Creating Launch Daemons and Agents — by Apple Developer Documentation (archive)