Skip to content

Modernize MacOS-Maid: full Python rewrite (v1.0.0) - #4

Merged
jgamblin merged 45 commits into
masterfrom
modernize-v1
Apr 29, 2026
Merged

Modernize MacOS-Maid: full Python rewrite (v1.0.0)#4
jgamblin merged 45 commits into
masterfrom
modernize-v1

Conversation

@jgamblin

@jgamblin jgamblin commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Overview

Full modernization of MacOS-Maid: the 2018 bash script is replaced by a Python 3.10+ package with a Click CLI, YAML config, safety-first defaults, and 13 modules spanning disk cleanup and security auditing.

Safety fixes from review

Three rounds of review (enterprise IT, Python expert, adversarial) shaped the final design. Headline fixes:

  • WiFi retentionkeep_days was dead code in an earlier pass and defaulted to wipe-all. Now honors a 90-day retention window.
  • system_cache allowlist — previously iterated all of ~/Library/Caches. Now uses a strict allowlist (Xcode / yarn / nsurlsessiond) plus diagnostic reports only. Never touches /private/var/folders.
  • Per-module enabled flag on all 13 modules for fleet/MDM control.
  • Finding.severity typed as a Severity enum with back-compat str coercion.
  • Homebrew update / upgrade default to False — never installs software without explicit opt-in.
  • Privacy requires_sudo lowered to False (SFL2 / TCC / Downloads are all user-scoped).
  • SystemCacheModule paths computed lazily to avoid import-time Path.home() issues under sudo -H / containers.

Architecture

  • src/macos_maid/modules/ — 13 modules subclassing Module from base.py
  • MaidConfig with 13 per-module typed @dataclasses (HomebrewConfig, WiFiConfig, GitConfig, …) — no more .get("key", default) in module wiring
  • Runner orchestrates module execution with sudo gating and audit logging
  • Reporter renders results to terminal / JSON / markdown
  • Severity levels modeled as an IntEnum with worst_severity() helper

CLI

Eight commands: clean, audit, report, list, init, version, log, config. Safety-first defaults (dry-run, no sudo unless requested). Smoke-tested on macOS 26 Tahoe.

Quality gates

  • 198 tests, 84% coverage (pytest --cov=macos_maid)
  • ruff check . clean
  • mypy src/ clean (strict mode)
  • CI matrix: macos-15 + macos-26 (Tahoe) × Python 3.10 / 3.11 / 3.12, GitHub Actions v6 (Node 24)
  • Drift test guards against divergence between DEFAULT_CONFIG dict and dataclass defaults

What changed from the original bash

  • Replaced maid.sh with a modular Python package
  • Removed unsafe operations: diskutil secureErase freespace, /private/var/folders deletion, hardcoded SSID blacklist
  • Added security audit coverage: SIP, FileVault, Gatekeeper, XProtect, firewall, app signing, launch daemons/agents, TCC permissions
  • Added optional integrations: osquery, lynis, knockknock
  • Added Severity enum, typed config, audit log, JSON/markdown reporters

Breaking changes

N/A — this is a clean rewrite replacing the old bash entrypoint.

🤖 Generated with Claude Code

jgamblin and others added 30 commits April 12, 2026 16:13
Remove old maid.sh bash script (preserved in git history).
Set up src layout with pyproject.toml, click/pyyaml/rich deps,
and dev tooling (pytest, ruff, mypy).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ScanResult, CleanResult, Finding, AuditResult dataclasses
with .empty() factory methods. Abstract Module base class
with scan/clean/audit interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs every destructive action with module, detail, and bytes
reclaimed. Saves to ~/.maid/last_run.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detects arch, macOS version, filesystem type, Homebrew prefix,
and WiFi interface. Adapts for Intel vs Apple Silicon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deep merges user config with defaults. Safety-critical values
default to off (git cleanup, docker stopped containers, etc).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Filters by category or module name, skips sudo modules unless
--sudo flag is set, logs all actions to audit log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Formats cleanup and audit results for terminal, JSON, or
markdown output. Includes platform header and byte formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ands

Click-based CLI with --dry-run, --sudo, --dev, --security flags,
category/module filtering, and config file support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements HomebrewModule with scan, clean, and audit methods:
- scan() checks for outdated packages and cache size
- clean() runs brew update, upgrade, and cleanup --prune=all
- audit() returns empty (no security checks)
- Returns empty results if brew is not installed

Added 5 comprehensive tests covering metadata, scan (installed and not installed), clean, and audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements trash cleanup module that reports trash size via du -sk and
empties trash via AppleScript. Module includes scan(), clean(), and
audit() methods with full error handling.

Tests use mocked _get_trash_size and _empty_trash functions. All 6 tests
pass: metadata, scan with items, scan empty, clean success, clean error,
and audit empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SystemCacheModule for safe cleanup of:
- User Library caches (~/Library/Caches)
- System diagnostic reports (/Library/Logs/DiagnosticReports)
- User diagnostic reports (~/Library/Logs/DiagnosticReports)

SAFETY: Module explicitly avoids /private/var/folders which the
original macos-maid script dangerously cleaned.

Tests include critical safety check that verifies NONE of the
directories contain /private/var/folders.

Module requires sudo for system log access, category="both".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements GitModule for cleaning up git repositories with safety features:
- Disabled by default, requires explicit configuration
- Prunes remote branches and optionally deletes merged local branches
- Never deletes protected branches (main, master, develop)
- Reports large repositories when enabled
- Only deletes branches that are merged into the default branch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds WiFi module to clean up stale wireless networks with safety
features. ALWAYS keeps currently connected network regardless of config.

Features:
- Configurable retention period (default: 90 days)
- Network allowlist (keep_ssids)
- Hardcoded safety: never removes current network
- Uses networksetup commands for network management

Tests include coverage for:
- Current network protection (always kept)
- Allowlist functionality
- Empty/stale network detection
- Error handling
- Command parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add read-only audit module that checks macOS security settings:
- System Integrity Protection (SIP)
- FileVault disk encryption
- Gatekeeper
- XProtect malware protection
- Application Firewall

Module returns empty for scan() and clean() (audit-only).
audit() runs all 5 checks and determines worst status (fail > warn > pass).
All checks include remediation guidance for failed/warn states.

Includes comprehensive test coverage with mocked subprocess calls.
Added SystemIntegrityModule to module registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements NetworkModule with DNS cache flushing and network security audits:
- scan(): Reports DNS cache flush action
- clean(): Flushes DNS cache using dscacheutil and mDNSResponder
- audit(): Checks firewall status, open TCP ports, and VPN profiles

Security checks include:
- macOS Application Firewall status (pass/fail)
- Open TCP listening ports with process names (info)
- Configured VPN profiles (info)

Module requires sudo for DNS flush operations.
Tests mock all system commands and verify:
- Module metadata and category
- DNS flush success and error handling
- Firewall enabled/disabled detection
- Open ports and VPN profile reporting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add read-only audit module that flags non-Apple launch daemons and agents.
Never removes or disables them. Scans /Library/LaunchDaemons, /Library/LaunchAgents,
and ~/Library/LaunchAgents for .plist files and reports non-Apple items as info findings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements privacy and security auditing with TCC permission tracking and Downloads cleanup awareness. Module is read-only by default and never deletes Downloads files without explicit configuration.

Features:
- TCC permissions audit (reads user-level TCC.db in read-only mode)
- Recent items clearing (optional, configured via clear_recent flag)
- Old Downloads reporting (report-only by default, never deletes)
- Configurable thresholds for Downloads age tracking

Safety:
- Never requests Full Disk Access
- Downloads are report-only by default
- TCC database opened in read-only mode
- No destructive operations without explicit configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ToolsModule to integrate with external security tools:
- Lynis: System hardening audit with hardening index and suggestions
- osquery: Targeted queries for unsigned processes and network listeners
- KnockKnock: Persistent malware scanning (manual usage)

Module is audit-only (no cleanup operations) and never installs tools.
Suggests installation via Homebrew if tools are not found.
Config keys: lynis_enabled, osquery_enabled, knockknock_enabled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements read-only security audit for installed applications. Scans /Applications for .app bundles and checks code signing status with tiered warnings:
- Signed or App Store apps marked as ok
- Unsigned apps without elevated permissions marked as info
- Unsigned apps with elevated permissions marked as warn

Checks include codesign verification, App Store origin detection, and heuristic checks for elevated permissions via helper tools and privileged executables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive supporting documentation for MacOS Maid v1.0.0:

- CHANGELOG.md: Keep a Changelog format with v1.0.0 release notes
  documenting the complete Python rewrite, 13 modules, safety
  improvements, and removed dangerous operations

- SECURITY.md: Documents privacy guarantees (no telemetry), privilege
  model (unprivileged by default, sudo opt-in), safe defaults (dry-run
  first run, git disabled), audit-only modules, and vulnerability
  reporting via GitHub Security Advisories

- CONTRIBUTING.md: Development guide covering setup, module writing
  pattern (scan/clean/audit), testing requirements, code style (ruff +
  mypy), and PR process

- docs/modules.md: Detailed reference for all 13 modules including
  what each does, system commands used, config options, usage examples,
  and known limitations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Creates GitHub release on tag push. Runs tests before releasing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix line-length violations (E501), unused imports (F401), unused
variables (F841), ambiguous variable name (E741), and mypy type errors.
Add uv.lock to .gitignore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical fixes:
- Fix git branch parsing to skip current branch (was using lstrip incorrectly)
- Fix audit log double-counting bytes_reclaimed per item

Important fixes:
- Fix run_audit() to include sudo modules (audit is read-only)
- Replace rm -rf subprocess with shutil.rmtree in system_cache (+ symlink check)
- Fix format_bytes integer truncation for large values
- Remove unused rich dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add confirmation prompts, subprocess timeouts, sudo validation, symlink checks,
and non-zero exit codes for audit failures to improve safety and reliability.

Changes:
- Add --yes/-y flag to clean and report commands to skip confirmation
- Show preview and require confirmation before destructive operations
- Validate sudo access upfront when --sudo is passed
- Add timeout=120 to homebrew._run_brew() for slow operations
- Add timeout=10 to network DNS flush operations
- Add timeout=15 to network lsof port scanning
- Add timeout=5 to network firewall and VPN checks
- Add timeout=30 to trash du command
- Add timeout=60 to trash osascript empty operation
- Add timeout=10 to all wifi networksetup calls
- Add timeout=10 to privacy osascript call
- Add timeout=30 to dev_caches._dir_size() du command
- Add symlink check in dev_caches before shutil.rmtree()
- Add sys.exit(1) in audit command if any finding has severity "fail"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove no-op scan() and clean() stubs from audit-only modules (system_integrity, app_audit, launch_audit, tools) since base class now provides default implementations
- Replace inline severity ordering logic with shared worst_severity() function from base module across all audit modules (network, privacy, system_integrity, tools, app_audit)
- Create shared dir_size() utility in utils.py and update dev_caches and system_cache to use it
- Fix osquery SQL query in tools.py to use simpler non-system process detection instead of broken signature table subquery
- Improve KnockKnock integration in tools.py with clearer messaging
- Move json import to top of tools.py file
- Update tests to mock new shared utilities instead of removed private methods

All tests pass. Ruff checks pass on modified files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical fix: Module constructors now receive config parameters instead of
hardcoded defaults. This connects the YAML config system to actual module
behavior.

Changes:
- Updated get_all_modules() to accept MaidConfig parameter and pass config
  values to module constructors
- Added constructor parameters to WiFiModule, PrivacyModule, ToolsModule,
  GitModule, HomebrewModule, and DockerModule
- All CLI commands now pass loaded config to get_all_modules()
- Module constructors maintain sensible defaults for backward compatibility
- Updated tests to match new behavior

Safety defaults (install no software without explicit config):
- homebrew.update: False (was True)
- homebrew.upgrade: False (was True)

Modules affected:
- WiFiModule: keep_days, keep_ssids
- PrivacyModule: clear_recent, downloads_move_to_trash, downloads_older_than
- ToolsModule: lynis_enabled, osquery_enabled, knockknock_enabled
- GitModule: enabled, repos_dir, prune_remotes, delete_merged, protected_branches
- HomebrewModule: update, upgrade, cleanup
- DockerModule: remove_dangling_images, remove_unused_volumes, remove_stopped_containers

All tests pass. Ruff checks pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jgamblin and others added 15 commits April 12, 2026 17:28
- Fix CI mypy failure: add types-PyYAML to dev deps
- Change homebrew update/upgrade defaults to False (installs new software)
- Add Severity IntEnum and worst_severity() shared helper to base.py
- Remove @AbstractMethod from Module ABC, provide default empty implementations
- Replace assert with proper validation in MaidConfig
- Fix README: correct class names and example output to match actual CLI
- Fix CHANGELOG date, correct development status to Alpha
- Fix homebrew config fallback defaults in module registry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace macos-13 with macos-15 in CI matrix (macos-13 no longer available)
- Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to silence Node.js 20 deprecation
- Fix test_cli_audit: accept exit code 1 when audit finds real failures
  (CI runners have firewall disabled, triggering severity "fail")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- actions/checkout v4 → v6
- actions/setup-python v5 → v6
- Remove FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 workaround (no longer needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntegration, severity

Added comprehensive tests closing key coverage gaps:

1. Docker _parse_reclaimed_space: GB/MB/KB/B parsing, empty/no-match cases
2. Docker config flags: stopped containers, all flags disabled
3. CLI report command: dry-run with nonexistent config
4. CLI config command: displays 'not found, using defaults'
5. Config integration: verifies config values reach module constructors
6. Severity enum: from_str for all levels, str conversion
7. worst_severity: empty list, single/mixed findings

Coverage increased from 79% to 81% (187 tests passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e enabled, severity validation

Fix #1: system_cache too aggressive
- Changed from wiping entire ~/Library/Caches to explicit allowlist
- Now only cleans: Xcode, Homebrew, pip, yarn, nsurlsessiond caches
- Prevents destruction of app caches that don't regenerate gracefully (Outlook, Teams, browsers)
- Updated scan() to only report allowlisted directories
- Updated tests to reflect new allowlist approach

Fix #2: No per-module enabled config key
- Added "enabled": True to all module sections in DEFAULT_CONFIG
- Git remains "enabled": False by default
- Updated get_all_modules() to check cfg.get("enabled", True) for each module
- Modules with enabled=False are now excluded from the module list
- Applies to all 13 modules including those without explicit config

Fix #3: Finding.severity validation
- Added __post_init__ validation to Finding dataclass
- Raises ValueError if severity not in {"pass", "info", "warn", "fail"}
- Catches typos without requiring full enum migration
- Maintains Finding.severity as str for backward compatibility

All tests pass (186/186 excluding pre-existing wifi test failures).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…le-counting

Homebrew and pip caches were counted by both their dedicated modules
(homebrew, dev_caches) and the system_cache module. Remove them from
SAFE_CACHE_DIRS so each cache is only reported once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oped)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t-time Path.home()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace dict-based get_module_config().get() pattern with typed property
accessors (cfg.homebrew.update, cfg.git.enabled, etc.). Dataclass
defaults now carry fallback values. Eliminates 13 dict lookups.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Apple jumped from macOS 15 (Sequoia) to macOS 26 (Tahoe), skipping versions 16-25.
Smoke tests on macOS 26.4.1 reported 'macOS Unknown 26.4.1'. This update adds
macOS 26 to the name map and improves the test to validate both the new mapping
and ensure truly-unknown versions (99) still return 'Unknown'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jgamblin
jgamblin merged commit 00dfb1c into master Apr 29, 2026
7 checks passed
@jgamblin
jgamblin deleted the modernize-v1 branch April 29, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant