Last time I wrote about picking out and reaping only the "orphaned Chrome" processes. This time it's the monitoring script sitting right next to it, mem-hog-guard.sh. It could detect the problem just fine, but had no permission to kill anything, so all it ever did was send a notification. Here's how I handed it that permission in a way that's verified at every step, instead of in a form where one typo locks me out of sudo entirely.
The problem: it can see the culprit but can't touch it
The header comment of mem-hog-guard.sh records how this started.
# なぜ要るか(2026-08-09):
# macOSの dasd(Duet Activity Scheduler) が RSS 264MB のまま MEM 47GB / CMPRS 46GB を
# 抱え、swapを37GBまで押し上げて Chrome の launchPersistentContext が180秒で
# タイムアウト -> SNS全レーンが1日分まるごと0件で失敗した。稼働21時間で発生。
# RSS上位監視には一度も映らないため、誰も気づけなたかった。
In short: macOS's dasd (Duet Activity Scheduler) sat at an RSS of 264MB while holding MEM 47GB / CMPRS 46GB, pushed swap up to 37GB, and made Chrome's launchPersistentContext time out at 180 seconds. Every SNS lane failed with 0 items for a full day. It happened after 21 hours of uptime, and because it never showed up in the top-RSS view, nobody noticed.
dasd is a standard macOS daemon, and it's a safe target: if it dies, launchd restarts it immediately. So the detection logic was already properly built with AUTO_RESTART_NAMES="dasd" / AUTO_RESTART_THRESHOLD_MB=5120 (5GB). The problem was the single step after detection.
if in_list "$command" "$AUTO_RESTART_NAMES" && [ "$mem_mb" -ge "$AUTO_RESTART_THRESHOLD_MB" ] && ! cooldown_active "$command"; then
...
else
# dasd 等はroot所有。NOPASSWDルールが無い環境では -n が失敗するので通知に落とす。
cooldown_mark "$command"
if sudo -n /usr/bin/killall "$command" 2>/dev/null; then
log "restarted $command pid=$pid mem=$mem cmprs=$cmprs"
findings="${findings}✅ ${msg}"$'\n'
restarted=$((restarted + 1))
else
log "restart-failed(no-sudo) $command pid=$pid mem=$mem"
findings="${findings}⚠️ ${msg} — sudo権限が無く再起動できず。手動: sudo killall ${command}"$'\n'
fi
fi
dasd is owned by root, so the only way an unattended job can kill it is sudo -n (sudo with no password prompt). I hadn't set up a NOPASSWD rule, so sudo -n failed every single time. The log filled up with restart-failed(no-sudo), and the notifications just kept saying "⚠️ manual: sudo killall dasd" over and over.
There was a quieter second problem too: cooldown_mark "$command" is called before the script checks whether sudo succeeded. So while NOPASSWD was missing, every failed attempt still dutifully started a 30-minute cooldown (COOLDOWN_SEC=1800). Even as dasd kept growing, the next pass silently skipped re-detection. The detection logic was correct, but with no ability to act, the script had degraded into "quietly watching it happen."
The design: three stages, and only the narrowest possible root
enable-dasd-autorestart.sh fixes this. You could just add a NOPASSWD line via visudo and it would work, but that exposes you to two accidents: "a typo kills sudo itself" and "I think I added it, but it isn't actually in effect." So the script is built in three stages.
1. Pin the rule to one command, arguments included
RULE='matsubara ALL=(root) NOPASSWD: /usr/bin/killall dasd'
It's /usr/bin/killall dasd, not NOPASSWD: /usr/bin/killall. sudoers matches the command line including its arguments, so this one line permits exactly one thing: running killall against a process named dasd. No ALL, no bare path, no arbitrary process. The root access handed to an unattended script is limited to "this one command," not "if this breaks, everything is exposed."
2. Validate syntax with visudo -cf before installing
# 壊れたsudoersを入れるとsudo自体が死ぬので、必ず検証してから設置する
visudo -cf "$TMP"
install -m 440 -o root -g wheel "$TMP" /etc/sudoers.d/lily-dasd
Under /etc/sudoers.d/, a single file with broken syntax is enough to stop the sudo command from working at all. Once that happens, every task that needs root (including fixing sudoers) is dead. So the rule is written to a temp file, syntax-checked with visudo -cf, and only then installed for real with install -m 440 -o root -g wheel. The permissions are 440 (root-owned, read-only), which also closes the path where someone later opens it in vi, edits it directly, and breaks it. The installed file really does have exactly those permissions:
$ ls -la /etc/sudoers.d/lily-dasd
.r--r----- 53 root 12 Aug 13:47 /etc/sudoers.d/lily-dasd
3. Right after installing, verify it actually takes effect, before running anything
This is the most important part: installing the rule is not the point where the script says "done."
# 設置後の検証(matsubaraとしてNOPASSWDが効くか)
sudo -u matsubara sudo -n -l /usr/bin/killall dasd >/dev/null 2>&1 \
&& echo "[2/3] NOPASSWD 検証OK" \
|| { echo "[2/3] 🔴NOPASSWD検証NG"; exit 1; }
sudo -n -l is a mode that asks only "am I allowed to run this command?" without a password. If that fails, the script does exit 1 immediately, catching on the spot that the installed rule isn't actually effective. visudo's syntax check only tells you "this isn't broken"; whether it "works as intended" is a separate question you have to check yourself.
Only once that verification passes does the script run the first killall and measure the effect.
echo "[3/3] dasdを今すぐ回収(launchdが即座に再起動する)"
BEFORE="$(vm_stat | awk '/occupied by compressor/{gsub(/\./,"");printf "%.1f", $5*16384/1073741824}')"
killall dasd || true
sleep 5
AFTER="$(vm_stat | awk '/occupied by compressor/{gsub(/\./,"");printf "%.1f", $5*16384/1073741824}')"
echo "圧縮メモリ: ${BEFORE}GB -> ${AFTER}GB"
echo "swap: $(sysctl -n vm.swapusage)"
It converts vm_stat's occupied by compressor (a page count) into GB and prints before/after side by side, so the log records whether compressed memory actually dropped rather than "I think I killed it." From then on, mem-hog-guard.sh's own sudo -n /usr/bin/killall dasd succeeds unattended, and the lines that used to say restart-failed(no-sudo) become restarted $command.
Note
The fork in the road when designing a NOPASSWD rule is "ALL, or one command?"ALLtakes a second to set up, but it leaves an unattended script able to do anything as root. Here, the detection logic only ever decides "the name isdasdand it's over the threshold," so the permission was narrowed to match: exactly one line,/usr/bin/killall dasd. The key is making the scope of the sudo grant match the scope of what the monitoring script actually decides.
Pitfalls I hit
-
cooldown_markfires before sudo's result is known → While NOPASSWD was missing, every failed detection quietly stacked up a 30-minute cooldown, and the next pass silently skippeddasdeven as it kept growing -
visudo -cfchecks syntax, not effect → The syntax can be valid and the rule still fail to match because of a typo in the username or target command. Withoutsudo -n -lright after installing, you end up with "I thought I added it" -
NOPASSWD: /usr/bin/killallalone is too broad → Include thedasdargument so sudoers matches exactly one command.ALLis obviously too wide, but even a command-name-only grant lets you kill any process as root -
One broken file under
/etc/sudoers.d/takes down all of sudo → Write to a temp file, pass it throughvisudo -cf, andinstallonly what has been validated. Never edit in place
Summary
- The monitoring logic (
mem-hog-guard.sh) was correct from the start, but with no ability to act (sudo -n) it had degraded to "detect and notify" - When granting permission, use a NOPASSWD rule narrowed down to the command and its arguments, and avoid
ALLor broad grants - Install in this order:
visudo -cffor syntax →install -m 440for the real file →sudo -n -lto verify it takes effect → run once and measure before/after. Don't stop at "it should work" at any stage - Only hand root to an unattended script after the permission scope matches the scope of what the detection logic decides
Next up: the story left in a comment in mem-hog-guard.sh about how the cooldown was one global timer instead of per-process, so for the 30 minutes after fixing dasd, a different process was left completely unwatched.
When you've had to give an unattended script root for a single task, did you narrow the sudoers rule down to the arguments, or stop at the command path?
Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*
Top comments (0)