Stop macOS OOM crashes from taking down your agent work by sacrificing expendable GUI apps first.
This is a small macOS LaunchAgent for machines running Codex, OpenClaw, Claude Code, local agents, or other long-running AI work. It watches memory pressure trends and terminates only explicitly configured GUI app bundles when pressure gets dangerous.
It is not a generic "kill the biggest process" script. It fails closed: if it cannot prove a PID belongs inside a configured app bundle, it skips that PID.
macOS already compresses memory and swaps. What macOS does not know is your policy:
- browser tabs are expendable
- AI agent sessions are not
- terminal shells, SSH, OpenClaw, Codex, Python/Node workers, and launchd services should be protected
This watcher adds that policy layer.
| Tier | Apps |
|---|---|
| 1 | Google Chrome, Brave Browser, Safari |
| 2 | Discord, Lovable, evaOS Workbench |
| 3 | Telegram, Messages |
Tier 3 is opt-in. Messaging apps can be operational lifelines, so the installer defaults to MAX_TIER=2.
Primary signals:
vm.memory_pressurevm_statpageout ratevm_statswapout rate- compressor, free-page, and reclaimable-page percentages as supporting context
Low free pages and compressor churn alone are not triggers. On macOS, low free pages can be normal when inactive/file-backed memory is reclaimable. Termination requires real pageout or swapout pressure; reclaimable memory only affects severity.
The watcher suppresses repeat actions against the same app bundle for 10 minutes by default. That prevents it from fighting you if you intentionally reopen an app after it was sacrificed once.
It also skips candidate app bundles below 512MB total RSS by default. Closing a 170MB chat app is not an OOM strategy; it is collateral. The current frontmost app is protected by default.
The watcher:
- resolves PID executable paths with
proc_pidpath - ascends to the outermost
.appbundle - reads
Contents/Info.plist - requires exact
CFBundleIdentifiermatch - revalidates the PID before hard kill
- skips protected paths such as
.openclaw, Codex, Terminal, iTerm, VS Code, Claude, and ChatGPT - protects the frontmost app by default
- de-duplicates helper processes to one canonical app-level candidate per bundle
- avoids substring matching,
pkill, andkillall
./agent-resource-hog-watcher.py --self-test
./agent-resource-hog-watcher.py --inventory
./agent-resource-hog-watcher.py --print-plist | plutil -lint -
./agent-resource-hog-watcher.py --dry-run --force-severity warning --interval 0.1The forced trigger is for proof. Use it with --dry-run unless you intentionally want to test live termination.
./install.sh plist
./install.sh install --yes
./install.sh statusThe generated LaunchAgent runs observe-only by default:
~/Library/LaunchAgents/ai.100yen.resource-hog-watcher.plist
The installer copies the runtime script into:
~/.agent-resource-hog-watcher/bin/agent-resource-hog-watcher.py
That avoids launchd permission surprises from removable volumes, symlinked repo folders, or deleted checkouts.
Logs and status:
~/.agent-resource-hog-watcher/logs/resource-hog-watcher.log
~/.agent-resource-hog-watcher/logs/resource-hog-watcher.status.json
Optional tuning:
MAX_TIER=2 INTERVAL_SECONDS=2 ./install.sh install --yesArm live termination only after observing dry-run output and confirming the thresholds are right:
ARMED=1 MAX_TIER=1 INTERVAL_SECONDS=2 ./install.sh install --yesAllow tier-3 lifeline apps only if you really want that behavior:
MAX_TIER=3 ./install.sh install --yesChange repeat-kill suppression:
BUNDLE_COOLDOWN_SECONDS=300 ./install.sh install --yesChange the minimum app-bundle size required before termination:
MIN_BUNDLE_RSS_MB=1024 ./install.sh install --yes./install.sh unload --yes
./install.sh uninstall --yesuninstall archives the plist instead of deleting it.
- This cannot save a machine if the kernel never schedules the watcher before the OOM event.
- It does not add swap, purge caches, or tune kernel behavior.
- It intentionally protects agent/runtime processes outside configured GUI app bundles.
- Edit the target list before installing if these defaults do not match your workflow.
MIT.