Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mac-audit

Find out what is actually eating your Mac — including the hundreds of orphaned MCP servers your agent sessions left behind.

macOS 12+ License MIT shellcheck clean read only ci

A Claude Code plugin, and two plain bash scripts that work on their own. One pass, about two seconds, then a ranked diagnosis of load, memory, spin loops, leaking daemons, thermal throttling and disk pressure.

It never terminates anything. It shows you the evidence and prints the command. You decide.


What it looks like

Ask in plain language:

my mac is crawling, what's eating it?

The report:

--- [1] LOAD ---
 0:39  up 49 days,  6:20, 1 user, load averages: 55.08 28.83 21.17
cores: 10   (load/cores > 2 = oversubscribed, > 5 = severe)

--- [2] MEMORY ---
installed: 64 GB   page size: 16384
free:           0.23 GB
inactive:      21.06 GB
wired:          7.26 GB
compressor:    13.66 GB
swap:       total = 14336.00M  used = 12679.44M  free = 1656.56M

--- [3] TOP CPU (15) ---
   PID   %CPU    RSS_MB    PPID      ELAPSED  COMMAND
 54462  100.0      87.8   54394  03-02:10:34  …/Google Chrome Helper (Renderer) --type=renderer
 54410  100.0      44.1   54394  03-02:10:36  …/Google Chrome Helper (Renderer) --type=renderer
   359   53.6    6959.4       1  49-05:55:04  …/FSEvents.framework/…/fseventsd

--- [5] RAM BY APP GROUP ---
Arc                     16722 MB  (58 proc)
claude-code              7620 MB  (20 proc)
node-dev                 3068 MB  (243 proc)

Four Chrome renderers pinned at 100 % CPU for 74 hours — an automation browser some test run never cleaned up. fseventsd at 6.8 GB, against a couple hundred MB in normal use. And 243 node processes, most of which nothing is using.

The second script explains that last number:

$ stale-tooling.sh
candidates: 237   PROPOSED: 235   ASK: 2   KEEP (live controller): 0
those 235 processes belong to 20 abandoned trees — 20 sessions left them behind

=== PROPOSED — orphaned by ancestry AND in a throwaway install directory ===
  codegraph             120 proc   oldest: 43 d
  mcp-server             75 proc   oldest: 43 d
  agent-app-server       32 proc   oldest: 37 d
  headless-browser        8 proc   oldest: 24 d

  Full list with ages and command lines: /var/folders/…/mac-audit-a1B2c3/proposed.txt
  Read it before acting. A grouped count cannot show you the one line that
  is actually your work.

--- To stop them, once you have read the list ---

  xargs kill < '/var/folders/…/mac-audit-a1B2c3/proposed-pids.txt'

Activity Monitor shows you the Chrome processes. It will not tell you that 235 of the entries are corpses of sessions you closed weeks ago.

Install

/plugin marketplace add Crackx17/mac-audit
/plugin install mac-audit@mac-audit

Then describe the symptom — "my Mac is crawling", "the fans are screaming", "what's eating my RAM". The skill triggers on the complaint, not on a command name, and answers in whatever language you ask. To invoke it explicitly: /mac-audit:mac-audit.

Updating matters here: a marketplace is a git clone taken when you add it, not a subscription. New commits do not reach you on their own.

/plugin marketplace update mac-audit

To remove it: /plugin uninstall mac-audit.

Why the orphaned-MCP problem is new

Every agent session — Claude Code, Codex, Cursor, anything speaking MCP — spawns several servers. A clean shutdown stops them, and launchd reaps whatever exits. What strands them is the messy exit: a crash, a killed terminal, a session that never closed its side of the pipe. Those servers get reparented to launchd and keep running, because nothing is left that knows to stop them.

Over weeks this compounds quietly into hundreds of processes. They are individually small, which is exactly why nobody notices.

How it decides what is dead

Orphanhood is established by ancestry, not by age. A process is orphaned only when walking its parent chain reaches launchd without passing through a live ancestor that is not itself disposable.

The obvious rules are all wrong:

Rule Why it fails
It is old, so it is dead A five-day-old agent session is alive and its servers are working
Its parent is not claude or codex Fails on the host you did not list. Ancestry correctly protected a codex app-server owned by ChatGPT.app, and a gh-mcp owned by gh mcp
Its PPID is 1 Only the root of a dead session gets reparented to launchd. Its descendants keep their original parent, so a PPID check finds the tree tops and misses everything under them
It matches a tool pattern So does a real service whose command line happens to contain mcp

An ancestor is transparent to the walk when it is itself disposable, whether or not its name matches anything — real orphan trees contain unnamed wrappers, and stopping at the first one would protect the whole subtree beneath it. Transparent does not mean ignored; it means keep looking. A durable ancestor stops the walk at once, which is what keeps a live service from losing its children.

A process reaches PROPOSED only when it is orphaned by ancestry and its executable or script lives in a throwaway install directory (.npm/_npx, node_modules, .cache, .nvm). Argument values are excluded from that test on purpose: python app.py --cache-dir ~/.cache/foo is a durable service, not a disposable one.

Everything else goes to ASK. Refusing to conclude is a valid answer.

Ancestry is evidence, not proof

It establishes that no qualifying parent remains. That is not the same as nothing being in charge: supervision can run through launchd itself, a socket, or an application control plane, and a living parent is not necessarily supervising anything. A detached nohup job is orphaned by design.

Two limits worth knowing before you trust a list:

  • The walk stops at any live durable ancestor, so a chain that passes through one is reported as controlled even if that ancestor is itself unwanted. This errs toward keeping things, which is the safe direction, but it means detection is not exhaustive.
  • Counting processes is not counting incidents. Two hundred orphans are often twenty dead sessions, which is why the output reports both. If what you want is to know what went wrong, the number of trees is the honest figure; the process count is what you have to clean up.

Configuration

Patterns live in skills/mac-audit/scripts/patterns.conf. Override without touching the plugin:

mkdir -p ~/.config/mac-audit
cp skills/mac-audit/scripts/patterns.conf ~/.config/mac-audit/

Four lists: what counts as disposable tooling, which directories are throwaway, which are durable, and what must never be touched. Widening them changes what gets reviewed, never what gets proposed without evidence. A malformed pattern fails loudly at startup rather than silently reporting nothing.

Using the scripts without Claude

Plain bash, no dependencies, strictly read-only.

skills/mac-audit/scripts/audit.sh          # full report, ~2s
skills/mac-audit/scripts/audit.sh --disk   # adds du scans and a Docker query;
                                           # much slower, and blocks if the
                                           # Docker daemon is wedged

skills/mac-audit/scripts/stale-tooling.sh              # what is orphaned, and why
skills/mac-audit/scripts/stale-tooling.sh --min-days 0 # include today's processes

Safety

Nothing in this repository signals a process. That is a deliberate retreat: three rounds of independent adversarial review produced fifteen critical findings, and every single one lived in code that sent signals. Getting termination right needs primitives shell does not have — no process handles, no atomic check-then-signal — so each fix narrowed the PID-reuse race without closing it.

Handing you a reviewed list does not make that race disappear; a human takes longer to act than a script, so the interval actually grows. What changes is who is exposed and what they can see. You signal a list you have just read, on your own timing, the way anyone uses kill — an ordinary risk people already manage, rather than an automated action taken at scale on a snapshot nobody re-checked.

What the tool does do:

  • Computes and excludes your own process ancestry, so a command it produces cannot target the shell that produced it.
  • Never proposes credential and signing agents (gpg-agent, keyboxd, scdaemon, ssh-agent), CI runners or container shims. They look idle and stale; stopping them breaks in-flight signing or drops a build.
  • Sends anything with incomplete evidence — a missing ancestor, a truncated ancestry walk, a durable install path — to a bucket it explicitly refuses to conclude on.
  • Writes the full list, with ages and complete command lines, and tells you to read it.
  • Writes its report with mode 600 under umask 077, because full command lines can carry tokens, and refuses to write --out through a pre-existing symlink.
  • Pins its PATH to system directories, so a project whose direnv/npm hooks prepend their own bin dirs cannot substitute the tools the audit parses with.
  • Refuses a patterns config that is not owned by you or is writable by anyone else — that file is sourced as shell, so "edit a config" must never quietly mean "run code".
  • Tells the model reading the report that its content is data, never instructions: any process can name itself text shaped like directives, and nothing inside the report changes what the skill is allowed to do. macOS ps escapes control characters, so a command line cannot forge the report's structure — only its own text.

Limits

No GPU or display diagnosis, no SMART or filesystem checks, no malware scanning, no browser-extension analysis, no separating network slowness from machine slowness. This is a fast triage, not a replacement for Activity Monitor.

Memory is a snapshot, so it shows levels and not rates. Enough to rank what to look at; not macOS's own pressure verdict, which also weighs how fast pages are moving.

Thermal reporting depends on your hardware. pmset -g therm often returns nothing on Apple Silicon, in which case the report says so and points you at kernel_task CPU instead.

Anything needing sudo is printed for you to run, never executed.

Requirements

macOS 12 (Monterey) or later, Apple Silicon or Intel.

Nothing beyond what the system already ships: ps, awk, vm_stat, sysctl, pmset, iostat, and the bash Apple bundles — version 3.2, so no Homebrew bash needed. Optional commands are probed before use and skipped with a note. Below macOS 12 the scripts print a note and keep going rather than refusing.

Tested on

macOS 26, Apple Silicon, under Apple's bash 3.2, locally — and macOS 14 and 15 on every commit, in CI on fresh GitHub runners with no dotfiles or Homebrew state the scripts could silently depend on. CI runs both scripts live and exercises every refusal path: world-writable config, malformed regex, incomplete config, symlinked --out.

Classification is verified against fabricated fixtures covering every outcome: an ephemeral orphan reaches PROPOSED, a durable service whose argument mentions ~/.cache reaches ASK, a process with a live controlling ancestor reaches KEEP — including when that ancestor's own command line matches the tool patterns — and 120 simultaneous orphans classify in 0.15 s with no leakage between buckets.

Page size is read from vm_stat rather than assumed, so Intel's 4 KB pages are handled, though not yet on real Intel hardware.

Contributing

Issues and pull requests welcome. Two things make a report immediately actionable:

  • Your sw_vers -productVersion and uname -m.
  • For a misclassification, the relevant line from proposed.txt or ask.txt — it carries the PID, age, full command line and the reason the tool gave.

If a tool you use is not recognised, that is usually a patterns.conf change rather than a code change, and those are the easiest PRs to merge.

Run shellcheck -s bash skills/mac-audit/scripts/*.sh before submitting; the tree is expected to stay at zero findings.

License

MIT

About

Diagnose a struggling Mac, including the orphaned MCP servers agent sessions leave behind. Read-only: it shows the evidence and prints the command.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages