This document describes the security-relevant behavior of the current main branch, updated for V1.40.0 on 2026-05-30. It is intended as a public description of Mole's safety boundaries, destructive-operation controls, release integrity signals, and known limitations.
Mole is a local system maintenance tool. Its main risk surface is not remote code execution; it is unintended local damage caused by cleanup, uninstall, optimize, purge, installer cleanup, or other destructive operations.
The project is designed around safety-first defaults:
- destructive paths are validated before deletion
- critical system roots and sensitive user-data categories are protected
- sudo use is bounded and additional restrictions apply when elevated deletion is required
- symlink handling is conservative
- preview, confirmation, timeout, and operation logging are used to make destructive behavior more visible and auditable
Mole prioritizes bounded cleanup over aggressive cleanup. When uncertainty exists, the tool should refuse, skip, or require stronger confirmation instead of widening deletion scope.
The project continues to strengthen:
- release integrity and public security signals
- targeted regression coverage for high-risk paths
- clearer documentation for privilege boundaries and known limitations
The highest-risk areas in Mole are:
- direct file and directory deletion
- recursive cleanup across common user and system cache locations
- uninstall flows that combine app removal with remnant cleanup
- project artifact purge for large dependency/build directories
- elevated cleanup paths that require sudo
- release, install, and update trust signals for distributed artifacts
mo analyze is intentionally lower-risk than cleanup flows:
- it does not require sudo
- it respects normal user permissions and SIP
- delete actions require explicit confirmation
- deletion routes through Finder Trash behavior rather than direct permanent removal
All destructive shell file operations are routed through guarded helpers in lib/core/file_ops.sh.
Core controls include:
validate_path_for_deletion()rejects empty paths- relative paths are rejected
- path traversal segments such as
..as a path component are rejected - paths containing control characters are rejected
- raw
find ... -deleteis avoided for security-sensitive cleanup logic - removal flows use guarded helpers such as
safe_remove(),safe_sudo_remove(),safe_find_delete(), andsafe_sudo_find_delete() - uninstall removal flows that move items to Trash use
mole_delete, which validates the path again and records the operation result.mole_deletenow also validates symlinks instead of skipping them, and normalizes the target by collapsing repeated slashes and stripping a trailing slash before the protected-path check, so equivalent path spellings cannot slip past protection. - incomplete download cleanup skips files currently open (lsof check) and uses quoted glob patterns to prevent word-splitting on filenames that contain spaces
Blocked paths remain protected even with sudo. Examples include:
/
/System
/bin
/sbin
/usr
/etc
/var
/private
/Library/Extensions
Some subpaths under otherwise protected roots are explicitly allowlisted for bounded cleanup where the project intentionally supports cache/log maintenance. Examples include:
/private/tmp/private/var/tmp/private/var/log/private/var/folders/private/var/db/diagnostics/private/var/db/DiagnosticPipeline/private/var/db/powerlog/private/var/db/reportmemoryexception
This design keeps cleanup scoped to known-safe maintenance targets instead of broad root-level deletion patterns.
/
/System
/bin
/sbin
/usr
/etc
/var
/private
/Library/Extensions
Some subpaths under protected roots are explicitly allowlisted:
/private/tmp/private/var/tmp/private/var/log/private/var/folders/private/var/db/diagnostics/private/var/db/DiagnosticPipeline/private/var/db/powerlog/private/var/db/reportmemoryexception
In addition to path blocking, these categories are protected:
- Keychains, password managers, credentials
- VPN/proxy tools (Shadowsocks, V2Ray, Clash, Tailscale, AmneziaWG, WireGuard, NetworkExtension preferences)
- AI tools (Cursor, Claude, ChatGPT, Ollama)
- Codex Desktop runtime state and active VM/runtime caches
- OrbStack and similar local container/VM runtimes: live container and machine images under
~/Library/Group Containers/*dev.orbstackand~/.orbstack, plus protected bundlesdev.orbstack.*anddev.kdrag0n.MacVirt. Rebuildable caches such as~/Library/Caches/dev.orbstack.OrbStackremain cleanable. - Browser history and cookies
- Apple-owned app group containers, including
group.com.apple.notes - Time Machine data (during active backup)
com.apple.*LaunchAgents/LaunchDaemons- user-owned
~/Library/LaunchAgents/*.plistautomation/configuration - iCloud-synced
Mobile Documents
All deletion routes pass through lib/core/file_ops.sh:
validate_path_for_deletion()- Empty, relative, traversal checksshould_protect_path()- Prefix and pattern matchingsafe_remove(),safe_find_delete(),safe_sudo_remove()- Guarded operations
The current design rationale is kept in this audit document so the safety model stays next to the implementation notes.
Mole has explicit protected-path and protected-category logic in addition to root-path blocking.
Protected or conservatively handled categories include:
- system components such as Control Center, System Settings, TCC, Spotlight, Finder, and Dock-related state
- keychains, password-manager data, tokens, credentials, and similar sensitive material
- VPN and proxy tools such as Shadowsocks, V2Ray, Clash, Tailscale, AmneziaWG, WireGuard, and NetworkExtension preferences
- AI tools in generic protected-data logic, including Cursor, Claude, ChatGPT, and Ollama
- Codex Desktop runtime state and active VM/runtime caches
- OrbStack and similar local container/VM runtimes, including live data under
~/Library/Group Containers/*dev.orbstackand~/.orbstack, while rebuildable runtime caches stay eligible for cleanup ~/Library/Messages/Attachments- Apple Notes and other Apple-owned app group containers, including
~/Library/Group Containers/group.com.apple.notes - browser history and cookies
- Time Machine data while backup state is active or ambiguous
com.apple.*LaunchAgents and LaunchDaemons- user-owned
~/Library/LaunchAgents/*.plistautomation/configuration - iCloud-synced
Mobile Documentsdata
Project purge also uses conservative heuristics:
- purge targets must be inside configured project boundaries
- direct-child artifact cleanup is only allowed in single-project mode
- recently modified artifacts are treated as recent for 7 days
- nested artifacts are filtered to avoid parent-child over-deletion
- protected vendor/build-output heuristics block ambiguous directories
Developer cleanup also preserves high-value state. Examples intentionally left alone include:
~/.cargo/bin~/.rustup~/.mix/archives~/.stack/programs
Symlink behavior is intentionally conservative.
- path validation checks symlink targets before deletion
- symlinks pointing at protected system targets are rejected
mole_deletevalidates symlinks rather than skipping them, so a symlink whose target resolves into a protected root is refused instead of silently movedsafe_sudo_remove()refuses to sudo-delete symlinkssafe_find_delete()andsafe_sudo_find_delete()refuse to scan symlinked base directories- installer discovery avoids treating symlinked installer files as deletion candidates
- analyzer scanning skips following symlinks to unexpected targets
Path traversal handling is also explicit:
- non-absolute paths are rejected for destructive helpers
..is rejected when it appears as a path component- legitimate names containing
..inside a single path element remain allowed to avoid false positives for real application data mo analyzedelete validates the raw user-supplied path beforefilepath.Absresolves it, then validates the resolved absolute path a second time, closing a window where traversal segments could surviveAbsnormalization
Mole uses sudo for a subset of system-maintenance paths, but elevated behavior is still bounded by validation and protected-path rules.
Key properties:
- sudo access is explicitly requested instead of assumed
- non-interactive preview remains conservative when sudo is unavailable
- protected roots remain blocked even when sudo is available
- sudo deletion uses the same path validation gate as non-sudo deletion
- sudo cleanup skips or reports denied operations instead of widening scope
- sudo-required uninstall paths are routed to the invoking user's Trash where possible instead of root-owned Trash or direct deletion
- sudo Trash routing refuses unsafe Trash locations, including symlinked Trash directories
- authentication, SIP/MDM, and read-only filesystem failures are classified separately in file-operation results
- sudo credential prompting passes through the system's native PAM prompt rather than a hardcoded string, ensuring correct behavior across locales and PAM configurations
- Touch ID PAM configuration (
mo touchid) usessudo install -m 444 -o root -g wheelfor atomic file writes, preventing temporary permission windows where PAM files could be user-writable (fixed in V1.39.0; prior versions usedsudo mvwhich preserved temp-file ownership) - the perl-based command timeout fallback creates a new process group with
setpgid(0, 0)rather than callingsetsid(), so the timed child keeps the controlling terminal. This lets nested sudo inside a Homebrew cask uninstall script reuse the already-cached credential instead of failing on a detached tty, while the group-kill cleanup semantics (kill TERM -pid) are unchanged.
When sudo is denied or unavailable, Mole prefers skipping privileged cleanup to forcing execution through unsafe fallback behavior.
Mole is not intended to aggressively delete high-value user data.
Examples of conservative handling include:
- sensitive app families are excluded from generic orphan cleanup
- orphaned app data waits for inactivity windows before cleanup
- Claude VM orphan cleanup uses a separate stricter rule
- uninstall file lists are decoded and revalidated before removal
- reverse-DNS bundle ID validation is required before LaunchAgent and LaunchDaemon pattern matching; bundle ID matching uses boundary-aware comparisons (
mole_name_starts_with_bundle_id_boundary,mole_name_has_bundle_id_boundary) to prevent cross-app false matches (e.g.com.examplenot matchingcom.example123), anddefaults deleteis guarded bymole_is_reverse_dns_bundle_id()to reject malformed or adversarial domain strings - LaunchAgents that only declare
MachServicesare unload-only and are not treated as safe deletion targets without a backing executable or bundle match force_kill_app()refuses to terminate a process whose resolved name matches a known system process, and this guard runs before the entire pgrep/AppleScript/pkill escalation ladder, so a third-party app cannot weaponize it by setting a system-likeCFBundleExecutable- receipt payload removal is gated by
receipt_payload_path_is_allowlisted(), which requires a well-formed reverse-DNS bundle ID and only allows files whose basename is anchored to that bundle ID under/Library/LaunchAgents,/Library/LaunchDaemons,/Library/PrivilegedHelperTools, or/private/var/db/receipts - apps managed by an official vendor uninstaller are excluded from Mole's own removal list, so the vendor's uninstall flow remains authoritative
- XDG-style dotdirs belonging to a standalone CLI tool that shares a name with a GUI app are preserved during uninstall, preventing collateral removal of unrelated CLI state (issue #993, for example a CLI sharing a name with
Claude.apporOpenCode.app) - batch uninstall now displays system-level remnants for review instead of deleting them; the confirmation prompt is retained and any
launchctl unload/bootoutruns under dry-run andMOLE_TEST_MODEguards
Installed-app detection is broader than a single /Applications scan and includes:
/Applications/System/Applications~/Applications- Homebrew Caskroom locations
- Setapp application paths
This reduces the risk of incorrectly classifying active software as orphaned data.
Mole exposes multiple safety controls before and during destructive actions:
--dry-runpreviews are available for major destructive commands- dry-run output deduplicates targets by filesystem identity (device+inode), so aliased paths and symlinks do not appear as separate items
- interactive high-risk flows require explicit confirmation before deletion
- purge marks recent projects conservatively and leaves them unselected by default
- purge configuration is written atomically (mktemp then rename) to prevent partial writes if the process is interrupted
- analyzer delete uses Finder Trash rather than direct permanent removal
- operation logs are written to
~/Library/Logs/mole/operations.logunless disabled withMO_NO_OPLOG=1 mole_deleteTrash and permanent deletion attempts are also recorded by the file-operation layer with result status, target path, and error context where availablemo history(lib/core/history.sh) is read-only: it readsoperations.loganddeletions.logto surface recent cleanup activity and performs no deletion or out-of-bounds writes- timeouts bound external commands so stalled discovery or uninstall operations do not silently hang the entire flow
Relevant timeout behavior includes:
- orphan and Spotlight checks: 2s
- LaunchServices rebuild during uninstall: bounded 10s and 15s steps
- Homebrew uninstall cask flow: 300s by default, extended for large apps when needed
- project scans and sizing operations: bounded to avoid whole-home stalls
Optimize tasks are maintenance actions rather than bulk deletion, but they still touch user-visible state, so they are bounded conservatively:
- Dock Refresh no longer deletes any
*.dbunder~/Library/Application Support/Dock. The previous implementation wipeddesktoppicture.dband reset the user's wallpaper (#995); refreshing the Dock now relies onkillallplus touching the plist instead. - Spotlight orphan rule cleanup operates only in the user domain through
defaults, runs under a dry-run guard, removes only entries whose app is confirmed no longer installed (bundle_has_installed_app), requires a well-formed reverse-DNS bundle ID, and never touchesSystem.*orcom.apple.*rules. - Font Cache Rebuild (
atsutil databases -remove) was removed because clearing the font cache could corrupt font rendering with no reliable benefit.
Mole treats release trust as part of its security posture, not just a packaging detail.
Repository-level signals include:
- weekly Dependabot updates for Go modules and GitHub Actions
- pre-commit hook that mirrors GitHub CI checks locally (shell syntax, shfmt, shellcheck, Go vet)
- CI checks for unsafe
rm -rfusage patterns and core protection behavior - targeted tests for path validation, purge boundaries, symlink behavior, dry-run flows, and destructive helpers
- macOS 14 and macOS 15 compatibility coverage for core Bats suites
- CodeQL scanning for Go and GitHub Actions workflows, with workflow permission hardening
- curated changelog-driven release notes for user-visible changes
- published SHA-256 checksums for release assets
- GitHub artifact attestations for release assets
- install-time verification of the GitHub Actions build-provenance attestation:
install.shrunsgh attestation verify(with--deny-self-hosted-runners) on the downloaded asset when the GitHub CLI is available, and a mismatch is treated as fatal before checksums are read. This moves attestation from a release-side artifact to an install-side check.
These controls do not eliminate all supply-chain risk, but they make release changes easier to review and verify.
There is no single tests/security.bats file. Instead, security-relevant behavior is covered by focused suites, including:
tests/core_safe_functions.batstests/clean_core.batstests/clean_user_core.batstests/clean_dev_caches.batstests/clean_system_maintenance.batstests/clean_apps.batstests/file_ops_mole_delete.batstests/purge.batstests/installer.batstests/optimize.batstests/uninstall_safety.batstests/uninstall_naming_variants.batstests/path_validation_fuzz.batstests/history.batstests/core_timeout.batscmd/analyze/*_test.go
Key coverage areas include:
- path validation rejects empty, relative, traversal, and system paths
- symlinked directories are rejected for destructive scans
- purge protects shallow or ambiguous paths and filters nested artifacts
- dry-run flows preview actions without applying them and do not emit duplicate targets
- confirmation flows exist for high-risk interactive operations
- LaunchAgent unload-only handling, Homebrew Cask paths, and sudo-required Trash routing
- Apple Notes group containers and other Apple-owned group containers remain protected
- sudo credential prompting and session management (
tests/manage_sudo.bats) - purge config path discovery and write behavior (
tests/purge_config_paths.bats) - hint and cleanup-hint flows (
tests/clean_hints.bats) - Touch ID PAM file permission enforcement (
tests/cli.bats) - bundle ID boundary matching and malformed-ID rejection (
tests/uninstall_safety.bats) - official-uninstaller exclusion and receipt payload allowlisting (
tests/uninstall_safety.bats) - uninstall behavior across localized and naming-variant app names (
tests/uninstall_naming_variants.bats) - property-style path validation fuzzing over the corpus in
tests/fuzz_corpus/(tests/path_validation_fuzz.bats) - read-only history rendering from operation logs (
tests/history.bats) - command timeout behavior including process-group cleanup (
tests/core_timeout.bats) - bash 3.2 empty-array nounset compatibility (
tests/uninstall_scan_bash32.bats)
- Cleanup is destructive. Most cleanup flows do not provide undo.
mo analyzedelete is safer because it uses Trash, but other cleanup flows are permanent once confirmed.mo uninstallnow routes more removals through Trash, but Trash availability, permissions, and volume behavior still depend on the local macOS environment.- Generic orphan data waits 30 days before cleanup; this is conservative but heuristic.
- Claude VM orphan cleanup waits 7 days before cleanup; this is also heuristic.
- Time Machine safety windows are hour-based and intentionally conservative.
- Localized app names may still be missed in some heuristic paths, though bundle IDs are preferred where available.
- Users who want immediate removal of app data should use explicit uninstall flows rather than waiting for orphan cleanup.
- Release artifacts include checksums and attestations, but downstream package-manager trust also depends on external distribution infrastructure.
mo history --jsonescapes strings byte by byte underLC_ALL=C(history_json_escape) for portable behavior on bash 3.2. Printable multibyte bytes are emitted verbatim, so the emitted JSON stays valid UTF-8, but the escaper does not perform Unicode-aware codepoint iteration. This is a known display-layer detail, not a correctness issue.- Planned follow-up work includes stronger destructive-command threat modeling, more regression coverage for high-risk paths, and continued hardening of release integrity and disclosure workflow.
For reporting procedures and supported versions, see SECURITY.md.