Browse Source

feat(mac-ops): Add sysdiagnose-helper + mac-vs-windows-ops cross-reference

sysdiagnose-helper.sh   Wraps Apple's sysdiagnose tool. Three actions:
                        (default) trigger sysdiagnose under sudo;
                        --list shows existing bundles;
                        --inspect shows what's inside a bundle without
                        extracting + flags sensitive content (hostnames,
                        /Users/ paths, IPs, profiles, TCC.db). Surfaces
                        privacy implications before bundles get shared.

mac-vs-windows-ops.md   Cross-reference doc: side-by-side diagnostic
                        ladders, script equivalents (crash-triage ↔
                        panic-triage etc.), command-translation cheat
                        sheet, "when to use which" decision table.
                        For households with both OSes or support roles
                        that span both.

Tests now: 16 scripts + 10 reference docs + 81-test self-suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter 3 weeks ago
parent
commit
a0b7bbfa1b

+ 1 - 1
.claude-plugin/plugin.json

@@ -1,6 +1,6 @@
 {
   "name": "claude-mods",
-  "version": "2.7.1",
+  "version": "2.7.2",
   "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 78 skills, 2 commands, 6 rules, 4 hooks, 13 output styles, modern CLI tools",
   "author": "0xDarkMatter",
   "repository": "https://github.com/0xDarkMatter/claude-mods",

+ 3 - 0
README.md

@@ -22,6 +22,9 @@ From Python async patterns to Rust ownership models, from AWS Fargate deployment
 
 ## Recent Updates
 
+**v2.7.2** (May 2026)
+- 🩺 **`mac-ops` final polish** - Added `sysdiagnose-helper.sh` — wraps Apple's `sysdiagnose` tool with privacy-aware bundle inspection (`--list`, `--inspect`, plain run). Bundles can be 1-2GB and contain hostnames/usernames/IPs; helper surfaces what's inside before sharing. New `mac-vs-windows-ops.md` cross-reference doc — side-by-side diagnostic ladders, script equivalents, "when to use which" decision table, command-translation cheat sheet for support workflows that span both OSes. Total: 16 scripts, 10 reference docs, 5,411 lines, 81-test self-suite.
+
 **v2.7.1** (May 2026)
 - 🩺 **`mac-ops` expansion** - Three additional diagnostic scripts: `kext-audit.sh` (loaded kexts + system extensions + SIP/security policy state + Apple Silicon `bputil` boot policy), `firewall-audit.sh` (ALF + pf + Network Extension content filters + utun tunnel inventory), `network-locations.sh` (Network Location profiles + per-service DNS/proxy/search-domain + service order). New `apple-silicon-specifics.md` reference catalogs the M1+ vs Intel differences (boot recovery UX, security policy tiers, kexts vs system extensions, panic format, SMC absorption into the SoC). Health-audit's verdict block now has cross-script wayfinding — when a panic / storage / TCC / wake / startup issue is detected, the "Next:" line points at the right drilldown script directly.
 

+ 117 - 0
skills/mac-ops/references/mac-vs-windows-ops.md

@@ -0,0 +1,117 @@
+# mac-ops ↔ windows-ops Cross-Reference
+
+Load this when you need to do the same diagnostic on the other OS, or when a household has both Macs and Windows machines and you want consistent verdict output.
+
+The skills mirror each other deliberately — same diagnostic ladder structure, same `[PASS]/[FAIL]/[WARN]/[INFO]` output, same `--json` / `--redact` / `--quiet` / `--verbose` modes. The differences below are the **OS-specific surface** that determined what each skill calls out.
+
+## Diagnostic ladder side-by-side
+
+| Rung | mac-ops | windows-ops | Notes |
+|---|---|---|---|
+| 1 | Hardware (pmset, SMC events, thermal) | Hardware (WHEA-Logger) | Same purpose, different log surface |
+| 2 | Storage (APFS, IO errors via log show, snapshot bloat) | Storage (disk 7/52/153/154, storahci 129) | Both prioritize storage as the highest-yield audit |
+| 3 | Panic record (DiagnosticReports/*.{panic,ips}) | Crash record (Event 41 + BugCheck code) | Different file/event formats; same triage flow |
+| 4 | Pre-panic timeline | Pre-crash timeline | Identical methodology, different log commands |
+| 5 | Startup inventory (Login Items + LaunchAgents + LaunchDaemons + profiles) | Startup inventory (Run keys + Services + Tasks + Folders + GroupPolicy) | macOS has 4 mechanisms; Windows has 5 |
+| 6 | Resource pressure (CPU/mem snapshots) | Resource pressure | Same kind of check |
+| 7 | **TCC permissions (mac-unique)** | **(no equivalent — Windows uses per-API permission, not centralized)** | The biggest unique-to-mac dimension |
+| 8 | Verdict | Verdict | Same shape, different prompts |
+
+mac-ops has 8 rungs vs windows-ops's 7 because TCC (Transparency, Consent, Control) is enough of a distinct failure mode that it earns its own rung. Windows has no equivalent — per-API permissions exist but aren't routed through a single user-visible system.
+
+## Script equivalents
+
+| windows-ops | mac-ops | Notes |
+|---|---|---|
+| `health-audit.ps1` | `health-audit.sh` | Same orchestrator role |
+| `disk-health.ps1` | `disk-health.sh` | APFS instead of NTFS; `diskutil` instead of `Get-Disk` |
+| `crash-triage.ps1` | `panic-triage.sh` | Event 41 vs `.panic`/`.ips`; same pre-window query pattern |
+| `drive-dependencies.ps1` | `drive-dependencies.sh` | "safe to disconnect?" check on both sides |
+| `safe-disable-startup.ps1` | `safe-disable-startup.sh` | `StartupApproved` byte flip vs `launchctl disable`; both reversible |
+| `recover-clone.ps1` | `recover-clone.sh` | `robocopy /R:0` vs `rsync --partial --inplace --no-whole-file --append-verify --ignore-errors` |
+| `boot-perf.ps1` | `boot-perf.sh` | Diagnostics-Performance log vs unified log |
+| — | `tcc-audit.sh` | mac-unique |
+| — | `wake-reasons.sh` | mac has pmset log; Windows uses `powercfg` (not as detailed) |
+| — | `spotlight-status.sh` | mac-unique (Windows Search has WSearch service but rarely a diagnostic concern) |
+| — | `storage-pressure.sh` | mac-unique (APFS snapshots + local TM are the "where did my disk go?" cause) |
+| — | `kext-audit.sh` | Windows has WDM drivers; loaded via different mechanism, not panic-prone in the same way |
+| — | `firewall-audit.sh` | Windows has its own firewall stack — different audit. Could exist as `windows-ops/firewall-audit.ps1` but isn't yet. |
+| — | `network-locations.sh` | mac-unique — Network Locations is a macOS feature |
+| — | `sysdiagnose-helper.sh` | mac-unique (Windows has `dxdiag` and `msinfo32` but different scope) |
+
+## Reference doc equivalents
+
+| windows-ops | mac-ops | Notes |
+|---|---|---|
+| `storage-events.md` | `storage-events.md` | Event IDs catalog vs log-show patterns |
+| `bugcheck-codes.md` | `panic-codes.md` | BSOD stop codes vs panic strings + kext provenance |
+| `startup-mechanisms.md` | `startup-mechanisms.md` | 5 Windows mechanisms vs 4 macOS |
+| `recovery-patterns.md` | `recovery-patterns.md` | BCD/Windows RE vs recoveryOS/Share Disk |
+| `remote-diagnostics.md` | `remote-diagnostics.md` | WS-Man/PSRemoting vs SSH |
+| — | `tcc-mechanics.md` | mac-unique |
+| — | `launchd-deep-dive.md` | mac-unique (Windows has SCM + Task Scheduler, conceptually different) |
+| — | `apple-silicon-specifics.md` | mac-unique |
+
+## Conventions in common
+
+- **Verdict-first output**: every script ends with a SUMMARY block + a "Next:" hint pointing at the right drilldown
+- **Reversible operations**: `disable` ops always have a corresponding `--enable`; no script destroys data without `--apply`
+- **Cardinal rules per OS**:
+  - mac-ops: never `fsck_apfs -y` a failing drive
+  - windows-ops: never `chkdsk /f` a failing drive
+  - Both: image first, repair second
+- **JSON output as a contract**: stdout is pure NDJSON; stderr may have noise. Use `2>/dev/null` when piping to `jq`.
+- **Opsec mode**: `--redact` masks RFC1918 / CGNAT / link-local / MAC / tailnet names / UUIDs on both skills
+- **Cross-platform addresses preserved**: Tailscale's `100.100.100.100` and public DNS resolvers stay visible as diagnostic anchors
+
+## When to use which
+
+| You have... | Use |
+|---|---|
+| A Windows machine with kernel crashes | `windows-ops/scripts/crash-triage.ps1` |
+| A Mac with kernel panics | `mac-ops/scripts/panic-triage.sh` |
+| A networking problem on either | `net-ops` (cross-platform) |
+| A failing drive on either | mac/windows-ops `disk-health` then `recover-clone` |
+| App can't access mic / screen / files on Mac | `mac-ops/scripts/tcc-audit.sh` (no Windows analog) |
+| Slow boot on either | mac/windows-ops `boot-perf` |
+| Mac wakes at 3am | `mac-ops/scripts/wake-reasons.sh` (no Windows analog — `powercfg /lastwake` is similar but less detailed) |
+| Mixed-OS household, same support issue | Run both skills' `health-audit` and diff |
+
+## Differences that matter for diagnosis
+
+1. **Windows tells you BugCheck codes**: `0xEF`, `0xD1`, `0x124` — searchable, classifiable, well-documented
+   **macOS gives panic strings**: free-form text, must pattern-match (catalog in `panic-codes.md`)
+
+2. **Windows storage signal is event-ID based**: stable across versions, single-shot grep
+   **macOS storage signal is in unified log**: rich but slow to query; `log show --last 30d` takes 30-60s
+
+3. **Windows has 5 startup mechanisms**: registry Run keys, services, scheduled tasks, startup folders, group policy
+   **macOS has 4**: Login Items, LaunchAgents (user + system), LaunchDaemons, legacy LoginHook
+
+4. **Windows kernel-extension panics are common**: third-party drivers ship as kexts/WDM
+   **macOS Apple Silicon panics are rarer**: kexts deprecated; system extensions run user-mode
+
+5. **macOS has TCC, Windows doesn't**: privacy permissions are a *first-class diagnostic concern* on mac, not on Windows
+
+6. **macOS has APFS snapshots + local TM**: "disk full" is often purgeable space; Windows doesn't have the same hidden-allocation surface
+
+7. **Remote diagnostics**: Windows has PSRemoting / WinRM (rich); macOS is SSH-only (simpler)
+
+## Same diagnostic, different commands
+
+| What | Windows | macOS |
+|---|---|---|
+| List loaded drivers/kexts | `driverquery` | `kextstat` / `kmutil showloaded` |
+| List running services | `Get-Service` | `launchctl print` |
+| Check kernel crash log | `Get-WinEvent -Id 41` | `ls /Library/Logs/DiagnosticReports/*.panic` |
+| Check disk SMART | `Get-PhysicalDisk` reliability counter | `diskutil info` or `smartctl` (brew) |
+| Disable startup app | StartupApproved registry byte | `launchctl disable` |
+| Boot to recovery | `Cmd-R` (Mac on Intel) / power button (Apple Silicon) | `Cmd-R` / power button |
+| Verbose boot | (always shows on BSOD) | `nvram boot-args="-v"` |
+| Reset SMC | Shift-Ctrl-Option-Power 10s (Intel only) | n/a on Apple Silicon (SoC handles it) |
+
+## Cross-references
+
+- `mac-ops/SKILL.md` — the macOS diagnostic skill
+- `windows-ops/SKILL.md` — the Windows diagnostic skill
+- `net-ops/SKILL.md` — networking layer (both OSes)

+ 153 - 0
skills/mac-ops/scripts/sysdiagnose-helper.sh

@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+# mac-ops :: sysdiagnose-helper.sh
+# Run Apple's sysdiagnose tool, inspect its output, and prepare a sanitized
+# version for sharing.
+#
+# sysdiagnose captures the WORKS — unified log dumps, system_profiler, kext
+# inventory, process listings, network state, IOReg, accessibility config,
+# spindump, etc. The output is huge (often 500MB-2GB compressed) and contains
+# personal data, hostnames, paths under /Users/, network info. Don't share
+# without inspection.
+
+set -u
+
+ACTION="run"
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --inspect) ACTION="inspect"; shift ;;
+        --inspect=*) ACTION="inspect"; BUNDLE="${1#--inspect=}"; shift ;;
+        --list) ACTION="list"; shift ;;
+        --help|-h)
+            cat <<EOF
+Usage: $0 [options]
+
+  (no args)            Trigger sysdiagnose; report where the bundle is written
+  --list               List existing sysdiagnose bundles on this machine
+  --inspect[=PATH]     Inspect a bundle and report what it contains
+  --json, --redact, --quiet, --verbose
+
+Apple's sysdiagnose:
+  Bundle location: /var/tmp/sysdiagnose_*.tar.gz
+  Trigger via:
+    sudo sysdiagnose                   CLI, prompts for trigger
+    Option-Cmd-Ctrl-Shift-.            Keyboard chord (system-wide)
+
+What's in a sysdiagnose bundle (high level):
+  - Full unified log dump (last few hours / days)
+  - system_profiler full report
+  - kextstat / kext info
+  - Process listing + memory usage
+  - Network state (ifconfig, netstat, route, scutil)
+  - pmset state and log
+  - DiskUtil and APFS state
+  - DiagnosticReports/ (crashes + panics)
+  - Spindump (samples of running processes)
+  - IORegistry dump
+  - Configuration profiles
+
+Privacy: bundles contain hostnames, usernames, paths under /Users/, IP
+addresses, sometimes app-specific identifiers. Inspect before sharing.
+EOF
+            exit 0 ;;
+        *) shift ;;
+    esac
+done
+
+source "$(dirname "$0")/_lib/common.sh"
+parse_common_flags "$@"
+maybe_filter_self "$@"
+
+case "$ACTION" in
+    list)
+        section "1. EXISTING SYSDIAGNOSE BUNDLES"
+        bundles=$(ls -lt /var/tmp/sysdiagnose_*.tar.gz 2>/dev/null | head -10)
+        if [[ -z "$bundles" ]]; then
+            log_info "Sysdiagnose bundles" "none found in /var/tmp"
+        else
+            count=$(echo "$bundles" | wc -l | tr -d ' ')
+            log_info "Sysdiagnose bundles" "$count found"
+            echo "$bundles" | sed 's/^/  /'
+        fi
+        ;;
+
+    inspect)
+        section "1. BUNDLE INSPECTION"
+        BUNDLE="${BUNDLE:-$(ls -t /var/tmp/sysdiagnose_*.tar.gz 2>/dev/null | head -1)}"
+        if [[ -z "$BUNDLE" ]] || [[ ! -f "$BUNDLE" ]]; then
+            log_fail "Bundle" "not found: $BUNDLE"
+            exit 3
+        fi
+        log_pass "Bundle" "$BUNDLE"
+        size=$(ls -lh "$BUNDLE" 2>/dev/null | awk '{print $5}')
+        note "  Size: $size"
+
+        # Probe contents without extracting
+        note ""
+        note "  Top-level contents:"
+        tar tzf "$BUNDLE" 2>/dev/null | awk -F/ '{print $1"/"$2}' | sort -u | head -20 | sed 's/^/    /'
+
+        # Sensitive contents inventory
+        note ""
+        note "  Potentially sensitive contents:"
+        for pattern in "logarchive" "DiagnosticReports" "system_profiler" "ifconfig" "scutil" "profiles" "TCC"; do
+            count=$(tar tzf "$BUNDLE" 2>/dev/null | grep -c "$pattern" || echo 0)
+            count="${count:-0}"
+            if [[ "$count" -gt 0 ]]; then
+                printf "    %-25s %s files\n" "$pattern" "$count"
+            fi
+        done
+
+        note ""
+        note "  Before sharing this bundle:"
+        note "    1. Extract: tar xzf $BUNDLE -C /tmp/inspect"
+        note "    2. Review: less /tmp/inspect/sysdiagnose_*/system_profiler.spx"
+        note "    3. Search for sensitive content: grep -r 'private-data-pattern' /tmp/inspect"
+        note "    4. If sharing publicly: use redaction tools (BBEdit, sed) on extracted log files first"
+        ;;
+
+    run)
+        section "1. PRECONDITION CHECK"
+        # sysdiagnose needs sudo
+        if ! sudo -n true 2>/dev/null; then
+            log_warn "sudo" "this script needs sudo to invoke sysdiagnose"
+            note "  Run with: sudo bash $0"
+            note "  Or trigger via keyboard chord: Option-Cmd-Ctrl-Shift-."
+            note "  Or: sudo sysdiagnose -f /tmp/   (saves to /tmp instead of /var/tmp)"
+            emit_summary
+            exit 5
+        fi
+
+        log_pass "sudo" "available"
+
+        # Free space check
+        free_mb=$(df -m /var/tmp 2>/dev/null | awk 'NR==2{print $4}')
+        if [[ "${free_mb:-0}" -lt 2048 ]]; then
+            log_warn "Free space on /var/tmp" "${free_mb} MB — sysdiagnose may need 1-2 GB"
+        else
+            log_pass "Free space on /var/tmp" "${free_mb} MB"
+        fi
+
+        section "2. RUNNING SYSDIAGNOSE"
+        note "  This takes 5-15 minutes and produces a large bundle in /var/tmp."
+        note "  Skipping the privacy prompt with -u (no UI) and not generating a profile (-Q):"
+        note ""
+        sudo sysdiagnose -u -Q -A "macops-helper" 2>&1 | tail -10 | sed 's/^/    /'
+
+        bundle=$(ls -t /var/tmp/sysdiagnose_*.tar.gz 2>/dev/null | head -1)
+        if [[ -n "$bundle" ]] && [[ -f "$bundle" ]]; then
+            log_pass "Bundle written" "$bundle"
+            size=$(ls -lh "$bundle" | awk '{print $5}')
+            note "  Size: $size"
+            note ""
+            note "  Next:"
+            note "    bash $0 --inspect           # see what's in the bundle"
+            note "    bash $0 --list              # all bundles on this machine"
+            note "  To share with Apple support, upload directly via Apple Feedback Assistant."
+        else
+            log_warn "Bundle" "not found after sysdiagnose ran"
+        fi
+        ;;
+esac
+
+emit_summary

+ 2 - 0
skills/mac-ops/tests/run.sh

@@ -174,6 +174,7 @@ expected_scripts=(
     disk-health.sh drive-dependencies.sh boot-perf.sh recover-clone.sh
     tcc-audit.sh wake-reasons.sh spotlight-status.sh storage-pressure.sh
     kext-audit.sh firewall-audit.sh network-locations.sh
+    sysdiagnose-helper.sh
 )
 for s in "${expected_scripts[@]}"; do
     assert "script exists: $s" test -f "$root/scripts/$s"
@@ -188,6 +189,7 @@ expected_refs=(
     storage-events.md recovery-patterns.md tcc-mechanics.md
     launchd-deep-dive.md panic-codes.md startup-mechanisms.md
     remote-diagnostics.md apple-silicon-specifics.md
+    mac-vs-windows-ops.md
 )
 for r in "${expected_refs[@]}"; do
     assert "reference exists: $r" test -f "$root/references/$r"