A security monitor that can be switched off quietly is worse than none, because it still looks reassuring. This post covers how MacSentry, my open-source Mac monitor, tries to make silencing it noisy: telling a reboot apart from kill -9, two processes that watch each other, an append-only log, a dead man's switch, and a Telegram detail that lets malware delete your alerts if you are not careful.
The goal is not to be unstoppable. Someone with root can, given time, stop any local monitor. The goal is that the attempt itself gets reported, from outside the machine, before it succeeds.
Get the evidence off the machine first
Anything stored only on the Mac can be cleaned up by whoever gets in. So every finding is sent to Telegram the moment it happens, and the local log is a second copy, not the primary record.
Delivery has to survive the conditions in which alerts matter most:
- Retry only transient failures. No connection, HTTP 429 and 5xx are retried every 10 seconds for up to about five minutes, because right after boot or wake the network is often not up yet. Other errors (a revoked token, a wrong chat ID) are logged once instead of retried forever.
- Respect the message limit. The Bot API accepts 1–4096 characters per message. Long alerts, such as a diff of a changed file, are cut at a safe length with a note pointing to the full entry in the local log.
- On shutdown, send synchronously. When launchd sends
SIGTERM, the daemon makes one send attempt and waits up to five seconds for it before exiting, so the "Mac is shutting down" message actually leaves.
A Telegram bot can delete its own alerts for 48 hours
This is the detail I missed at first. The Bot API's deleteMessage lets a bot delete its own outgoing messages in private chats and groups, as long as they were sent less than 48 hours ago. The token is all you need to call it.
MacSentry has a menu bar app running as the user, and that app needs a token to send its own alerts. Its token therefore lives in the user's home folder, readable by anything running as that user. If the root daemon used the same bot, malware running as the user could read the token and delete the daemon's alerts too, including the one about itself.
The fix is a second bot. The installer asks for a separate token for the daemon and stores it in /etc/macsentry.conf, owned by root with mode 0600. The daemon also checks whether its token matches the app's and, if so, sends a reminder once a week. A user-level attacker can still delete the app's messages, but not the daemon's, and the daemon is the one that reports persistence and tampering.
Telling a reboot from kill -9
A process cannot catch SIGKILL, so it cannot report its own forced death. It can, however, notice on the next start that its previous run did not end normally. The daemon combines two facts (shortened from daemon/Sources/main.swift, alert text translated):
let boot = bootTime() // sysctl KERN_BOOTTIME
if readText(sp("lastboot")) != String(boot) {
alert("🟢 The Mac just started")
} else if exists(sp("clean_stop")) {
info("▶️ macsentryd restarted")
} else {
alert("⚠️ macsentryd was stopped abruptly (kill -9 or a crash) and launchd restarted it")
}
The SIGTERM handler writes the clean_stop marker before exiting. If the boot time is unchanged and the marker is missing, the previous instance died without running its handler, which means SIGKILL or a crash. Because the job has KeepAlive, launchd restarts it within seconds and the alert goes out.
Planned stops must not look like attacks
A monitor that cries wolf on every update teaches you to ignore it. The installer therefore leaves markers before it touches anything: updating (the stop is an update, not a shutdown), clean_stop (the next start is not a kill -9) and just_updated (changes to MacSentry's own files in the next ten minutes are reported silently instead of as alerts). The installer also waits for the old daemon to exit fully before starting the new one, because the old one may still be sending its last message.
The same idea covers rebuilds. The daemon records the code signature of its own binaries. A rebuild signed with the same certificate is reported silently; a binary whose signer changed, or that lost its signature, is reported as a possible swap. The same check runs for the executable of every LaunchDaemon and LaunchAgent in /Library: an update usually keeps its publisher, a swapped binary usually does not.
Two processes that watch each other
MacSentry has two long-running parts with different weaknesses:
- The daemon runs as root from boot, before anyone logs in. A normal user cannot stop it.
- The menu bar app runs as the user. It is a LaunchAgent with
KeepAliveset to{SuccessfulExit: false}, and it has no Quit item.
They check each other. The daemon alerts if no MacSentry app is running for the logged-in user; the app alerts if the daemon or the Endpoint Security helper has been missing for about a minute, and again when it comes back.
The SuccessfulExit detail matters. launchd.plist(5) defines SuccessfulExit: false as "restart the job unless it exits with status zero". When a second copy of the app starts (for example, opened from Finder while the launchd copy is running), it must exit with a non-zero status. Exiting with 0 would tell launchd the job finished normally, and if that happened to the only copy, nothing would be left running.
Watch the commands that silence monitors
Endpoint Security events (covered in reading eslogger from Swift) let MacSentry see commands, not just their effects. Rules flag:
launchctl bootout,unloadordisableaimed at MacSentry, Wazuh or common EDR agents, andpkillorkillallof their processes;chflagsremoving the append-only or immutable flags, andlog erase;- turning off SIP, Gatekeeper, the firewall or
pf.
Each alert includes the user and the full parent chain, so you can see whether the command came from your terminal or from something you do not recognize.
An append-only log, and what it does not protect
The local log is created with the system append-only flag:
chflags sappnd /var/log/macsentry.log
According to chflags(1), only the super-user can set sappnd, and chflags(2) defines the flag as "the file may only be appended to". While it is set, existing lines cannot be rewritten or truncated, and the file cannot be removed, even by root. Root can still clear the flag first, so this stops non-root processes and accidental edits, not a determined administrator. Clearing the flag is itself one of the commands above, so it is reported before the log can be touched.
A cryptographic chain is stronger when you control the storage: I described one, with hash chains and an HMAC key kept outside the database, in building a tamper-evident audit log in PostgreSQL. For a local file on a machine you may no longer trust, sending each entry off the machine as it happens matters more than how the local copy is protected.
A dead man's switch covers what the Mac cannot report
Some events end with nothing left to report them: the Mac is wiped, the power is cut, the daemon is removed by someone with root. For those, the daemon pings an outside URL every minute, for example a check on healthchecks.io. The service alerts you when the pings stop. It is the same principle as monitoring the monitor in self-hosting side projects: the signal that something died has to come from somewhere that did not die with it.
Keep the secrets out of reach of what you watch
- The daemon's token sits in a root-only file and is only ever in memory; it never appears on a command line. The early shell version passed it to
curlon standard input (curl -K -) for the same reason: arguments show up inps. - The Endpoint Security helper never holds a token. It writes findings into a spool directory that only root can read, and the daemon sends them.
- File diffs and everything the helper sends are masked before they leave the machine: Telegram bot tokens, and values that follow names like
TOKEN=orPASSWORD:.
Summary
| Threat | What MacSentry does |
|---|---|
| Local evidence cleaned up | Send each finding to Telegram immediately; retry transient failures for ~5 minutes |
| Malware deletes the bot's alerts | Separate root-only bot for the daemon; warn when tokens are shared |
kill -9 on the daemon |
Boot time plus a clean-stop marker; launchd restarts and the restart is reported |
| False alarms from updates | Installer markers; same-signer rebuilds reported silently |
| App or daemon stopped | Each watches the other; app relaunched by launchd unless it exits non-zero |
| Commands that disable monitoring | Endpoint Security rules with the full parent chain |
| Machine wiped or powered off | Heartbeat to an outside dead man's switch |
None of this makes a Mac safe on its own. MacSentry is a tripwire next to FileVault, updates and an EDR if you have one. What these details buy is that turning the tripwire off is itself something you hear about.
If you've worked on making a local monitor hard to switch off quietly, I'd love to compare notes. My email is on the home page.