A Claude Code skill that finds out why a Mac is slow, hot, or out of memory — and tells apart the processes that are actually the problem from the ones that merely looked busy for a second.
A snapshot cannot distinguish these two processes:
- one compiling something for three seconds, and
- one that has been quietly burning a full core for two days.
At the instant you look, they are identical. This skill samples repeatedly over a real time window and then asks how often something showed up as heavy — which is the question that separates a sustained hog from a normal burst.
It also hunts for a second, distinct problem: orphaned processes. A dev
server, test watcher, or build daemon whose parent terminal or IDE has exited
keeps running, reparented to launchd, consuming CPU and RAM for days.
Nothing surfaces these in the usual tools, because from the outside they look
like any other running process.
| Bucket | Meaning |
|---|---|
| Consistently heavy | Appeared in most samples with a high average — the real ongoing load |
| One-off spikes | Appeared once or twice with a high peak — normal, not an action item |
True zombies (STAT=Z) |
Already dead, using no resources — harmless, listed only for completeness |
| Orphaned processes | Alive, parent gone, reported with working directory and age |
| Memory hogs by app | RSS summed across all helper processes of each app |
The last one matters more than it sounds: any Electron or Chromium app spreads
its footprint over a dozen-plus helper PIDs, so per-process memory rankings
systematically understate them. These scripts group by .app bundle.
- macOS (Apple Silicon first; Intel partially supported)
- Python 3 (system Python is fine)
- mac-system-stat installed
as a sibling skill — this skill reuses its
hoststathelper for CPU/memory/temperature/power readings instead of reimplementing metric collection.
Clone into your Claude Code skills directory, next to mac-system-stat:
git clone https://github.com/procoders/mac-hog-hunter.git ~/.claude/skills/mac-hog-hunterThe layout must end up like this, since the skill calls its sibling by relative path:
~/.claude/skills/
├── mac-hog-hunter/
└── mac-system-stat/ # https://github.com/tomcatzh/mac-system-stat
Then just describe the symptom to Claude Code — "my Mac is slow", "why is it so hot", "what's eating my RAM" — and the skill triggers on its own.
Each script is independently useful outside the skill:
| Script | What it does |
|---|---|
scripts/monitor.sh |
Samples hoststat plus top CPU/RSS processes on an interval into a log |
scripts/aggregate.py |
Reads that log, groups by app bundle, separates sustained load from spikes |
scripts/mem_by_app.py |
One live pass summing RSS per app across all its helper processes |
scripts/find_zombies.py |
Finds orphans and true zombies, with working directory and age |
scripts/kill_gracefully.sh |
SIGTERM, wait, then SIGKILL only for what is still alive |
PPID == 1 alone is not enough. On macOS every process whose parent dies gets
reparented to launchd (PID 1) — but launchd also directly parents plenty
of legitimate services on purpose, so the signal on its own cannot tell
"orphaned" from "supposed to run forever".
find_zombies.py cross-checks against launchctl list, which enumerates the
jobs launchd actually manages. It then filters out OS-bundled binaries, app
extensions, and crash handlers, since those are designed to detach from
their parent — a crash handler specifically must outlive the process it
reports on. Without that filter the raw PPID == 1 signal is almost entirely
false positives: in testing it returned 92 matches, of which fewer than 10
were real.
hooks/worktree-orphan-cleanup.sh addresses the same problem from the other
end: prevention.
git worktree remove deletes a directory but does nothing about processes
still running with that directory as their working directory. Those become
orphans the moment their parent session exits. The hook runs before the
removal and kills anything whose cwd is inside the target path.
Install it as a PreToolUse hook in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"if": "Bash(git *)",
"hooks": [
{
"type": "command",
"command": "~/.claude/skills/mac-hog-hunter/hooks/worktree-orphan-cleanup.sh",
"timeout": 15
}
]
},
{
"matcher": "Bash",
"if": "Bash(rm *)",
"hooks": [
{
"type": "command",
"command": "~/.claude/skills/mac-hog-hunter/hooks/worktree-orphan-cleanup.sh",
"timeout": 15
}
]
}
]
}
}The hook never blocks the command it precedes — it always exits 0. A failure to clean up should not stop your git operation.
Note that it kills matching processes automatically and without confirmation. That is the intended trade-off for a hook, but it is a real behavior change: if a worktree removal catches a process you wanted to keep, it dies.
A few things that cost real debugging time, recorded so nobody repeats them:
- macOS
pshas noetimes. That is a GNU extension. BSDpsonly offersetimein[[DD-]HH:]MM:SS, which the script parses itself. - Batch your
lsofcalls. Querying working directories one PID at a time is roughly a hundred times slower than one batched call across every PID at once. On ~700 processes that is the difference between 0.7 seconds and a multi-minute hang. - Resolve symlinks before comparing paths.
lsofreports/private/tmpwherepwdsays/tmp, so path comparisons silently fail withoutpwd -P. ps auxoutput can sort into your results. The header row sorts like any other line and will land in a top-N list.
- mac-system-stat by tomcatzh — required dependency, MIT licensed
- devclean by ImL1s — the
SIGTERM → wait → SIGKILL sequencing and the
PPID == 1orphan signal
MIT — see LICENSE.