What's cleaning /tmp on macOS?

Have you ever wondered how /tmp is cleaned on macOS? I did and went on a quick exploration. Here's what I found.

launchd and launch daemons

Launch daemons are background processes that can perform any task we define. And launchd is the macOS system-wide and per-user daemon manager which is started by the kernel during the system boot process. It manages processes, both for the system as a whole and for individual users..

System-wide daemons provided by macOS are stored in /System/Library/LaunchDaemons. And there's a lot of them:

ls -1 /System/Library/LaunchDaemons | wc -l
422

And there's one specifically for cleaning /tmp: /System/Library/LaunchDaemons/com.apple.tmp_cleaner.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.apple.tmp_cleaner</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/libexec/tmp_cleaner</string>
	</array>
	<key>LowPriorityIO</key>
	<true/>
	<key>Nice</key>
	<integer>1</integer>
	<key>StartCalendarInterval</key>
	<dict>
		<key>Hour</key>
		<integer>0</integer>
	</dict>
</dict>
</plist>

So what does this do? Simply put, it runs the script /usr/libexec/tmp_cleaner at a specific time interval (every day at hour 0, meaning midnight), with some settings (Nice = 1, LowPriorityIO = True). Reading the doc, I also note that if the computer is asleep when it should run, the job is postponed to run the next time the computer wakes up (which makes sense since most personal computers are asleep when midnight hits, and this would never run otherwise).

Here are some excerpts from the manual page for launchd.plist to help understand these bits:

StartCalendarInterval <dictionary of integers or array of dictionary of integers>: This optional key causes the job to be started every calendar interval as specified. Missing arguments are considered to be wildcard. The semantics are much like crontab(5). Unlike cron which skips job invocations when the computer is asleep, launchd will start the job the next time the computer wakes up. If multiple intervals transpire before the computer is woken, those events will be coalesced into one event upon wake from sleep.

LowPriorityIO <boolean>. This optional key specifies whether the kernel should consider this daemon to be low priority when doing file system I/O.

Nice <integer>. This optional key specifies what nice(3) value should be applied to the daemon.

tmp_cleaner

Let's have a look at /usr/libexec/tmp_cleaner. Here's mine, from macOS 26.5.2:

#!/bin/sh
#
# Perform temporary directory cleaning so that long-lived systems
# don't end up with excessively old files there.
#

# Configurations
daily_clean_tmps_dirs="/tmp"				# Delete under here
daily_clean_tmps_days="3"				# If not accessed for
daily_clean_tmps_ignore=".X*-lock .X11-unix .ICE-unix .font-unix .XIM-unix"
daily_clean_tmps_ignore="$daily_clean_tmps_ignore quota.user quota.group"
							# Don't delete these

echo "Removing old temporary files:"

set -f noglob
args="-atime +$daily_clean_tmps_days -mtime +$daily_clean_tmps_days"
args="${args} -ctime +$daily_clean_tmps_days"
dargs="-empty -mtime +$daily_clean_tmps_days"
dargs="${dargs} ! -name .vfs_rsrc_streams_*"
args="$args "`echo " ${daily_clean_tmps_ignore% }" |
    sed 's/[ 	][ 	]*/ ! -name /g'`
dargs="$dargs "`echo " ${daily_clean_tmps_ignore% }" |
    sed 's/[ 	][ 	]*/ ! -name /g'`

for dir in $daily_clean_tmps_dirs
do
    [ ."${dir#/}" != ."$dir" -a -d $dir ] && cd $dir && {
    find -dx . -fstype local -type f $args -delete -print
    find -dx . -fstype local ! -name . -type d $dargs -delete -print
    } | sed "s,^\\.,  $dir,"
done
set -f glob

exit 0

This script will find files and directories older than 3 days (atime, ctime, mtime +3) in $daily_clean_tmps_dirs and delete them if they do not match the $daily_clean_tmps_ignore. There are a lot of subtleties about the way the argument strings are built, but this is what it does.

Ok great! So what can we do with that?

Let's say we wanted to keep files in /tmp for a full month. We could edit this file so that it applies to files older than 30 days. We could also make it run on another folder, like ~/tmp/ or something similar that we use for scratch files. And since we already have the launch daemon running for this specific script, we wouldn't have much to do :)

Keep in mind though that macOS updates would overwrite these changes.

While floating around /usr/libexec/, I took a peek at other sh or bash scripts.

List of files containing the sh/bash shebang in /usr/libexec/
sudo rg "#!/bin/bash|#!/bin/sh" /usr/libexec/

/usr/libexec/locate.updatedb
1:#!/bin/sh

/usr/libexec/locate.mklocatedb
1:#!/bin/sh

/usr/libexec/efi-dump-logs
1:#!/bin/bash

/usr/libexec/tmp_cleaner
1:#!/bin/sh

/usr/libexec/gkreport
1:#!/bin/bash

/usr/libexec/upsshutdown
1:#!/bin/sh

/usr/libexec/locate.concatdb
1:#!/bin/sh

/usr/libexec/feedback/sleepwake.sh
1:#!/bin/sh

/usr/libexec/security-checksystem
1:#!/bin/bash

/usr/libexec/makewhatis.local
1:#!/bin/sh

/usr/libexec/postfix/postfixsetup
1:#!/bin/sh

/usr/libexec/postfix/postfix-script
1:#!/bin/sh

/usr/libexec/postfix/mk_postfix_spool.sh
1:#!/bin/sh

/usr/libexec/postfix/postfix-tls-script
1:#!/bin/sh

/usr/libexec/postfix/set_credentials.sh
1:#!/bin/sh

/usr/libexec/postfix/post-install
1:#!/bin/sh

/usr/libexec/postfix/postfix-wrapper
1:#!/bin/sh

References