mac-ops · git:20261005.e2f07a1 · 2026-10-05 · sha256 a92a4d3c7b3d84d8

mac-ops git:20261005.e2f07a1A

Immutable. This exact content never changes and is served at /api/v1/blob/a92a4d3c7b3d84d8.

---
name: mac-ops
description: "macOS workstation diagnostics: kernel panics, failing drives, launchd startup audit, wake reasons, TCC denials, APFS snapshots. Triggers on: mac is slow, won't boot, kernel panic, login loop, drive failing, launchd, TCC permissions, Full Disk Access, Gatekeeper, sysdiagnose."
license: MIT
allowed-tools: "Read Write Bash"
metadata:
  author: claude-mods
  related-skills: windows-ops, net-ops, debug-ops, perf-ops
---

# mac-ops

> Verified as of 2026-07 — content assumes macOS 13 (Ventura) and later (`.ips` panic format on macOS 12+, universal SSH on macOS 13+).

## Helps with

Slow Mac, silent drive failure, unexplained panics, TCC denials, a full disk, 3am wakes, runaway system daemons, login loops, safe eject, failing-drive cloning, remote and Apple Silicon diagnostics. Where each signal lives: [references/symptom-guide.md](references/symptom-guide.md).

## The Universal Insight

**macOS tells you what's wrong if you ask the right log in the right way.** Most users (and most tutorials) reach for Activity Monitor or "About This Mac". The actual diagnostic signal lives in `log show` (the unified logging system), in `/Library/Logs/DiagnosticReports/`, in `pmset -g log`, in the TCC databases, and in `launchctl print`. This skill packages the queries that turn noise into a verdict.

The most common diagnostic failure: treating symptoms in isolation. "Slow boot" → disable login items. "Kernel panic" → reinstall macOS. "Random freezes" → reset SMC/NVRAM. These are reasonable last resorts, but the data to identify the *actual* cause is sitting in the unified log untouched. Always audit before treating.

## The Diagnostic Ladder

Walk down the layers in order. Each rung has a binary outcome:

```
1. Hardware health     — pmset, SMC errors, thermal events, Secure Enclave
2. Storage health      — APFS state, IO errors, snapshot bloat
3. Panic record        — DiagnosticReports/*.{panic,ips} + kext provenance
4. Pre-panic timeline  — log show last 10 minutes before each panic
5. Startup inventory   — Login Items + LaunchAgents + LaunchDaemons + profiles
6. Resource pressure   — top CPU/mem, mds_stores, photoanalysisd, cloudd
7. Permissions / TCC   — what app is denied what (the macOS-unique rung)
8. Verdict             — what's failing, what to do
```

The most interesting failures cluster at rungs 2 (storage), 5 (startup bloat), and 7 (TCC denials). The least interesting (but most-treated) is rung 6.

## Workflow

### 1. Run the comprehensive audit

```bash
scripts/health-audit.sh
```

Produces a verdict block: hardware events, storage health per volume, recent panics, top resource consumers, startup inventory, TCC denials. Scan for `[FAIL]` markers — that's where to drill.

### 2. Drill into the failing layer

| Symptom | Script |
|---|---|
| Storage errors flagged | `scripts/disk-health.sh -v /Volumes/X` (or `-d disk2`) — focused per-volume deep dive: APFS state, IO errors, snapshot bloat, verdict |
| Recent panic | `scripts/panic-triage.sh` (latest by default) or `-f /Library/Logs/DiagnosticReports/Kernel_*.panic` — kext + pre-panic timeline |
| "Is it safe to eject volume X?" | `scripts/drive-dependencies.sh -v /Volumes/X` — open files, Spotlight index, TM target, Photos lib, helper-tool bookmarks |
| "Why is boot taking so long?" | `scripts/boot-perf.sh` — per-boot durations from log show, with slow-component flags |
| App can't see screen/mic/files | `scripts/tcc-audit.sh -a <bundle-id-or-name>` — what TCC has granted, what's been denied recently |
| Mac waking at night | `scripts/wake-reasons.sh` — pmset log breakdown by reason class |
| Spotlight broken / mds CPU spike | `scripts/spotlight-status.sh` — index state per volume, common fixes |
| Storage "full" but disk usage doesn't add up | `scripts/storage-pressure.sh` — APFS snapshots, local Time Machine, purgeable bytes |
| Kernel panic blames a kext / loaded kext audit | `scripts/kext-audit.sh` — third-party kexts + system extensions + SIP/security policy state |
| Firewall behavior / VPN tunnel inventory | `scripts/firewall-audit.sh` — ALF + pf + Network Extension content filters + utun inventory |
| Network preferences across location profiles | `scripts/network-locations.sh` — DNS / proxy / search domains per location, service order |

### 3. Apply the minimum reversible fix

| Action | Script |
|---|---|
| Disable startup item by name | `scripts/safe-disable-startup.sh -n <pattern>` — works across Login Items + LaunchAgents (no sudo for user-scope) |
| List current state of all startup entries | `scripts/safe-disable-startup.sh --list` |
| Re-enable previously disabled | `scripts/safe-disable-startup.sh -n <pattern> --enable` |
| Disable system-scope daemon (admin) | `sudo launchctl disable system/<label>` then `sudo launchctl bootout system/<label>` |
| Reset TCC for a specific service+bundle | `tccutil reset <Service> <bundle-id>` (per-service, not global) |
| Safe clone from failing drive | `scripts/recover-clone.sh -s <source> -d <destination>` — rsync `--partial --inplace --no-whole-file` |

All disables are reversible — Login Items via `osascript` System Events, LaunchAgents via `launchctl disable`. The inverse re-enables.

## Storage Health & Failure Detection

The highest-yield audit. Failing drives cause slow boots (kernel waits on probe timeouts), instability (IO retries cascade into kernel hangs), and panics (paging failures kill `WindowServer`). Three independent data sources to cross-reference:

### IO error events

```bash
log show --last 30d --style compact \
    --predicate 'subsystem == "com.apple.iokit" AND (eventMessage CONTAINS "I/O error" OR eventMessage CONTAINS "media error")' \
    2>/dev/null | head -30
```

Healthy drives produce zero of these per month. Dozens = active failure regardless of what "About This Mac → Storage" claims.

### APFS health

```bash
diskutil apfs list
diskutil apfs verifyVolume /        # READ-ONLY — does not write
```

Look for `Verify failed` per-volume, container free-space mismatches, or snapshot trees growing without bound.

### SMART status (via vendor / smartmontools)

macOS's built-in SMART status (`diskutil info /dev/diskN`) reports only `Verified` or `Failing`. For real attributes, install `smartmontools` (`brew install smartmontools`) and use `smartctl -a /dev/diskN`. NVMe drives often return blank — fall back to vendor tools or the per-vendor utilities.

### Disk → volume mapping

```bash
diskutil list
diskutil info disk2s1
```

Cross-reference with `df -h` for mount point.

## Boot Performance & Startup Management

macOS has **four primary startup mechanisms**, each requiring different tooling. System Settings only shows one of them (Login Items). Full inventory in `references/startup-mechanisms.md`.

| Mechanism | Where | How to inspect | How to disable |
|---|---|---|---|
| Login Items | System Settings → General → Login Items | `osascript` System Events | `osascript` (no sudo) |
| User LaunchAgents | `~/Library/LaunchAgents/*.plist` | `launchctl print gui/$UID` | `launchctl disable gui/$UID/<label>` |
| System LaunchAgents (per-user) | `/Library/LaunchAgents/*.plist` | `launchctl print gui/$UID` | `launchctl disable gui/$UID/<label>` (no sudo for current user) |
| System LaunchDaemons | `/Library/LaunchDaemons/*.plist` | `sudo launchctl print system` | `sudo launchctl disable system/<label>` |
| (Legacy) LoginHook | `com.apple.loginwindow` LoginHook key | `sudo defaults read com.apple.loginwindow LoginHook` | `sudo defaults delete com.apple.loginwindow LoginHook` |

### `launchctl disable` vs `bootout`

| Command | Effect |
|---|---|
| `launchctl disable <domain>/<label>` | Persistently disabled across reboots. **Reversible** with `enable`. |
| `launchctl bootout <domain>/<label>` | Unloads the running service NOW. Comes back on next reboot if not also `disable`d. |
| `launchctl unload <plist>` | Legacy form. Avoid in new scripts. |

The right pair for "kill this daemon permanently": `disable` then `bootout`. The script `scripts/safe-disable-startup.sh` does both.

### Boot duration measurement

macOS records boot timing in the unified log. Approximate via:

```bash
log show --last 1h --style compact \
    --predicate 'eventMessage CONTAINS "BOOT_TIME" OR eventMessage CONTAINS "loginwindow"' \
    | head -50
```

Healthy Apple Silicon Mac: 10-20s to login screen. Intel Mac with spinning disk (vintage 2015 Mini): 25-45s. Failing storage: 60+s with stalls.

## Panic Analysis & Diagnostic Reports

### Where panics live

```
/Library/Logs/DiagnosticReports/*.panic          (legacy Intel + early Apple Silicon)
/Library/Logs/DiagnosticReports/*.ips            (modern format, all panics on macOS 12+)
~/Library/Logs/DiagnosticReports/                (per-user crashes, not panics)
```

`.ips` files are JSON. The `.panic` files are plain text but follow a strict structure.

### Anatomy of a panic report

```
panic(cpu N caller 0x...): "Sleep wake failure in EFI"
Loaded kexts:
  com.apple.driver.AppleEFIRuntime           ...
  com.eltima.ProductX                        2.1.7    <— third-party suspect
  ...
```

The **kext list** is the most actionable signal. A panic that loaded only Apple kexts is harder to fix than one with a clear third-party suspect — kext-extraction-and-removal is the first move.

### Common panic strings (full catalog in `references/panic-codes.md`)

| String fragment | Likely cause |
|---|---|
| "Sleep wake failure" | Driver hung during S3/S4 transition (often USB, Bluetooth, GPU) |
| "Unable to find driver" | Boot-time kext load failure — likely after macOS update |
| "Unresponsive bootstrap subsystem" | `launchd` deadlock — usually third-party LaunchDaemon |
| "WindowServer panic" | GPU driver or display kext fault |
| "double_fault" / "page_fault" | Kernel-mode memory corruption — kext bug or RAM fault |
| "panic_kthread" | Kernel watchdog timeout — driver in infinite loop |

### Pre-panic timeline

The panic record alone rarely tells you the cause. The **events in the 10 minutes before** are where the story is:

```bash
scripts/panic-triage.sh -t '2026-05-15 03:14:22' -m 10
```

Look for:
- Disk arbitration errors before panic → storage failure cascade
- `kernel` warnings naming a third-party kext before panic → driver hang
- `powerd` / `assertion` messages before sleep panic → pmset assertion held by misbehaving app

## TCC (Privacy Permissions) Audit

A macOS-unique diagnostic layer. The TCC databases at:

```
/Library/Application Support/com.apple.TCC/TCC.db     (system, requires sudo)
~/Library/Application Support/com.apple.TCC/TCC.db    (per-user)
```

…store every "Allow / Deny" grant ever made. Reading them tells you exactly which app has Screen Recording, Camera, Microphone, Full Disk Access, Accessibility, Automation, etc. — and which apps have been **denied** recently (the most common "this app doesn't work" cause).

Full schema and reset procedures in `references/tcc-mechanics.md`. The `scripts/tcc-audit.sh` script wraps the common queries.

## Common Failure Modes

| Symptom | First check | Common cause |
|---|---|---|
| Slow boot, used to be fast | `startup-audit.sh` | Login Item / LaunchAgent bloat (Adobe CC, Docker, Setapp) |
| Slow boot, getting worse | `disk-health.sh` | Failing SSD — APFS retries inflating boot time |
| Random freezes + hard restart | `disk-health.sh` + `panic-triage.sh` | IO errors cascading into kernel hang |
| Kernel panic on wake | `panic-triage.sh` (look for "Sleep wake failure") | Driver power-state bug (often USB, GPU, Bluetooth) |
| App can't access screen/mic | `tcc-audit.sh -a <app>` | TCC denial, often from a recent system update |
| "Macintosh HD almost full" | `storage-pressure.sh` | Local Time Machine snapshots + purgeable cache |
| Mac wakes at 3am | `wake-reasons.sh` | Scheduled maintenance, BT keyboard tap, or kext bug |
| `mds_stores` CPU 100% | `spotlight-status.sh` | Reindex on volume with no on-disk index store |
| Login loop / gray screen | recoveryOS + safe boot + this skill's recovery docs | Bad LaunchAgent, corrupt Login Items, kext panic at boot |

## Recovery Patterns

### Failing-drive data recovery

**Never `fsck_apfs -y` a failing drive** — `-y` answers Yes to repairs, which writes back. Use `fsck_apfs -n` (verify-only) first. Image first, repair the image second.

```bash
# Safe clone with no retries (skips bad sectors fast)
rsync -avh --partial --inplace --no-whole-file --append-verify \
    /Volumes/Failing/important/ /Volumes/Rescue/important/
```

For bit-level recovery, install `gddrescue` via Homebrew and use a map file so the operation is resumable. Documented in `references/recovery-patterns.md`.

### Physically removing a failing drive

If a drive is causing boot stalls or panics:

1. Identify via `disk-health.sh`
2. Verify nothing critical points at it (`drive-dependencies.sh -v /Volumes/X`)
3. `diskutil unmount /Volumes/X` (or `diskutil eject /dev/diskN` for the whole device)
4. Physically disconnect / power down before remount attempts

## Voice & Output Style

Output follows the claude-mods diagnostic convention:

- `[PASS]` / `[FAIL]` / `[WARN]` / `[INFO]` prefixes for scan rows
- Verdict block at the bottom with specific findings + recommended actions
- Volume identifications include disk identifier, mount point, APFS role
- Panic references include UTC timestamp, panic string, primary suspect kext
- Panel rendering via `skills/_lib/term.sh` when a TTY is present; raw text otherwise
- `--json` emits NDJSON for piping; `--redact` masks private addrs / hostnames / serial numbers

## What This Skill Doesn't Cover

- **Network diagnostics** → use `net-ops`
- **Windows-side workstation issues** → use `windows-ops`
- **Specific application performance profiling** → use `perf-ops`
- **Source-code-level debugging** → use `debug-ops`
- **iOS / iPadOS device issues** — different platform
- **MDM authoring** (creating configuration profiles) — out of scope; we read them, not author them

## Cross-References

| When | Use |
|---|---|
| Triaging a remote Mac | `net-ops/ssh-bootstrap.sh` to land, then this skill's scripts |
| Panic blames a network kext | Combine with `net-ops` for VPN/DNS interactions |
| Same pattern on multiple Macs | Run `health-audit.sh --json` on each, diff outputs |
| Suspect Windows + Mac in same household | `windows-ops` + `mac-ops`, same conventions |

## References

- `references/storage-events.md` — IO error patterns in unified log, APFS-specific event vocabulary, disk arbitration messages. Load when investigating volume errors or correlating IO failures to a specific device.

- `references/panic-codes.md` — Common kernel panic strings, kext-extraction patterns, Apple Silicon vs Intel panic format differences. Load when decoding a non-trivial `.panic`/`.ips` file or matching a symptom to a likely cause.

- `references/startup-mechanisms.md` — Deep dive on Login Items, LaunchAgents (user + system), LaunchDaemons, legacy LoginHook, configuration profile login items. Load when doing a full startup audit or hunting vendor-installed auto-launch hooks.

- `references/recovery-patterns.md` — Failing-drive recovery (rsync, gddrescue), APFS snapshot rollback, target disk mode, recoveryOS, single-user mode on Apple Silicon vs Intel. Load when responding to "my drive is dying" or any destructive operation.

- `references/remote-diagnostics.md` — SSH staging pattern (`scp -r mac-ops/ remote:`), `kickstart` for enabling ARD, sudo over SSH considerations. Load when troubleshooting "my parents' Mac across town".

- `references/tcc-mechanics.md` — How TCC works under the hood. Both TCC.db locations, "service" string catalog (kTCCServiceScreenCapture, kTCCServiceAccessibility, etc.), grant types, when to use `tccutil reset` vs editing the DB, SIP interaction. Load when an app silently fails to access screen / mic / files.

- `references/launchd-deep-dive.md` — launchd plist semantics, `RunAtLoad` vs `KeepAlive`, `ThrottleInterval`, why daemons fail to load, `disable` vs `bootout` vs `unload`, domain targets (system, user, gui), and Apple Silicon specifics (system extensions replacing kexts).

- `references/symptom-guide.md` — Symptom-by-symptom index: what each complaint usually is, which log or database holds its signal, and the misleading first instinct. Load when a report is vague and you need the starting rung.

## Worked example

A 3am-wake, slow-by-day Mac taken end to end through the `health-audit.sh` panel and the drill-down scripts, with the full command sequence: [references/worked-examples.md](references/worked-examples.md#worked-example).