This file is the shared source of truth for any AI agent working on this repo (Claude Code, Codex, etc.). CLAUDE.md is a symlink to this file. Put machine-specific or personal overrides in AGENTS.local.md / CLAUDE.local.md; both are gitignored.
Mole is a macOS system cleanup and optimization tool with shell and Go components. It performs file cleanup, app protection checks, and maintenance tasks, so safety rules matter more than speed.
mole- main shell entrypoint.bin/- command entry scripts such as clean, analyze, status, uninstall, purge, installer, completion, and touchid.lib/core/- shared shell safety, UI, file operations, operation logs, app protection logic, and centralized timeout constants (timeouts.sh).lib/core/app_protection_data.sh- readonly bundle ID and pattern arrays consumed byapp_protection.sh. Data only, no logic.lib/clean/- cleanup flows.lib/manage/- whitelist, update, autofix, and purge path management.lib/optimize/- optimization tasks.lib/check/- health, diagnostics, and dev environment checks.lib/uninstall/- app uninstall flows and package-manager removal helpers.lib/ui/- reusable menus and app selectors.cmd/analyze/- Go disk-analysis TUI.main.gois bootstrap only;model.goholds types and accessor methods;update.goholds the Bubble Tea Update chain.cmd/status/- Go status dashboard.tests/- Bats and shell test coverage.tests/fuzz_corpus/holds property-test corpora consumed bypath_validation_fuzz.bats.scripts/- check, test, build, and release helpers.audit_bundle_drift.shandperf_baseline.shback the monthly bundle audit and per-PR perf gate.docs/SECURITY_DESIGN.md- design doc for the path validation / app protection / # SAFE annotation contract.SECURITY_AUDIT.md- security review notes.
./scripts/check.sh --format
MOLE_TEST_NO_AUTH=1 ./scripts/test.sh
MOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats
MOLE_DRY_RUN=1 ./mole clean
MOLE_TEST_NO_AUTH=1 ./mole clean --dry-run
MOLE_TEST_NO_AUTH=1 ./mole purge --dry-run
MOLE_TEST_NO_AUTH=1 ./mole installer --dry-run
find bin lib -name '*.sh' -print0 | xargs -0 -n1 bash -n
make build
go test ./...Public docs and examples should prefer the installed mo command. Use ./mole in this repository when verifying source-tree behavior before installation. analyze and analyse are both accepted command spellings.
- Never use raw
rm -rforfind -delete; use safe deletion helpers. - Use
mole_deletefromlib/core/file_ops.shfor removals so Trash routing, operation logs, dry-run behavior, and path protection stay consistent. - Never modify protected paths such as
/System,/Library/Apple, orcom.apple.*. - Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.
- Never let verification block on sudo, AppleScript, or macOS authorization prompts unless the task explicitly targets auth behavior.
- Use
MOLE_DRY_RUN=1before destructive cleanup flows. - Use
MOLE_TEST_NO_AUTH=1for tests, manual repro, and verification unless real auth behavior is being tested. - Any new direct use of
sudo,osascript, orlaunchctlmust have aMOLE_TEST_MODE/MOLE_TEST_NO_AUTHguard or be fully mocked in tests. - Do not change ESC timeout behavior in
lib/core/ui.shunless explicitly requested. - Preserve operation logging to the project log path unless the user explicitly asks to change
MO_NO_OPLOGbehavior.
- Use helpers from
lib/core/file_ops.shfor deletion logic. - Check
should_protect_path()before adding cleanup behavior. - Check app protection helpers before adding app cache, uninstall, or leftover cleanup behavior.
- Keep AI-tool cache cleanup conservative. Claude Code, opencode, Copilot CLI, Zed, Warp, Ghostty, and similar developer tools may have active versions, config, credentials, or session state that must not be removed accidentally.
- Keep shell code formatted with
./scripts/check.sh --format. - Prefer targeted Bats tests during development; run the full suite before committing.
- Do not add AI attribution trailers to commits.
start_section/end_section/note_activityhave three intentionally different implementations inlib/core/base.sh,bin/clean.sh, andbin/purge.sh. Source order decides which one wins, and the wording, color, and dry-run export semantics differ on purpose. Read the cross-reference comment inlib/core/base.shbefore changing any of them.
These files are intentionally large. Do not start by splitting them. Keep edits narrow, preserve local safety boundaries, and run the listed tests when touching each area.
lib/clean/user.showns user-level cleanup flows, browser caches, cloud/app support cleanup, device firmware, and Apple Silicon caches. RunMOLE_TEST_NO_AUTH=1 bats tests/clean_user_core.bats tests/clean_app_caches.bats tests/clean_cached_device_firmware.batswhen touching this area, orMOLE_TEST_NO_AUTH=1 ./scripts/test.shif behavior crosses sections.lib/core/app_protection.showns uninstall/data/path protection policy and bundle matching;lib/core/app_protection_data.showns the protected app category lists. RunMOLE_TEST_NO_AUTH=1 bats tests/uninstall_safety.bats tests/uninstall_naming_variants.bats tests/bundle_resolver.bats.lib/clean/project.showns purge discovery, project artifact filtering, purge menus, and purge config. RunMOLE_TEST_NO_AUTH=1 bats tests/purge.bats tests/purge_config_paths.bats.bin/uninstall.showns uninstall command orchestration, app inventory, metadata refresh, and list/json output. RunMOLE_TEST_NO_AUTH=1 bats tests/uninstall.bats tests/uninstall_scan_bash32.bats.lib/clean/dev.showns developer-tool cleanup, language/toolchain caches, AI agent caches, and Codex runtime handling. RunMOLE_TEST_NO_AUTH=1 bats tests/clean_dev_caches.bats tests/dev_extended.bats.lib/optimize/tasks.showns optimize task registration and system maintenance actions. RunMOLE_TEST_NO_AUTH=1 bats tests/optimize.bats tests/optimize_db.bats.bin/clean.showns clean command orchestration, section output, and safe cleanup execution. RunMOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats tests/clean_apps.bats tests/cli.bats.cmd/analyze/update.goowns the Bubble TeaUpdatechain and message handlers (Init, scanCmd, updateKey, goBack, switchToOverviewMode, enterSelectedDir). This is the largest file incmd/analyze/and the natural landing spot for new key bindings, message types, or navigation behavior. Rungo test ./cmd/analyze.cmd/analyze/main.gois bootstrap only (flag parsing,main(), helpers);cmd/analyze/model.goholds types and the model struct.cmd/analyze/analyze_test.goandcmd/status/view_test.goare test hotspots. Add new cases near related behavior; split later only when touching many adjacent cases. Rungo test ./cmd/....
mo clean- deep cleanup and leftovers for apps that are already gone.mo uninstall- remove installed apps and related leftovers.mo optimize- maintenance and diagnostics, with--whitelistsupport.mo analyze/mo analyse- Go disk explorer; safer for ad hoc cleanup because it uses Trash routing.mo status- live health dashboard and JSON output for automation.mo check/mo doctor- run system diagnostics (updates, health, security, config, dev environment) with optional auto-fix prompts.mo purge- project build artifact cleanup, with configurable scan paths throughmo purge --paths.mo installer- installer-file discovery and cleanup.mo completion,mo touchid,mo update, andmo removemanage shell integration, sudo auth convenience, updates, and uninstalling Mole itself.
- Shell changes: run
./scripts/check.sh --format, then the relevant Bats test orMOLE_TEST_NO_AUTH=1 ./scripts/test.sh. - Go changes: run
go test ./.... - Cleanup behavior: verify with dry-run or test mode first.
- File operation changes: run
MOLE_TEST_NO_AUTH=1 bats tests/file_ops_mole_delete.bats tests/user_file_ops.bats. - Installer changes: run
MOLE_TEST_NO_AUTH=1 bats tests/installer.bats tests/installer_fd.bats tests/installer_zip.bats. - Purge changes: run
MOLE_TEST_NO_AUTH=1 bats tests/purge.bats tests/purge_config_paths.bats. - Whitelist or management changes: run
MOLE_TEST_NO_AUTH=1 bats tests/manage_whitelist.bats tests/manage_sudo.bats. - Uninstall changes: run
MOLE_TEST_NO_AUTH=1 bats tests/uninstall.bats tests/uninstall_remove_file_list.bats. - Documentation-only changes: check links and commands.
make check, make format, make test, make test-go, and make verify are wrappers around the scripts above. make verify intentionally runs check plus Go tests only; use the full Bats suite before risky cleanup, uninstall, or release work.
If golangci-lint reports issues from deleted temporary worktrees or non-existent paths, clear its local cache and rerun the linter:
golangci-lint cache clean
golangci-lint run ./cmd/...- When closing a fixed bug or shipped feature, use project wording from the issue context and include the expected release path only when confirmed.
Tag-driven flow. The release.yml workflow watches 'V*' tag pushes (capital V), builds amd64 and arm64 binaries on macOS, generates SHA256SUMS, attaches build provenance, creates the GitHub Release without notes, then bumps the personal Homebrew tap and opens a Homebrew core PR.
grep '^VERSION=' molematches the new version.SECURITY_AUDIT.mdopening line reflects the new version and date.git status -sis empty or only contains intentionally staged release work.git log origin/main..HEAD --onelineshows only commits you intend to ship../scripts/check.sh --formatandMOLE_TEST_NO_AUTH=1 MOLE_TEST_JOBS=2 BATS_FORMATTER=tap ./scripts/test.shboth exit 0.go test ./cmd/...andmake buildboth pass.
git push origin main
git tag V<version> # capital V; release workflow ignores lowercase v
git push origin V<version>Wait for the workflow to finish (typically 2 minutes for V1.38.0). The workflow creates the release with assets but generate_release_notes: false, so notes must be added in a follow-up step.
gh release edit V<version> --repo tw93/Mole \
--title "V<version> <CodeName> <emoji>" \
--notes-file <path>Format follows V1.37.0 onward: bilingual numbered changelog (English first, 中文 second), then a Thanks 💖 block with sponsors and contributors, ending with the repo blockquote link. Order changelog items by user-perceived impact, not chronological commit order.
Recent sponsors via gh api graphql:
gh api graphql -f query='{user(login:"tw93"){sponsorshipsAsMaintainer(first:30, orderBy:{field:CREATED_AT, direction:DESC}){nodes{sponsorEntity{... on User{login} ... on Organization{login}}}}}}'The minimal query above works on a token without read:user scope. Adding createdAt or privacyLevel requires read:user.
Add the standard reaction set (+1, laugh, hooray, heart, rocket, eyes):
RELEASE_ID=$(gh api repos/tw93/Mole/releases/tags/V<version> --jq '.id')
for r in +1 laugh hooray heart rocket eyes; do
gh api "repos/tw93/Mole/releases/$RELEASE_ID/reactions" -X POST -f content="$r" --silent
doneThese are real bugs hit on this codebase. Each one cost time. Re-read before touching the same area.
- bash 3.2 nounset on empty arrays: macOS default bash raises "unbound variable" when expanding
"${arr[@]}"on an empty array underset -u. Always guard with[[ ${#arr[@]} -gt 0 ]]before expansion. Hit inlib/manage/whitelist.shforDEFAULT_OPTIMIZE_WHITELIST_PATTERNS=(). [[ -n "$var" ]] && cmdreturns 1 when var is empty: underset -e(or any caller that reads the exit code), this short-circuit form propagates exit 1 from the test, even though the intent was "skip silently". If the surrounding compound command relies on exit 0 (for example a{...} > file ||redirect), the optional cmd silently breaks the success path. Use plainif/fiwhenever the conditional sits inside an exit-code-sensitive block. Hit ininstall.shwrite_install_channel_metadata(stable channel always tripped the warning).- bats heredoc steals bytes from
read -n1: when the inner script runs viabash <<'EOF' ... EOF, aread -r -s -n1in the function under test consumes the next byte from the heredoc source itself, corrupting the next command (e.g.echobecomescho, exit 127). Fix is to redirect the function's stdin from/dev/nullinside the test. run_with_timeoutexecs the binary, bypassing bash function mocks: gtimeout/timeout exec the real PATH binary, so a shell-function override of (e.g.)osascriptis invisible. Tests must use a PATH stub directory and prepend it toPATH, not function shadowing.gh release createconflicts with the workflow-created release: the workflow already creates the release on tag push, so post-tag note publishing must usegh release edit, nevercreate.- Tag prefix is case-sensitive:
release.ymlfilters on'V*'. A lowercasev1.38.0tag will not trigger the workflow.
- Order items by user-perceived impact, not commit chronology. The headline change goes first; internal safety hardening, performance, and bug fixes follow.
- Verify every mentioned command still exists in HEAD before listing it.
mo check / mo doctorwas removed in the same release cycle that I almost shipped notes claiming it as a feature. - Pick icons that match the action, not the category. A broom (🧹) on insight rows mis-signalled "all of these are safe to delete", which is wrong for iOS Backups, Xcode Archives, and Old Downloads. Eyes (👀) match "look here" without that false promise.
- No em dash anywhere in user-facing text. Use commas, periods, colons, or semicolons. (Global rule, but worth re-stating because it has been violated repeatedly in release drafts.)
- No parenthesised PR refs or thanks inline. Move PR numbers and contributor handles to a single closing thanks block to keep the changelog scannable.