{"id":51328131,"url":"https://github.com/fl4p/macos-oom-guard","last_synced_at":"2026-07-01T20:04:08.442Z","repository":{"id":366422476,"uuid":"1268958843","full_name":"fl4p/macos-oom-guard","owner":"fl4p","description":"System-wide OOM killer for macOS — kills the biggest runaway process before the kernel panic-reboots from swap exhaustion (the earlyoom/systemd-oomd macOS is missing).","archived":false,"fork":false,"pushed_at":"2026-06-14T06:10:40.000Z","size":10,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-21T19:28:27.350Z","etag":null,"topics":["apple-silicon","earlyoom","launchd","macos","memory-management","oom","oom-killer","sysadmin"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fl4p.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-14T06:10:38.000Z","updated_at":"2026-06-14T06:10:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/fl4p/macos-oom-guard","commit_stats":null,"previous_names":["fl4p/macos-oom-guard"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/fl4p/macos-oom-guard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fl4p%2Fmacos-oom-guard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fl4p%2Fmacos-oom-guard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fl4p%2Fmacos-oom-guard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fl4p%2Fmacos-oom-guard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fl4p","download_url":"https://codeload.github.com/fl4p/macos-oom-guard/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fl4p%2Fmacos-oom-guard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35020872,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apple-silicon","earlyoom","launchd","macos","memory-management","oom","oom-killer","sysadmin"],"created_at":"2026-07-01T20:04:07.867Z","updated_at":"2026-07-01T20:04:08.434Z","avatar_url":"https://github.com/fl4p.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# macos-oom-guard\n\nA system-wide **OOM killer for macOS** — the one Apple doesn't ship.\n\nWhen a process eats all your memory, macOS doesn't kill it. It compresses, then\ngrows swapfiles on the boot disk *indefinitely*, on the assumption that memory\npressure is transient. On Apple Silicon the swap **segment table is finite**, so\nwhen it saturates while a process keeps demanding pages, the whole VM deadlocks —\n`WindowServer` misses its userspace watchdog check-in and the kernel\n**panic-reboots the entire machine** (`AppleARMWatchdogTimer`) rather than let the\nGUI hang. You lose everything, not just the runaway process.\n\nThis is a tiny, self-protecting root daemon that does what Linux's `earlyoom` /\n`systemd-oomd` do: watch memory, and **SIGKILL the biggest offending process\n*before* the box falls off the cliff** — so you lose one app instead of the machine.\n\n```\n$ python3 macos_oom_guard.py --status\nphysical RAM        : 38.7 GB\nmemorystatus_level  : 86   (0..100, % available; trigger when \u003c crit)\nswap used           : 2.6 GB\n\ntop eligible (killable) processes by phys_footprint:\n     2.4 GB  pid    893  /Users/you/Applications/PyCharm.app/Contents/MacOS/pycharm\n     1.5 GB  pid   1333  /Applications/Google Chrome.app/.../Google Chrome Helper\n...\n-\u003e would kill first: pid 893 (2.4 GB) .../pycharm\n```\n\n## Why the built-in mechanisms don't save you\n\n- **Jetsam** (`kern.memorystatus_*`, the in-kernel killer) exists, but on desktop it\n  only aggressively targets sandboxed / idle apps. A long-running `python3` (or any\n  heavy CLI process) launched from a terminal has no jetsam high-watermark and\n  survives right up to the deadlock.\n- **`memory_pressure`** (the only built-in CLI) is a pressure *generator* for\n  testing — not a killer.\n- **In-process guards** (a watchdog thread inside your own job) can't help: they only\n  see their own process tree, they usually poll\n  `kern.memorystatus_vm_pressure_level` — which, measured live, stays at `1/NORMAL`\n  even in deep swap-death — and the watchdog thread itself gets CPU-starved during\n  the exact freeze it's supposed to catch.\n\n## How it works\n\n- **Triggers on `kern.memorystatus_level`** — a `0..100` \"% memory available\" gauge\n  that jetsam itself trends on and that falls *smoothly* as memory fills (unlike the\n  bucketed pressure level). It **never** triggers on memory footprint: a healthy Mac\n  can sit at a huge footprint of compressed/sparse data while perfectly green.\n  Footprint is only used to *rank* victims. Secondary trigger: swap used past a\n  configurable multiple of RAM.\n- **Ranks victims by `ri_phys_footprint`** (`proc_pid_rusage`) — the same metric\n  Activity Monitor's \"Memory\" column and jetsam use. (A swapped hog's RSS collapses,\n  and footprint measured from outside overcounts compressed pages — `phys_footprint`\n  is the honest one.)\n- **Protects the system two ways**: a name denylist (`WindowServer`, `launchd`,\n  `kernel_task`, `coreaudiod`, …) **and** a location rule — it will only ever kill\n  executables under `/Applications`, `/Users`, `/opt/homebrew`, or `/usr/local`. A\n  system daemon, WindowServer, or the kernel can never be the victim.\n- **Self-protects**: runs as root at `nice -20` and calls `mlockall()` so the killer\n  is never swapped out when it's needed most. The hot poll loop does **zero\n  fork/exec and zero per-iteration allocation** (pure `ctypes` sysctl/libproc);\n  full process enumeration happens only once it's already near the threshold.\n\nNo dependencies — pure Python 3 stdlib + `ctypes`. Single file. Apple Silicon and\nIntel.\n\n## Install\n\n```bash\n# 1. See what it reads and what it WOULD kill right now (read-only, safe):\npython3 macos_oom_guard.py --status\n\n# 2. Watch it in the foreground in dry-run — logs decisions, never kills.\n#    Recommended: run a heavy job and confirm it would pick the right victim.\npython3 macos_oom_guard.py --run --dry-run\n\n# 3. Install as a boot-time root LaunchDaemon (armed):\nsudo python3 macos_oom_guard.py --install\n\n# Uninstall:\nsudo python3 macos_oom_guard.py --uninstall\n```\n\nThe installer writes `/Library/LaunchDaemons/io.github.fl4p.macos-oom-guard.plist`\nand bootstraps it with `launchctl`. Logs go to `/var/log/macos-oom-guard.log`:\n\n```bash\ntail -f /var/log/macos-oom-guard.log\n```\n\nTo install in dry-run mode (logs would-be kills but never acts), set\n`OOMG_DRY_RUN=1` before `--install` (it's baked into the plist).\n\n## Configuration\n\nAll tunable via environment variables (also baked into the plist at `--install`):\n\n| Variable | Meaning | Default |\n|---|---|---|\n| `OOMG_CRIT_LEVEL` | kill when `memorystatus_level` drops below this | `10` |\n| `OOMG_WARN_LEVEL` | start logging/enumerating below this | `25` |\n| `OOMG_SWAP_MULT` | also kill when `swap_used \u003e MULT × RAM` (and level \u003c 20) | `1.5` |\n| `OOMG_MIN_VICTIM_GB` | never kill a process smaller than this footprint | `1.5` |\n| `OOMG_STRIKES` | consecutive trips required before killing | `2` |\n| `OOMG_POLL_S` | poll interval, seconds | `1.0` |\n| `OOMG_DRY_RUN` | `1` = log but never kill | `0` |\n\n## Choosing what gets killed\n\nThe default policy is **\"kill the biggest non-protected user process.\"** That's the\nright call at a real cliff: by the time `memorystatus_level` hits single digits, the\nactual runaway is tens of GB and dwarfs everything else, so it gets picked. But note\nthat *at idle* the biggest user process might be your editor — edit the\n`PROTECT_NAMES` set in the script (e.g. add `\"pycharm\"`, `\"Code\"`) if you'd rather\nshield specific apps, at the cost of not killing them if *they* are the one that\nleaks.\n\n## Safety notes\n\n- This sends `SIGKILL` (no clean shutdown) — by design, because at the cliff a\n  `SIGTERM` may never get serviced. Unsaved work in the victim is lost. That is still\n  strictly better than a kernel panic, which loses **all** unsaved work everywhere.\n- Run `--status` and `--run --dry-run` first to satisfy yourself the victim\n  selection matches your expectations before arming it.\n- It requires root to install (LaunchDaemon, `mlockall`, killing other users'\n  processes).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffl4p%2Fmacos-oom-guard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffl4p%2Fmacos-oom-guard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffl4p%2Fmacos-oom-guard/lists"}