Keep your Mac and PC clean, light, and running like new. Fast open-source CLI, plus native Mac and Windows apps.
# Mole Agent Guide
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 and must stay one, so Claude and Codex receive the same project contract. Put machine-specific or personal overrides in `AGENTS.local.md` / `CLAUDE.local.md`; both are gitignored.
Layout: this file is always loaded and holds the cross-cutting contract. Area rules live in `.claude/rules/<area>.md`; each starts with `paths:` frontmatter, and Claude Code injects it only when a session touches matching files. **Codex and other runtimes that do not auto-load `.claude/rules` must open the matching file from the table below before editing that area.** Every agent reads `.claude/rules/github.md` before triaging or fixing an issue, before reviewing, editing, merging or closing a PR, before any issue, PR or Discussion reply, and before writing an announcement, because no file edit triggers it. A rule keeps its invariant, scope and why, plus the failure mechanism, measured numbers, counterexamples, locked test names and rejected alternatives; incident stories live in `.claude/skills/bugs/references/` and `.claude/skills/release-flow/SKILL.md`. An area rule goes in its rule file; a rule that applies across areas goes here.
| File | Scope |
|---|---|
| `.claude/rules/shell.md` | Shell portability: BSD-only tools never through PATH, duplication audit, dead-symbol checks |
| `.claude/rules/core.md` | `lib/core/file_ops.sh` deletion funnel and sizing, `lib/core/base.sh` section helpers and process guard, `lib/core/ui.sh` frames and input drain, `lib/ui/` menus |
| `.claude/rules/clean.md` | `mo clean` orchestration and sections, user/app/system caches, browsers, firmware, logs, orphan services, project caches, Homebrew, whitelist |
| `.claude/rules/clean-dev.md` | `lib/clean/dev.sh`: developer and toolchain caches, recovery contracts, Nix, simctl, AI tool caches |
| `.claude/rules/purge-installer.md` | `mo purge` discovery and protection, worktrees, `mo installer` |
| `.claude/rules/uninstall.md` | `mo uninstall` inventory, display names, app protection, leftovers, sibling guard, batch execution, Homebrew casks |
| `.claude/rules/optimize.md` | `mo optimize` tasks, preference repair, Spotlight, LaunchServices |
| `.claude/rules/slim.md` | `mo slim`: Intel slice removal scope, seal-aware thinning, verified swap |
| `.claude/rules/go-tuis.md` | `cmd/analyze` and `cmd/status` Go TUIs |
| `.claude/rules/install-update.md` | `mole` router pins, `install.sh`, `mo update`, self-heal, install lock, `mo remove` |
| `.claude/rules/tests.md` | Bats and Go test validity, test lint, CI jobs |
| `.claude/rules/github.md` | Issue, PR and Discussion replies and closeout, Mole Mac invitation, announcements, issue version preflight |
| `.claude/rules/readme.md` | README copies in every language |
The hotspot files named in the rule 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.
## Product synchronization
Use these exact names in synchronization records: **Mole Mac**, **Mole for Windows**, **Mole CLI (macOS)** and **Mole CLI (Windows)**. These identify existing products, not a product rename; never use an unqualified "Mac", "Windows" or "CLI" when the destination is ambiguous.
Every checkout uses root `SYNC.md` as its synchronization entry point. `AGENTS.md` owns the workflow, `SYNC.md` owns cross-product decisions, development records own test/install receipts, and security reviews own residual risks. Reuse an existing entry and ID; link its owner instead of copying the backlog. Preserve dated evidence, but correct an obsolete current-status claim in place.
Before a sync, freeze all available source HEADs, branches, worktree changes and remote heads. Read the owning entries and production consumers, then inspect commits since their last reviewed source. Classify each destination as `Pending`, `Implemented`, `Intentional difference` or `Not applicable`, with a reason and next check for anything unresolved. Record source commit/entry point, destination commit/entry point, verification evidence and delivery separately. Only a verified package/feed proves `Released`; pushed source, CI and a development installation do not. An unavailable checkout or missing evidence stays explicitly unverified.
Cover all four relationships when asked for a complete sync: the two apps, the two CLIs, Mole CLI (macOS) with Mole Mac, and Mole CLI (Windows) with Mole for Windows. Safety and correctness contracts transfer only after checking the destination's real consumer; platform APIs, GUI-only capabilities and private catalogs do not transfer automatically. No version bump, release, private-source disclosure or feature expansion is authorized merely by recording a row. Update the affected record and its source references in the same task, then verify every renamed path/link and report the remaining queue.
## Project
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.
## Product Direction
Mole is a terminal-first macOS maintenance toolkit. Its core job is to help power users inspect reclaimable space, remove known-safe leftovers, uninstall apps safely, run bounded maintenance, and check health from a CLI, script, or compact TUI. It is not a general Mac control center, package manager, background monitor, or GUI feature mirror.
### What Mole Should Do
- Make cleanup and uninstall actions boring, reviewable, logged, protected by path/app rules, and dry-run capable.
- Prefer reversible user-facing removals through Trash where the command surface expects recoverability.
- Keep `clean`, `uninstall`, `purge`, and `installer` focused on reclaimable files, app leftovers, rebuildable caches, installer artifacts, and exact known cleanup targets.
- Keep `analyze` as a disk explorer and ad hoc cleanup surface. Optimize first paint, navigation, sorting, filtering, and safe deletion before adding dashboard-style features.
- Keep `status` as a compact read-only health dashboard plus stable JSON/NDJSON automation output. It may surface actionable signals, but should not become an iStat clone, alerting daemon, or configurable metrics workbench.
- Keep `optimize` focused on explicit, bounded maintenance tasks that can be explained before execution and tested without real authorization prompts.
- Keep command UX dense and terminal-native: short labels, stable alignment, predictable shortcuts, one-screen summaries, then optional drill-down.
- Keep routine per-item cleanup skips and timeouts out of the default summary. Do not add retry reminders or tuning variables there; retain diagnostics in logs and `--debug`, honest partial totals, and visible command-level failures. See `.claude/skills/bugs/references/state-accounting-and-progress.md`.
- Keep Mole Mac references as a cross-link or support path. The CLI and Mac app can share product values without requiring feature parity.
### What Mole Should Not Do
- Do not add broad system modification, privacy reset, package management, app bundle patching, or device-management features just because they are technically possible.
- Do not remove or rewrite third-party app bundle contents, signed resources, user documents, credentials, sessions, active databases, or active developer-tool state. The one named exception is `mo slim` removing Intel slices from apps the user owns (maintainer decision 2026-10-19); its scope and verification contract are in `.claude/rules/slim.md` and do not widen by analogy.
- Do not add background agents, persistent monitoring, notifications, schedulers, menu bar behavior, or GUI-like state unless explicitly requested and justified as CLI scope.
- Do not broaden leftover matching from exact app or bundle evidence into vendor-wide, TeamID-prefix, generic-name, or fallback wildcard deletion.
- Do not turn `status` into a noisy dashboard. Extra rows, live alerts, and tuning controls need a common user action, not just an available metric.
- Do not add prompts, preferences, or output modes to solve every edge case. Prefer quieter defaults, preview/read-only guidance, or declining unsupported operations. A new flag, environment variable, or config key is the same weight as a new setting: it passes only when no single default is right for everyone, and the fix-by-default alternative has to be stated and rejected first. Reaching for a knob to close an issue is the default failure here, not an edge case.
- Do not treat Mole Mac features as required CLI gaps. The CLI should stay narrower, scriptable, and safety-first when parity would add complexity or ambiguity.
### Product Decision Filter
Before accepting a new feature, answer these questions in the PR, issue, or review notes when the fit is not obvious:
1. Does it clearly belong to clean, uninstall, analyze, optimize, status, purge, history, installer, slim, update, completion, touchid, or remove?
2. Is it safe by default, previewable where destructive, testable without real auth, and explainable in one terminal screen?
3. Can the user verify what will change before Mole changes it?
4. Is the target data locally rebuildable, disposable, or backed by exact app/bundle evidence?
5. Would this be better as Mole Mac UI, documentation, a warning, or an explicit "not supported" answer?
If the answer is no or unclear, decline the feature, narrow it, or park it until the product value beats the added surface area.
A magnitude answer needs a sample from real users, not the maintainer's disk. When keep-or-kill turns on how much space a target holds, how many users hit it, or what share of a directory it is, ship a read-only probe first: list, `du`, and `status` only, no writes and no sudo, seconds to run, output delimited so a reporter can paste it whole. A local measurement can be wrong by orders of magnitude in either direction, killing a target that is large on real machines or hiding one the maintainer's disk never had. Have the probe print structure, not only totals, because the design usually turns on the ratio: rebuildable versus authored, active versus stale, already covered versus not. Write the reversal threshold as a number before the sample arrives, then reconcile against it.
## Repository Map
- `.claude/skills/` is the canonical home for project skills, which carry `metadata.internal: true` so `npx skills add tw93/Mole` never installs them. The one published skill, `mole`, lives in `skills/mole/` for both CLIs, and `.claude/skills/mole` links to it. It ships three ways: `npx skills`, and the Claude Code and Codex marketplaces (`.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json`), which both point at `plugins/mole`. That folder carries both manifests, a README and `.claude-plugin/icon.png` (the Mole mark), the shape Anthropic's plugin directory accepts; check it with `claude plugin validate ./plugins/mole`. Codex copies plugin files and drops symbolic links, so `plugins/mole/skills/mole` is a real copy; after editing the skill run `rm -rf plugins/mole/skills/mole && cp -R skills/mole plugins/mole/skills/mole`. `llms.txt` tells an agent how to install the CLI and the skill. mole.fit serves a third copy at `site/skills/mole/` in the private mole-mac checkout, which no gate compares; refresh it in the same task (the command is in that repo's `.claude/rules/site-geo.md`). `.agents/skills/` contains relative symlinks for Codex discovery; do not maintain copied skill bodies. `.claude/rules/` holds the path-scoped area rules listed above.
- `.claude/agents/` contains focused Claude review profiles. They must read the current contract from this file and the rule files instead of copying a frozen version of the safety or portability rules.
- `mole` - the CLI entrypoint. It is a **router only**: it parses args, renders the menu, and dispatches. Business logic does not belong here. Self-update lives in `lib/manage/update.sh` and self-removal in `lib/manage/remove.sh`. Its `VERSION=` line is read by `install.sh` with `sed` and its `SCRIPT_DIR=` line is rewritten by it; read `.claude/rules/install-update.md` before touching either.
- `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 by `app_protection.sh`. Data only, no logic.
- `cmd/analyze/` and `cmd/status/` - Go TUIs (see `.claude/rules/go-tuis.md`).
- `tests/` - Bats suites, PTY drivers and fixtures (see `.claude/rules/tests.md`).
- `scripts/` - check, test, build, and release helpers. `audit_bundle_drift.sh` backs the monthly bundle audit; `audit_function_duplication.py` gates same-body-different-name shell functions and runs inside `check.sh` (`--list` shows every group); `audit_destructive_sinks.py` enforces explicit safety annotations on raw recursive deletion commands; `audit_bats_assertions.py` rejects Bats assertions that cannot fail; `audit_timeout_status.py` keeps every comparison against the timeout status 124 inside `lib/core/timeout.sh`; `audit_skill_commands.py` checks every `mo` command and flag in `skills/mole` against this branch and the `windows` branch; per-PR perf is covered by `tests/core_performance.bats`.
- `docs/SECURITY_DESIGN.md` - design doc for the path validation / app protection / # SAFE annotation contract.
- `SECURITY_AUDIT.md` - security review notes.
## Commands
```bash
./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.
## Critical Safety Rules
- Route deletion through `mole_delete` and the safe helpers in `lib/core/file_ops.sh`, so Trash routing, operation logs, dry-run behavior, and path protection stay consistent. Raw `rm -rf` and `find -delete` are allowed only with a `# SAFE: <one-sentence reason>` annotation on the same line, which is the contract `docs/SECURITY_DESIGN.md` Layer 2 defines and `scripts/audit_destructive_sinks.py` enforces from both `check.sh` and `.github/workflows/test.yml`. Self-created mktemp files use the same annotation for direct `rm -f`; do not route scratch paths through `mole_delete`, which would add Trash routing and an operation-log entry to temporary work.
- Never modify protected paths such as `/System`, `/Library/Apple`, or `com.apple.*`. Check `should_protect_path()` before adding cleanup behavior, and the app protection helpers before adding app cache, uninstall, or leftover cleanup behavior.
- Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.
- Use `MOLE_DRY_RUN=1` (or dry-run / test mode) before destructive cleanup flows. Use `MOLE_TEST_NO_AUTH=1` for tests, manual repro, and verification, and never let verification block on sudo, AppleScript, or macOS authorization prompts, unless the task explicitly targets real auth behavior.
- Any new direct use of `sudo`, `osascript`, or `launchctl` must have a `MOLE_TEST_MODE` / `MOLE_TEST_NO_AUTH` guard or be fully mocked in tests.
- Sudo gates must not treat typed password characters as "skip". Only an explicit skip key should skip privileged cleanup; direct typed input must proceed into the real sudo prompt and have a regression test. The clean command's `SYSTEM_CLEAN=false` applies to every later cleanup section, including Xcode system caches and orphaned services; a cached sudo credential is not renewed consent. Mixed user/system helpers keep their unprivileged path. Do not persist the choice across commands. Locked by the Space-skip cases in `tests/clean_core.bats`.
- CLI entrypoints that source user-scoped state reject an externally root invocation before sourcing anything. Run Mole as the regular user and retain its narrow internal sudo requests; do not restore root execution of user-owned package managers or cache initialization.
- Never run a privileged path-based delete or move through an invoking-user-mutable ancestor. `safe_sudo_remove`, `safe_sudo_find_delete`, and `mole_delete` must downgrade or fail closed there; privileged Trash moves must cross into dedicated immutable root-owned staging under `/Library` before the invoking user moves the item into Trash.
- Never auto-delete Software Update-owned staging trees such as `/Library/Updates` or `/macOS Install Data`. Directory age, process lists, and Software Update plist state cannot prove those trees stay inactive across a scan-to-delete window; keep this surface read-only.
- Automated clean/optimize preserves Prometheus data, Git configuration transaction locks, and Git/shell-history backups: age, an idle owner, or a backup filename does not prove regenerability. Explicit uninstall keeps its separate ownership and confirmation contract. Locked by `tests/clean_developer_state.bats`.
- CoreDuet cleanup lets SQLite recover and checkpoint its own WAL/SHM before pruning old records. A stopped writer may leave committed recent records only in WAL, so no sidecar is unlinked directly, including when SQL fails or SQLite is unavailable. Locked by `tests/optimize_db.bats` and `tests/optimize_removal_outcomes.bats`.
- Never delete, truncate, or vacuum a live PowerLog database: any `Current*` file one level below `/private/var/db/powerlog/Library/PerfPowerTelemetry/<store>/` (`CurrentBackgroundProcessingDB.BGSQL`, `CurrentXcodeOrganizerDB.XCSQL` and the other stores), with its `-wal` / `-shm` companions. Size and mtime cannot prove that Apple has closed every SQLite connection; the 7-day power-log sweep once removed a user's `CurrentXcodeOrganizerDB.XCSQL` when only the BackgroundProcessing path was guarded. Rotated `Archived*` files stay cleanable, and abnormal-size handling stays read-only. Locked by `validate_path_for_deletion rejects the live database of every power telemetry store` and `safe_sudo_find_delete never previews or removes active powerlog database aliases` in `tests/core_safe_functions.bats`.
- Read-only exact-path, SQLite, container-cache, Codex staging, and complete-visibility probe timeouts mean unknown: retain the target or database family without cancelling unrelated cleanup. Signal interruptions still cancel, and so does a timeout at an uninstall removal sink (`mole_delete` and the Trash batches in `remove_file_list`); a timeout in a third-party owner command skips that one command instead (see the owner-command rule in `.claude/rules/clean.md`). Preserve cancellation statuses through uninstall validation and both Trash batch sinks, stop before later candidates, and keep the completed-path ledger intact. The shared handle probes own this distinction for validation, final-sink, cache-directory, and dry-run callers, including a partially consumed shared deadline; locked by the `#1595` and `#1653` cases in `tests/core_safe_functions.bats`.
- Long cleanup scans need both an overall wall-clock budget and inner-loop checkpoints. A timed-out producer must not feed partial output into a deletion loop: materialize only completed scans, discard results on nonzero status, and propagate timeout/failure instead of reporting success. Probe and action must use the same pattern, type, age, and depth. If a project/artifact scan times out, degrade to partial or skipped-slow-scan output instead of appearing hung. Scan pools use `mole_wait_for_any_worker` in the owning shell for Bash 3.2 compatibility, refill whichever slot finishes, and keep each PID, status, and output tied to its original item rather than completion order. Purge activity workers follow the same refill policy. Polling sleeps must handle interruption under `set -e` so cancellation drains bounded workers, removes result files, and restores caller traps before returning; `tests/purge_menu_pty.py` exercises the bare-call path.
- **`_MOLE_COMPLETE_LSOF_MODE` is memoized and has no reset, so never probe it before the sudo session exists.** The first call to `_mole_complete_lsof_mode` caches `direct`, `sudo` or `unknown` for the rest of the process and every later call short-circuits on it. `start_cleanup` in `bin/clean.sh` settles sudo before any cleanup step, and `lib/uninstall/batch.sh` runs `ensure_sudo_session` before `_batch_execute_removals`; that ordering is load-bearing. Do not probe during the uninstall preview, which runs before that gate. Filter preview candidates only with the static uninstall protection policy, and use that same filtered plan for sizing and execution. Record retained-file reasons at the actual guard or sink within each app attempt instead of probing again after removal. If a preview ever needs the answer, clear the memo after `ensure_sudo_session` succeeds, not just read it earlier. Why the preview probe was rejected is in `.claude/skills/bugs/references/deletion-evidence-and-final-sink.md` section 2.
- **`install.sh` stays fail-closed on verification failure.** A checksum or attestation mismatch aborts and says why; it must never downgrade to a source build, which turns "the binary was tampered with" into a quieter path with weaker verification. Resolving no release tag and falling back to `main` must warn that this is a nightly source install. The abort cases in `tests/install_checksum.bats` pin both. Keep the README install URL on unpinned `main`: pinning it there blocks fixes from reaching new installs. The install lock, self-heal and `mo remove` ownership rules are in `.claude/rules/install-update.md`.
- **A gate that refuses must name which cause it hit and what to run next.** Preserve one factual cause line plus one cause-specific next action, keep a new earlier gate at least as actionable as the failure it replaces, and pin reason-code routing rather than catch-all prose. Source-invariant tests skip comments and fail when they match zero intended code sites. The install-lock instance is in `.claude/rules/install-update.md`; incident examples and test traps live in `.claude/skills/bugs/references/test-validity-and-refusal-diagnostics.md`.
- **`mo remove` preserves non-default config roots** and never treats a filename of `mo` or `mole` as launcher ownership; the full contract and its locks are in `.claude/rules/install-update.md`.
- **Never machine-parse `plutil -p`.** Its output is documented as unstable: booleans print differently across macOS releases, and nesting is invisible, so a nested key reads as a top-level one. Read the XML from `plutil -convert xml1` instead and compare tags for equality (plutil spells an empty container `<dict/>`, which a prefix match counts as an opening tag and desyncs the depth). Decode `&` last, since a key holding the literal text `<` arrives escaped twice. Locked by the `#1512` cases in `tests/clean_app_caches.bats`, which must stay on the macos-15/macos-26 compatibility job; the regression story is in `.claude/skills/bugs/references/shell-and-test-pitfalls.md` section 7.
- Process-name probes and kills must treat `CFBundleExecutable` as a literal: escape ERE metacharacters and reject empty or control-byte names before `pgrep`/`pkill`, because the bundle controls that string and an unescaped `.*` matches nearly every process. Locked by `tests/uninstall_process_pattern.bats`.
- Do not change ESC timeout behavior in `lib/core/ui.sh` unless explicitly requested.
- Preserve operation logging to the project log path unless the user explicitly asks to change `MO_NO_OPLOG` behavior. Write control bytes as `\n`, `\r`, `\t` or `\xHH`, never raw, at both single and batch operation-log append boundaries and within deletion-log TSV fields so a path cannot forge a record, and leave backslashes literal so history shows a name exactly as it is on disk; a name holding a backslash followed by `n` therefore logs the same text as one holding a newline, which is accepted because every call still appends one record. `mo history` text mode applies the same escape to rows older logs wrote raw and `--json` keeps the stored value. Never change the actual action path. `_mole_escape_log_value` lives in `lib/core/base.sh` because `mo history` dispatches before `common.sh` loads. Locked by `audit logs keep backslash names verbatim while control bytes stay escaped`, `operation log escaping leaves no control byte for any byte value` and `mo history text prints legacy control bytes escaped and keeps JSON content` in `tests/history.bats`.
- Every action that changes the Mac appends one operations.log row in the shared format Mole Mac and the support diagnostic parse: refusals log `SKIPPED` with a fixed reason, a dry-run session is named `<command>-dry-run`, Homebrew, launchd, login item, Dock, optimize, Touch ID and self-update changes log `REMOVED` or `REBUILT`, `mo analyze` logs each Trash move, and a failed append warns once instead of failing silently. A new sink logs through this format, and `mo remove` keeps `~/Library/Logs/mole`. Mole CLI (Windows) writes nothing in dry runs; that difference is recorded in SYNC.md. Locked by `dry-run sessions are named as previews in markers and rows` in `tests/history.bats`, `validation refusals leave a SKIPPED row with a fixed reason in operations.log` and `an unwritable operations.log warns once instead of failing silently` in `tests/file_ops_mole_delete.bats`, `remove_mole deletes manual binaries and caches but keeps the operation logs` in `tests/uninstall.bats`, and `cmd/analyze/oplog_test.go`.
- **PRs touching destructive sinks need line-by-line review.** For `find_app_files`, `mole_delete`, `remove_file_list`, container traversal, identifier-prefix wildcards, or recursion that ends in deletion, audit every primary and fallback branch for matcher breadth, protected-path coverage, and preserved confirmation. Exact bundle ID or path evidence is required; vendor prefixes and common-name globs are not. Treat specialist or AI review output as a claim to verify, never as approval.
## Working Rules
- When the maintainer reports a defect while testing Mole changes, diagnose it, apply the verified minimal fix, run the required checks, then commit and push the scoped fix as tw93 and report the result without asking again for repair or submission approval. This standing authorization excludes releases and destructive verification.
- Treat `.claude/skills/bugs/SKILL.md` as an on-demand router, not a universal review preflight. Load only the linked reference families signaled by deletion evidence, uncertain probes, bounded Shell/macOS work, persisted state or accounting, progress, test validity, or refusal diagnostics. Unrelated documentation, release copy, and administrative work should not pay for incident history.
- `WORKLOG.md` is an untracked cross-session handoff ledger and is never committed. Durable rules go into this file, a `.claude/rules/` file or a skill; release and queue evidence belongs in the release notes and issue threads. A tracked copy was removed in `0123a42a`, came back with the V1.57.0 triage, and went stale within two days, publishing unsent-email and private-diagnostic bookkeeping.
- 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.
## Verification
- Shell changes: run `./scripts/check.sh --format`, then the relevant Bats test or `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh`. `--format` only formats; CI runs `./scripts/check.sh --no-format`, which is where golangci-lint, ShellCheck and the audit scripts run, so run that before pushing. A green `--format` run let a golangci-lint `modernize` failure reach `main` on 2026-10-10. Test code has its own shellcheck step (`.claude/rules/tests.md`).
- `lib/core/sudo.sh` changes: run `MOLE_TEST_NO_AUTH=1 bats tests/manage_sudo.bats`.
- Go changes: run `go test ./...`.
- Area changes: run the test command listed for that hotspot in its `.claude/rules/` file; do not guess a narrower verifier.
- Documentation-only changes: check links and commands.
- **Read the suite's own summary line, never a count you invent.** `scripts/test.sh` splits timing-sensitive files into sequential Bats runs whose TAP output differs from the main parallel batch. Counting one output prefix therefore undercounts a complete suite. The judges are the test runner's summary and captured exit status; reconcile any derived count against them before reporting it.
- **Run the suite with `$TERM` set.** A backgrounded `./scripts/test.sh` inherits no TTY, `tput` fails, and the bats validator dies on a broken pipe partway through, leaving a truncated log and a nonzero exit that looks like a red suite. `TERM=xterm-256color MOLE_TEST_NO_AUTH=1 ./scripts/test.sh` runs to completion. Classify that one as setup, not as a product failure.
- **A `cancelled` CI run is not a passing one.** Every per-commit workflow sets `cancel-in-progress: true` on `${{ github.workflow }}-${{ github.ref }}` (`release.yml` sets no `concurrency`), so a later push or merge on `main` cancels the checks still running for the previous commit. Land a fix and then merge a queued PR and the fix commit's own `Check` and `Validation` end up `cancelled`, which `gh run list` reports without any red. Verify against the sha that currently contains the change, and read `--json status,conclusion` rather than colors.
- Never pipe a test, check, or CI run into `tail` or `head`. The pipeline reports the pager's exit code, so a red run reads green. Let it print in full, or capture to a file and check the status separately.
`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:
```bash
golangci-lint cache clean
golangci-lint run ./cmd/...
```
## Release
**Never rewrite history that a published `V*` tag can reach, in any editor or agent.** Every descendant commit gets a new SHA, tags follow the rebuilt commits, and GitHub writes the tag's commit SHA into `archive/refs/tags/<TAG>.tar.gz` as a pax global header, so the tarball's checksum changes while every file stays byte for byte identical. Anything that pinned a hash derived from that tarball is then silently wrong, starting with the `homebrew-core` formula. Before any `filter-branch`, `filter-repo` or history-rewriting rebase, list the published tags the rewritten range reaches and treat every downstream checksum derived from them as invalidated; if that list is not empty, do not rewrite. Strip AI co-author trailers when merging a pull request, never retroactively. The V1.54.0 incident (#1591) and live-release recovery are in `.claude/skills/release-flow/SKILL.md` under Release-only pitfalls.
The Windows CLI on the `windows` branch publishes its own `V<x.y.z>-windows` releases in this repository, always with `make_latest: false`, so `releases/latest` and the README version badge stay on macOS. Anything here that resolves a release from tags must accept only plain `V<x.y.z>`: `sort -V` ranks `V1.60.0-windows` above `V1.60.0`. Locked by `get_latest_release_tag_from_git ignores Windows CLI tags` in `tests/install_checksum.bats`.
Tag-driven flow via `release.yml` on capital-`V` tag pushes. The full release runbook (distribution channels, pre-flight checklist, tag/publish commands, curated notes handoff, release-only pitfalls) lives in `.claude/skills/release-flow/SKILL.md`; read it before starting any release-flavored task. Notes formatting stays owned by `.claude/skills/release-notes/SKILL.md`. One rule that always applies: restate which distribution channels a release-flavored run will touch and confirm with the maintainer before acting; channel scope is specified by the maintainer, never inferred.