- The problem. Cleanup rules like “not opened in 90 days” trust a file’s access time. On a Mac, an ordinary read() is recorded only once after each write.
- What we found. A memory-mapped read moves the date every time. On one Mac, 90.2% of 86.97 GB of files would record no read() at all.
- What we built. Mint stopped moving access dates itself in 1.0.80, and now lists a cold folder only when it can show that nobody uses it.
A file’s old access time is not enough evidence to delete it. In our tests on one APFS Mac, repeated reads could leave that timestamp unchanged, while a different way of reading the same file updated it every time. If you use a “not opened in 90 days” cleanup rule, here is what to check before trusting its list.
Read a file twice on a Mac and ask when it was last accessed. You get the time of the first read.
command accessed clock
echo "some text" > f 00:20:18 00:20:18
cat f 00:20:20 00:20:20
cat f 00:20:20 00:20:22
grep text f 00:20:20 00:20:24
map f 00:20:26 00:20:26
map f 00:20:28 00:20:28
cat f 00:20:28 00:20:30
echo more >> f 00:20:28 00:20:32
cat f 00:20:34 00:20:34
cat f 00:20:34 00:20:36
map is a one-liner that reads the file through a memory mapping. I've put it at the end. Everything else ships with macOS.
The first cat was recorded. The second one and the grep left nothing behind. Both mapped reads were recorded and the cat after them wasn't. Then I appended to the file and the next read counted again. Once.
Two rules
I went in expecting one rule. There are two, because the kernel treats the two ways of reading a file differently.
A read() moves the access time only when the access time is strictly earlier than the modification time. If the two dates are equal to the nanosecond, nothing moves; one nanosecond apart, it does. After that first read the access time is later than the write, so every read() from then on is ignored until something writes to the file again.
An mmap() sets the access time to now on every call, whatever the dates were before. The update happens at the call itself, so touching the pages afterwards changes nothing, and neither does unmapping. Launching a compiled program counts, and so does loading a dylib, since both map the file.
If you know Linux, the first rule will look like relatime. The one difference I can point to is that Linux also refreshes an access time that's more than a day old, and macOS doesn't. I gave a file an access time 400 days in the past, one day after its modification time, and read it. It stayed at 400 days.
I haven't found the mmap() rule written down anywhere. man mount says that without strictatime "some access time updates are skipped for performance reasons", and that's all it says. If it's documented somewhere, please send me the link.
Which programs leave a trace
I took a file whose access time was already later than its last write, read it with one program, and looked at the date.
These left it alone:
cat,grep,rg,head,wc,sort,md5,shasum,gzipcp,dittoandtarreading it as a sourceplutil,PlistBuddy,defaults read,mdls,mdimport- Python's
open().read(), asqlite3query - Swift's
Data(contentsOf:),String(contentsOf:)andNSImage(contentsOf:)
These moved it, every time:
- Python's
mmap, and Swift'sDatawith.alwaysMapped Datawith.mappedIfSafe, at every size down to 1 KB- ImageIO opened on a file URL (
CGImageSourceCreateWithURL), even when you only ask for the image's properties - a Quick Look thumbnail, which is rendered in another process
strings,otool,nmgit logon a packfile- running a compiled binary, and
dlopenof a dylib
A shell script was the odd one out: its first run was recorded and later runs weren't. My guess is that something scans a new executable once, maps it to do that, and after that the shell only reads the file. I didn't chase it.
Some files never record a read
How the file got there matters too.
tar, unzip, rsync -a and ditto give a file back its original modification time, which is in the past. Its access time is the moment of extraction, so it's already later than the write, and no read() of that file is ever recorded, not even the first. I extracted an entry dated January 2024. It came out with today's access time, and cat didn't move it.
cp, curl and git checkout write the file fresh, so the first read is recorded and then it stops. cp -p and mv keep whatever dates the source had.
What this does to "not used in 90 days"
A lot of tools judge a file stale by its access time, find -atime +90 included. So did mine.
For programs and libraries the date is fine, because every launch maps them. It's also fine for whatever a runtime maps, and that covers most local model weights.
For nearly everything else, the date you see is the first read after the last write, or the day the file was unpacked. A log you grep every morning keeps one date, and so does a dataset a script reads with read() a hundred times.
I counted on my own Mac, from metadata only, across nine folders: applications, Homebrew, Application Support, caches, and the home folders of a few developer tools. That came to 756,602 files and 86.97 GB. For 78.45 GB of it, 90.2%, the access time is already at or after the write, so a read() of any of those files leaves no trace. In /Applications the share was 98.7%, and in Homebrew 99.4%.
The dates can prove one thing, and it points the other way. If the access time is earlier than the modification time, nothing has read or mapped the file since it was last written, and that's a reading I trust.
If what you want is the last time a person opened a file, macOS keeps that somewhere else. When a file is opened in an app through Launch Services, by a double-click or by open, macOS writes an extended attribute called com.apple.lastuseddate#PS: 16 bytes, seconds then nanoseconds. Finder shows it as Date Last Opened. cat, a script's read and a Quick Look thumbnail don't set it. open -a Preview does, even in the background, and opening the file again moves it. There are two catches. A copy made with cp or ditto carries the source's stamp. And read the attribute itself, not Spotlight: mdls reported the creation minute as the last used date for files nobody had opened.
How I found out
I found out by getting it wrong. I make Mint, a storage app for the Mac, and one thing it offers is folders nobody has used in six months. "Used" meant the access time.
When I ran that on my own Mac, a lot of big folders that hadn't been written to in a year showed a read in the last few days. I hadn't opened them, but Mint had. It hashes files to find duplicates, and for many of them that hash was the first read after a write, the one read macOS records. A test file I'd dated 2020 came back stamped with the current minute.
The fix is one call at startup, setiopolicy_np with IOPOL_ATIME_UPDATES_OFF, which asks the kernel not to move access times for this process. It works for read(). While writing this post I tried mmap() under the same policy, and the access time moved anyway.
Mint was mapping files in five places. The one that mattered most drew a strip of pictures for each AI agent conversation, so opening that view marked every conversation as used that minute. All five read with pread now, and a check fails if anything in the app maps a file again. On copies of 24 real conversations, the new reader finds the same 2,726 pictures and leaves every access time where it was.
Two readers needed more than a pread. ImageIO maps the file when you open it by URL, so Mint now reads the bytes itself and hands ImageIO the data. Quick Look renders thumbnails in a process my setting doesn't reach, so Mint gives it an APFS clone of the file to render instead, and the clone is the one whose date moves. A clone takes under a millisecond and no extra space. All of this shipped in Mint 1.0.80 at the end of September.
My own notes had the rule wrong for a while too. They said the access time also moves "when it is a day old", which is the Linux rule.
Mint doesn't take a quiet access date as evidence any more. It lists a cold folder only when it can show that nobody uses it: either almost none of the folder's bytes have been read since they were written, which is the direction the dates can prove, or the app that owns the folder has written nothing anywhere for the whole window. When Mint can't work out who owns a folder, it doesn't list it. In a test with the window cut to 20 days, the only folder that looked cold by its dates was a 482 MB speech model. Mint couldn't name an owner for it and left it alone.
Try it
t() { stat -f '%Sa' -t '%H:%M:%S' "$1"; }
map() { osascript -l JavaScript -e 'ObjC.import("Foundation"); $.NSData.dataWithContentsOfFileOptionsError("'"$PWD/$1"'", 8, null).length' > /dev/null; }
echo "some text" > f
sleep 2; cat f > /dev/null; t f
sleep 2; cat f > /dev/null; t f
sleep 2; map f; t f
sleep 2; map f; t f
sleep 2; cat f > /dev/null; t f
The 8 is NSDataReadingMappedAlways, which is how map gets a mapping out of tools that ship with macOS.
All of this is macOS 27.0.1 (26A434) on Apple silicon, APFS, on the boot volume and on a fresh APFS disk image that I also unmounted and mounted again between reads. I first measured the read() rule on macOS 26. I haven't tried HFS+, a network volume, or a volume mounted with strictatime. If yours behaves differently, I'd like to hear about it at [email protected].