8 Runs a Day Became 3: Building a launchd plist Integrity Guard in 73 Lines A developer discovered that a launchd plist on macOS had been silently rewritten, reducing automated outreach DM runs from eight to three per day and costing dozens of missed opportunities. To prevent recurrence, they built a 73-line integrity guard that detects plist changes, logs forensic data, and restores correct schedules. The incident highlights that a running process may not be running correctly, emphasizing the need to monitor configuration values. At 12:04 on August 23, 2026, something quietly rewrote my outreach DM schedule down to three runs a day — 2:31 AM, 10:31 AM, and 6:31 PM. For the several days it took me to notice, that cost me dozens of missed opportunities every single day. I still don't know what did it. When you strip away the details, making money as a solo developer comes down to one equation: automation uptime = revenue. I was a college student making 100k yen a month, then 600k a month juggling multiple gigs, then back to zero when I was laid off for company reasons — and the reason I could build a Claude Code autonomous environment in six months and get to 1.2M yen in monthly revenue is that I had an outreach DM lane running while I slept. Automated outreach DMs are made up of four launchd jobs. Instagram, Threads, follower prospecting, and YouTube each have their own independent plist and Python script. These control their start times via StartCalendarInterval . For example, com.lily.outreach-ig runs eight times at 8:20, 10:20, 12:20, 14:20, 16:20, 18:20, 20:20, and 22:20, and com.lily.outreach-th runs seven times every two hours from 9:38 to 21:38 — sends happen during daytime hours. The problem is that a launchd plist is just an XML file, and anyone or anything can rewrite it . macOS does not notify the user when a plist changes. Even when launchctl list shows a job as "running," the schedule itself may be operating on post-rewrite values. That is exactly what happened on August 23: the job was alive. It was just running on a completely different setting — "three times a day, in the middle of the night." What tipped me off was looking at the DM send-count log manually one evening. The noon and 4 PM slots kept showing zero, so I dug in and found that StartCalendarInterval inside com.lily.outreach-ig 's plist had been reduced to just three entries: 2:31 / 10:31 / 18:31 . At the same time, com.lily.outreach-th had been cut from seven runs to three. The mtime last modified time was 2026-08-23 12:04. What I learned from this is that "running" and "running correctly" are two different things . What you need to monitor is not whether the process is alive, but the configuration values inside the plist themselves. If all you want is a simple integrity check, you could implement it with cron or Watch Paths. But what I insisted on this time was a design that leaves a forensic log at the same moment it restores . There are two reasons. The first is reproducibility. If the same rewrite happens repeatedly, a snapshot of which processes were running just before it gives me a way to narrow down the suspects. The second is the peace of mind of being able to trace why it changed after the fact. An automation environment is complex, and I can't rule out that I rewrote it myself by mistake from some script. With logs, I can separate "my own mistake" from "outside interference." When outreach-schedule-guard.sh detects something, it does four things: record the rewrite along with the mtime, capture a list of recently running processes, copy the plist as a timestamped backup, and restore the correct values and make launchd reload them. Of these, the forensic part takes up close to half of the script's 50 lines. I could have taken the "it just needs to work" route and only done the restore, but I didn't want to give up on finding the cause. Once you understand the structure of StartCalendarInterval , you see why reading it with Python's plistlib is the best approach. launchd plists are stored in XML format or in binary format via plutil -convert binary1 . Taking com.shun.self-repair.plist as an example, it has an array structure like this: