{"owner":"tw93","repo":"Mole","hasSkills":true,"totalSkillsCount":10,"totalTokensCount":19735,"categories":["anthropic-skill","subagent-persona","cursor-rule","root-instruction","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/release-flow/SKILL.md",".claude/agents/bash32-portability-reviewer.md",".cursor/rules/mole-test-safety.mdc",".claude/skills/release-notes/SKILL.md",".claude/skills/bugs/SKILL.md","AGENTS.md",".claude/agents/safety-reviewer.md",".claude/skills/release-notes/scripts/post-reactions.sh",".claude/skills/release-notes/agents/openai.yaml",".claude/skills/bugs/references/shell-and-test-pitfalls.md"],"skills":{".claude/skills/release-flow/SKILL.md":"---\nname: release-flow\ndescription: \"Mole CLI release runbook for distribution channels, pre-flight checks, capital-V tags, artifacts, and curated-note handoff. Use when assessing or executing a Mole release. Not for release-note copy alone or ordinary code review.\"\nmetadata:\n  internal: true\n---\n\n# Mole CLI Release Flow\n\nTag-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 opens a Homebrew core PR.\n\n## Distribution channels\n\n| Channel | What ships | Trigger | Automation |\n|---|---|---|---|\n| Nightly (`mo update --nightly`) | `main` HEAD via `install.sh` | Any commit pushed to `main` | Automatic; no tag or release involved |\n| GitHub stable release | amd64/arm64 binaries + `SHA256SUMS` | Push a capital-`V` tag | `release.yml` builds and creates the release; curated notes are a manual follow-up |\n| Homebrew core | Version-bump PR to `Homebrew/homebrew-core` | Same `V*` tag workflow | Automatic PR; merge timing is upstream's |\n| Nix flake | Source-built macOS package from the selected Git ref | `main` follows development; a release tag pins that release | Nix manages install, upgrade and removal; no separate registry publish |\n\nAt the start of any release-flavored task, restate which channels this run will touch and which it will not, and confirm with the maintainer before acting. Channel scope is specified by the maintainer, never inferred.\n\n## Pre-flight checklist\n\nResolve the latest published stable tag from GitHub before choosing the version or review range. Reconcile handoff claims against the current branch, worktree, and remote SHA; an earlier report of uncommitted work may describe commits that have already landed. Review all changes since that stable tag, not just the final fix batch.\n\n1. `grep '^VERSION=' mole` matches the new version.\n2. `SECURITY_AUDIT.md` opening line reflects the new version and date, and its CI coverage list matches the matrix in `.github/workflows/test.yml`.\n3. `git status -s` is empty or only contains intentionally staged release work.\n4. `git log origin/main..HEAD --oneline` shows only commits you intend to ship.\n5. `./scripts/check.sh --format` and `TERM=xterm-256color MOLE_TEST_NO_AUTH=1 MOLE_TEST_JOBS=2 BATS_FORMATTER=tap ./scripts/test.sh` both exit 0.\n6. `go test ./...` and `make build` both pass.\n7. `scripts/e2e_macos.py` exits 0 on a real Mac at the candidate commit. It installs IINA from its verified GitHub DMG, launches it, uninstalls it with every personal-data row selected through a real terminal, lists the DMG in `mo installer --dry-run`, deletes it from `mo analyze ~/Downloads`, and runs every other command read-only, so it changes only what it installed and never asks for a password. It refuses to start when the Mac already has IINA or any of its data. A failed run lists what it left behind; move those to the Trash before rerunning. Transcripts and `report.json` land in `~/Library/Logs/mole-e2e/<run>/`.\n\nUse the Go version declared in `go.mod` for local release builds, matching `actions/setup-go` in CI, then run `scripts/check_release_minos.sh` on both architectures. A newer local Go can raise the minimum macOS version even with `CGO_ENABLED=0`; during V1.54.0 verification, Go 1.27 produced macOS 13 binaries while the declared Go 1.25 toolchain preserved macOS 12. Rebuild with the declared toolchain instead of relaxing the minimum-OS gate. Check the `make release-amd64 release-arm64` outputs, not `make build`: the local build links `status` with cgo against the host SDK, so its `status-go` reports the host macOS as its minimum and says nothing about the release binaries.\n\nCapture the test runner's exit status and structured summary, with skipped tests reported separately. After pushing the candidate commit, wait for its required Check, Validation, CodeQL, and Nix workflows to finish successfully before tagging that exact SHA. Nix must verify both Darwin architectures and the installed/profile lifecycle. A cancelled run or a green check on another commit is not release evidence.\n\n## Tag and publish\n\n```bash\ngit push origin main\ngit tag V<version>          # capital V; release workflow ignores lowercase v\ngit push origin V<version>\n```\n\nWait for the workflow to finish. The workflow creates the release with assets but `generate_release_notes: false`, so notes must be added in a follow-up step.\n\nAfter the workflow finishes, verify the release assets before announcing anything: `gh release view V<version> --json assets --jq '.assets[].name'` must list all four `analyze-`/`status-darwin-{amd64,arm64}` binaries, both `binaries-darwin-*.tar.gz` Homebrew tarballs, AND `SHA256SUMS`. Install verification is fail-closed, so a release without a readable `SHA256SUMS` asset makes every install and `mo update` abort by design; a missing checksums file is a release blocker, not a cosmetic gap.\n\nDownload all seven assets. Verify the six payload checksums, attestations for all seven tied to the release tag, exact source commit, and `.github/workflows/release.yml`, and each archive's two expected binary members against the raw binary assets. Check Mach-O architecture and minimum macOS version on the downloaded binaries, then run the native architecture's Analyze and Status smoke probes. Successful local builds do not prove the public package contains those bytes.\n\nThen run a **script self-update smoke** before publishing notes or announcing: install the previous stable release through the script channel, run `mo update`, and confirm `mo --version` prints the candidate version. Script-installed clients execute the new tag's `install.sh`, so this is the only gate that exercises their real upgrade path; the pre-flight suite cannot cover it before the release exists. Homebrew is a separate downstream gate: verify it only after the core formula has updated, and never treat a script-channel smoke as proof that Homebrew is ready. If the script smoke fails, pull the release (see the pulling-and-re-releasing pitfall) before anyone is told to update.\n\nUse a fresh archive of the previous tag so ignored local `bin/*` builds cannot contaminate the old installation. Place the isolated prefix and config under a physical user-owned directory; `/tmp` or `/var` aliases and writable ancestors can correctly fail installer path checks. Isolate HOME, config/cache paths, and PATH, block host `brew` and `sudo`, and set `MOLE_TEST_NO_AUTH=1`. Verify `config/install_channel` retains `CHANNEL=stable`, `config/bin/{analyze,status}-go` match the verified release assets, changed installed shell sources match the tag, and a second update reports the current version. Keep the user's existing installation unchanged.\n\n## Apply curated release notes\n\nThe curated-notes flow (bilingual format, `gh release edit` instead of `create`, thanks block, and the six-reaction set) is owned by `.claude/skills/release-notes/SKILL.md`. `.agents/skills/release-notes` is a symlink to that canonical directory for Codex discovery, and its Codex-only invocation policy lives in `agents/openai.yaml`; do not replace the symlink with a copied mirror. Follow that skill; do not duplicate its format details here. Version, codename, and emoji go only in the release title; the body h1 is just `Mole`.\n\nAfter applying notes and the standard reactions through that skill, read back the published title, full body, and all six reactions. Report GitHub Stable and Homebrew availability separately; a successfully opened core PR still needs upstream tests and merge.\n\n- Issues whose reporter already verified the fix on nightly get no \"now in stable\" follow-up after the release, and none is proposed.\n\n## Release-notes craft\n\nFormat rules (impact ordering, command existence checks, icon semantics, no em dash, no inline PR refs) live in `.claude/skills/release-notes/SKILL.md` under \"Format rules\". Keep that skill as the single source of truth for notes formatting.\n\n## Release-only pitfalls\n\n- **Tag prefix is case-sensitive**: `release.yml` filters on `'V*'`. A lowercase `v1.38.0` tag will not trigger the workflow.\n- **Old clients fetch `install.sh` from the release tag, not from main**: a self-updating Mole downloads `raw.githubusercontent.com/tw93/mole/V<tag>/install.sh`, and tag content is immutable. An installer/updater bug therefore reaches existing stable users only through a new tag; fixing main changes Nightly but does not repair an already published stable updater.\n- **Never rewrite history an already published tag can reach**: the rule and the pre-rewrite tag audit are in `AGENTS.md` (Release). The incident behind it: on 2026-09-17, four days after V1.54.0 shipped, a `git filter-branch --msg-filter` run stripped a `Co-authored-by: Cursor` trailer from a commit dated 2026-05-06 that had 956 descendants. `--tag-name-filter cat` carried the tags onto the rebuilt commits, the release commit was rebuilt, and `brew upgrade mole` has failed the source checksum for every Intel user since, because Homebrew dropped Intel bottles so they all build from source (#1591). The enumeration was run afterwards: 25 published tags were reachable from that rewrite, so 25 source-tarball checksums moved, not one. Only V1.54.0 broke anything, because homebrew-core pins the checksum of the formula's current version alone, and `install.sh` anchors on the release assets' `SHA256SUMS` rather than on the tag archive it downloads. Any external consumer pinning an older tag's tarball is outside what can be checked from here. Recovery on a live release is a one-line `sha256` PR to `Homebrew/homebrew-core` with the current tarball's hash, and it takes two things that are easy to miss. The PR body must carry Homebrew's own template or a bot closes it within seconds as AI-written, editing it in reopens the same PR and opening a second one is explicitly refused. And a moved checksum is read as a possible supply-chain compromise, so a maintainer will hold the merge until the upstream author confirms in that thread that the change was benign; answer with the two commits' identical tree and the archive's pax header, which they can verify without trusting you, not with a promise. Check for an existing PR before opening one: the community usually files it first. Bottles are unaffected because BrewTestBot built them from the original download.\n- **Pulling and re-releasing a version**: `gh release delete V<old> --cleanup-tag` removes the release and remote tag. Delete the local tag, close the superseded Homebrew core PR with a one-line supersede comment before pushing the replacement tag (`release.yml` refuses to overwrite an existing `mole-<version>` fork branch and reuses, rather than recreates, an open PR for the same head), then bump `VERSION` and `SECURITY_AUDIT.md`, commit `release: V<new>`, tag, and run the normal publish flow. The Homebrew core PR regenerates on the new tag.\n\nWhen release work touches Shell code or tests, read `.claude/skills/bugs/references/shell-and-test-pitfalls.md` for Bash 3.2 arrays, heredoc input, mock bypasses, and CI-runner quirks.\n",".claude/agents/bash32-portability-reviewer.md":"---\nname: bash32-portability-reviewer\ndescription: Reviews Mole shell and Bats diffs against the current macOS Bash 3.2, errexit, timeout, TTY, BSD-tool, and CI-runner pitfalls recorded in .claude/skills/bugs/references/shell-and-test-pitfalls.md. Use after changes under mole, install.sh, bin/**, lib/**, scripts/**, or tests/*.bats.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole shell portability reviewer\n\nRead the current `.claude/skills/bugs/references/shell-and-test-pitfalls.md`\nbefore every review. It is the source of truth and grows when a new incident\nbecomes a stable invariant. Do not rely on a fixed count or a copied historical\nlist in this profile.\n\nYou read diffs, production context, and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base. Restrict findings to `mole`,\n   `install.sh`, `bin/**`, `lib/**`, `scripts/**`, and `tests/*.bats`.\n2. Turn every current pitfall bullet in that reference into a check against the\n   touched code. The list below is a search aid, not a replacement for it:\n   - moved functions using `BASH_SOURCE`, `$0`, or `FUNCNAME`;\n   - `du -s` calls outside `run_with_timeout`;\n   - possibly empty array expansion under `set -u`;\n   - functions called through `if` or `||` that rely on errexit internally;\n   - `[[ ... ]] && cmd` in exit-code-sensitive blocks;\n   - heredoc-driven tests of `read -n1` without redirected stdin;\n   - shell-function mocks hidden by timeout wrappers that exec a real binary;\n   - GNU-only command flags or CI fixtures that assume local macOS directories;\n   - PlistBuddy stdout leaking into assertions;\n   - tests that can pass on empty output or an early return;\n   - macOS-runner-specific errexit behavior around failing command mocks.\n3. Read enough surrounding code to prove the pattern actually fires. A grep hit\n   alone is not a finding.\n4. Check that the regression test reaches the intended branch and that every\n   assertion failure propagates. If local and CI behavior differ, require a\n   failure trace that exposes status, output, and mock calls.\n\n## Output\n\nFor every confirmed problem:\n\n```\nLANDMINE: <file>:<line> - <problem>\n  Pattern: <matched code>\n  Why it fires here: <context>\n  Fix: <one concrete change>\n```\n\nUse `UNVERIFIED: <file>:<line> - <missing evidence>` when context cannot resolve\na real risk. End with `VERDICT: <N> landmines, fix before merge` when findings\nexist, otherwise `VERDICT: no landmines found`. With no findings or unverified\nitems, output only `VERDICT: no landmines found`.\n",".cursor/rules/mole-test-safety.mdc":"---\ndescription: Mole test safety and verification defaults\nalwaysApply: true\n---\n\n# Mole Test Safety\n\n- Default test and verification commands must run with `MOLE_TEST_NO_AUTH=1` or through `scripts/test.sh`, which sets it automatically.\n- Tests must never trigger real `sudo`, Touch ID, password prompts, AppleScript permission dialogs, or system service changes.\n- Any new direct use of `sudo`, `osascript`, or `launchctl` must either be guarded by `MOLE_TEST_MODE` / `MOLE_TEST_NO_AUTH` or be fully mocked in the test path.\n- Prefer focused regression tests first, then run full no-auth verification before release-sensitive changes.\n",".claude/skills/release-notes/SKILL.md":"---\nname: release-notes\ndescription: Publish curated release notes for an existing Mole `V<version>` tag, including bilingual format, `gh release edit`, contributor thanks, and reactions. Use only when explicitly asked to edit or publish Mole release notes. Not for release readiness, tagging, or code review.\ndisable-model-invocation: true\nmetadata:\n  internal: true\n---\n\n# Mole release notes\n\nThis skill drives the curated-notes step that runs **after** `release.yml` has finished. The workflow creates the GitHub Release with assets but with `generate_release_notes: false`, so notes must be added in a follow-up `gh release edit` (never `gh release create`, the release already exists, and `create` will conflict).\n\n## Inputs to gather\n\nBefore drafting, confirm:\n\n1. **Version**. Capital `V`, e.g. `V1.38.0`. Lowercase `v` does not trigger the workflow and may indicate a botched tag.\n2. **CodeName + emoji**. Ask the user. The title format is `V<version> <CodeName> <emoji>`.\n3. **Release commit range**. `git log <previous-tag>..V<version> --oneline` gives the raw material.\n4. **User-visible behavior changes**. Scan the full commit message bodies (not just subjects) for narrowed detection, removed features, or controlled regressions. These belong in notes even when they are not bug-fix-shaped, because users will encounter the changed boundary in production.\n5. **Issue reporters and PR contributors in this cycle**. Derive them from what the release actually contains, never from a date window of closed issues: `git log --format='%an' <prev>..<tag>` for contributors, and the issue numbers the range's own commit messages cite for reporters, checking each is `state_reason: completed` and was fixed here rather than merely referenced as background. A closing window silently pulls in `not_planned` issues and everything the PREVIOUS release fixed, which is how V1.55.0 nearly shipped 30 names where 8 were real. Keep it short, for example `Issue reporters and PR contributors this cycle: @a · @b.` Exclude `tw93`, `youxi798` and bots.\n6. **Verify release exists**. `gh release view V<version> --repo tw93/Mole --json id,name` should return non-empty. If it doesn't, the workflow hasn't finished, wait, don't `gh release create`.\n\n## Pre-flight (published-tag evidence)\n\nUse the target tag's exact commit and the completed checks and public-asset evidence from [release-flow](../release-flow/SKILL.md). Reuse that evidence when it still matches the immutable tag; rerun only a missing or failed gate. A build of a newer working tree does not verify the release being described. The full build, test, asset, and script-update gates stay owned by `release-flow` rather than a second checklist here.\n\nIf evidence is missing, complete the applicable release-flow gate before publishing notes. Keep draft work read-only; an existing release object alone is not proof its assets or upgrade path passed.\n\n## Format\n\nStrictly follow the current compact release shape. Read the previous published stable release before the target tag as the live format reference. Exclude the candidate release: the workflow may already have made it the latest release with an empty body. Resolve the previous tag first, then read it with `gh release view <previous-tag> --repo tw93/Mole --json tagName,body`.\n\nStructure:\n\n```\n<div align=\"center\">\n  <img src=\"https://cdn.tw93.fun/pic/cole.png\" alt=\"Mole Logo\" width=\"120\" height=\"120\" style=\"border-radius:50%\" />\n  <h1 style=\"margin: 12px 0 6px;\">Mole</h1>\n  <p><em>Deep clean and optimize your Mac.</em></p>\n</div>\n\n### Changelog\n\n1. **<English headline>**: <one-sentence English elaboration>.\n2. ...\n\n### 更新日志\n\n1. **<中文 headline>**：<一句中文说明>。\n2. ...\n\n### Thanks\n\nIssue reporters and PR contributors this cycle: @handle1 · @handle2.\n\n### Mole Mac App\n\nPrefer a GUI? [Mole Mac App](https://mole.fit/) brings cleaning, app management, maintenance, disk analysis, and live system status into one native app, with review before deletion and a customizable menu bar HUD. It is $19 once, with lifetime updates and a 14-day refund. [Download and try it](https://mole.fit/download). The CLI stays free and open source.\n```\n\nNo `---` separators between sections, and no trailing repository link; the published pages end on the Mole Mac App line.\n\n### Format rules (all are documented bugs that have shipped before)\n\n- **Body h1 is just `Mole`**. Version, codename, and emoji live only in the `--title` argument (`V<version> <CodeName> <emoji>`); repeating them in the body header is redundant and has been explicitly rejected before.\n- **No em dash anywhere**. Use commas, periods, colons, semicolons, or parentheses.\n- **No sponsor list by default**. The current public release style thanks issue reporters and PR contributors for this cycle only.\n- **No emoji except the version emoji in the release title**. Body section headers stay plain, including `### Thanks` (the old `Thanks 💖` header is gone from the published pages).\n- **No inline PR refs, no inline `@handle` thanks**. PRs and people belong in the dedicated Thanks block only.\n- **English block first, 中文 block second**. Same numbered order in both blocks. Same number of items.\n- **Order items by user-perceived impact, not commit chronology**. Headline change first; internal safety hardening, performance, and bug fixes follow.\n- **Do not describe overview icons that no longer exist**. Analyze overview rows are text-only because emoji width and baselines vary across terminals. If icons return later, they must not imply that user data such as iOS Backups, Xcode Archives, or Old Downloads is safe to delete.\n- **Verify every command mentioned in the notes actually exists in HEAD**. The deleted `mo check` / `mo doctor` commands nearly shipped in notes as a \"feature\" after they were removed.\n- **An incident or troubleshooting note is one sentence of symptom plus one command**. No cause taxonomy, no command per branch; the user needs the one line that gets them unstuck. Match the previous release's language treatment for that note: if the last release carried it in one language, do not add a second.\n- **Keep the Mole Mac App cross-link to one restrained, fact-backed paragraph**. Validate the product scope, price, updates, refund window, and download URL against the current homepage before publishing.\n\n## Publish\n\nOnce the user approves the draft:\n\n```bash\ngh release edit V<version> --repo tw93/Mole \\\n  --title \"V<version> <CodeName> <emoji>\" \\\n  --notes-file <path-to-draft>\n```\n\n**Never** `gh release create`, it conflicts with the release the workflow already made.\n\nThen add the six reactions with this skill's helper (path is relative to this SKILL.md, not the repo-root `scripts/`): `bash \"$(dirname <this SKILL.md>)/scripts/post-reactions.sh\" V<version>`.\n\n## After publish\n\n- `gh release view V<version> --repo tw93/Mole --web` (open in browser) so the user can eyeball it.\n- Remind the user: the Homebrew Core PR is workflow-driven and should already be in flight; do not re-run it manually unless the workflow log shows a failure.\n\n## When NOT to act\n\nThis skill is user-invocable only. It must not run unprompted:\n\n- If the user mentions release notes in passing, draft only; do not call `gh release edit`.\n- If `gh release view` shows the release does not exist yet, wait for the workflow; do not create a competing release manually.\n- If the user has not given an explicit \"publish\" / \"提交\" signal, stop after the draft.\n\n## Helper script\n\n`scripts/post-reactions.sh <tag>` lives next to this SKILL.md and adds the six reactions (`+1`, `laugh`, `hooray`, `heart`, `rocket`, `eyes`) to the release.\n",".claude/skills/bugs/SKILL.md":"---\nname: bugs\ndescription: \"Mole incident catalog for cleanup safety, bounded probes, cancellation, dry-run parity, and actionable gates. Use for a Mole safety-sensitive diff; not for docs, release notes, or generic review.\"\nmetadata:\n  internal: true\n---\n\n# Mole bug patterns\n\nGeneric review belongs to Waza `check`; root-cause investigation of a live failure belongs to `hunt`.\n\n## Route before loading details\n\nChoose only the reference families touched by the evidence. A whole-project audit should classify surfaces first instead of loading every incident narrative.\n\n| # | Recurring shape | First probe | Read |\n|---|---|---|---|\n| 1 | Deletion candidate built from a weak name signal | Inspect name, bundle-id, fallback globs, and failed-owner shortcuts | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 2 | Existence or idleness decided by one probe | Enumerate every legitimate location and unknown outcome | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 3 | Guard present on only one branch | Diff dry-run, real, direct, fallback, and final-sink paths | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 4 | Unbounded external command | Count producer, consumer, inner-loop, and action bounds | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 5 | Bash 3.2, errexit, or pipefail trap | Check empty arrays, `fn || handler`, and optional actions | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 6 | TTY, stdin, or process-group theft | Inspect background workers and commands that may prompt | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 7 | System output parsed as a stable API | Force locale, validate shape, and join on identifiers not headings | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 8 | Persisted derived data outlives its algorithm | Trace schema, TTL, evidence fingerprint, and mutations | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 9 | Two paths compute one number differently | Find every producer and choose one definition | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 10 | Slow work looks frozen | Find operations over roughly one second outside feedback | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 11 | Regression test cannot fail | Prove positive control and pre-fix red state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 12 | Gate cannot explain why it refused | Map each reason to one cause and a next action that fails on the broken state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 13 | Mutation target is also accepted as a discovery container | Compare the recursive scan-root namespace with every purge target basename | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 14 | Owner metadata is treated as a complete, atomic inventory | Identify who writes it, whether absence is authoritative, and what locks mutation | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 15 | Cancellation stops one helper but later work continues | Classify each 124 as skip, section budget, or sticky cancel before tracing callers | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 16 | Async or cached data has no generation or freshness contract | Bind results to a request epoch and keep each sample's time, stale, and completeness fields together | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 17 | Publication gate trusts ambiguous or pre-existing state | Require exact source/tag equality, one generated target, and an expected-absence ref lease | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 18 | A sandbox well-known path is treated as app-private leftovers | Resolve Data/Downloads, Desktop, Pictures, Music, and Movies physically against $HOME | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n\nAGENTS.md and the path-scoped `.claude/rules/` files keep each rule stated fully enough to obey plus its test anchor; the incident story lives in these references or in `release-flow`, and the rule bullet points to the section that holds it. Moving a story here is a merge, never the deletion of a rule that still constrains behavior.\n\n## Trace the complete mutation lifecycle\n\nFor cleanup, purge, optimize, analyze deletion, or uninstall work, review the complete chain rather than the reported branch:\n\n```text\ndiscover or plan\n  -> cheap irreversible filters\n  -> owner and open-handle probes\n  -> size or metadata work\n  -> final owner re-probe\n  -> parent and target identity rebind\n  -> deletion or Trash sink\n  -> accounting, cancellation, and user output\n```\n\nAt every transition, answer:\n\n- Does live or unknown state fail closed?\n- Are timeouts classified by probe, sizing, removal, or section scope, with unknown evidence refusing deletion and cancellation stopping later mutation?\n- Are probe and sink bound to the same physical parent and target?\n- Do dry-run and real mode start from the same eligible plan without reusing stale authorization?\n- Are cheap missing, protected, whitelisted, and compiled-model filters ahead of recursive probes?\n- Does one cumulative deadline cover the dynamic scan scope, with checkpoints in nested loops?\n- Do refused, filtered, timed-out, or failed items stay out of cleaned counts and reclaimed bytes?\n- Can large candidates avoid per-item size work without making the reported total false?\n\nDo not trade final-sink rebinding or fail-closed owner checks for speed. Optimize absent targets, duplicated discovery probes, report-only work, and wrong-scope scans first.\n\n## Working contract\n\n- Before proposing a remedy, apply the Product Decision Filter in `AGENTS.md`: a new flag, environment variable, or visible retry message is a product change, even when the underlying fix is a safety improvement. Use the existing state/accounting reference when deciding what belongs in the normal summary.\n- A sandbox well-known directory is user data until physical resolution proves otherwise. `Data/Downloads` is usually `~/Downloads` under another name.\n- A refusal next step that succeeds on the broken state is not a next step. Do not relax the gate to make the message nicer.\n- Sweep siblings by call-site shape, not filename or helper name. Report `checked N / defective M / not applicable K`.\n- A recurring fix ships with a regression or source invariant that fails against the pre-fix code.\n- Treat tests as production consumers only after proving the production helper ran. Negative assertions require a positive trace.\n- A cancellation regression makes the next candidate otherwise eligible, then proves its probe and sink never run. Making every candidate fail for the same reason is a false sticky-cancellation test.\n- Absence-sensitive tests use an isolated `HOME` or fixture root; a shared `setup_file` home is not isolation.\n- Reproduce CI through `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh` when possible. If invoking Bats directly with jobs, preserve `--no-parallelize-within-files`; files share state and raw `bats --jobs 6 file.bats` changes semantics.\n- Treat specialist or AI reports as leads. Read the implementation, callers, fallback branches, and final sink yourself.\n\n## Verification bar\n\nUse the hotspot commands in the matching `.claude/rules/` file (routed from `AGENTS.md`); do not guess a narrower verifier. A typical Shell safety change finishes with:\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 bats tests/<area>.bats\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\ngo test ./...\nMOLE_TEST_NO_AUTH=1 MOLE_DRY_RUN=1 ./mole clean --dry-run\n```\n\nNever infer a production defect from a function name, comment, string, fixture, or `_test.go` match. Confirm the live call path and verify red-green before reporting the class fixed.\n","AGENTS.md":"# Mole Agent Guide\n\nThis 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.\n\nLayout: 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.\n\n| File | Scope |\n|---|---|\n| `.claude/rules/shell.md` | Shell portability: BSD-only tools never through PATH, duplication audit, dead-symbol checks |\n| `.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 |\n| `.claude/rules/clean.md` | `mo clean` orchestration and sections, user/app/system caches, browsers, firmware, logs, orphan services, project caches, Homebrew, whitelist |\n| `.claude/rules/clean-dev.md` | `lib/clean/dev.sh`: developer and toolchain caches, recovery contracts, Nix, simctl, AI tool caches |\n| `.claude/rules/purge-installer.md` | `mo purge` discovery and protection, worktrees, `mo installer` |\n| `.claude/rules/uninstall.md` | `mo uninstall` inventory, display names, app protection, leftovers, sibling guard, batch execution, Homebrew casks |\n| `.claude/rules/optimize.md` | `mo optimize` tasks, preference repair, Spotlight, LaunchServices |\n| `.claude/rules/slim.md` | `mo slim`: Intel slice removal scope, seal-aware thinning, verified swap |\n| `.claude/rules/go-tuis.md` | `cmd/analyze` and `cmd/status` Go TUIs |\n| `.claude/rules/install-update.md` | `mole` router pins, `install.sh`, `mo update`, self-heal, install lock, `mo remove` |\n| `.claude/rules/tests.md` | Bats and Go test validity, test lint, CI jobs |\n| `.claude/rules/github.md` | Issue, PR and Discussion replies and closeout, Mole Mac invitation, announcements, issue version preflight |\n| `.claude/rules/readme.md` | README copies in every language |\n\nThe 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.\n\n## Product synchronization\n\nUse 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.\n\nEvery 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.\n\nBefore 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.\n\nCover 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.\n\n## Project\n\nMole 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.\n\n## Product Direction\n\nMole 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.\n\n### What Mole Should Do\n\n- Make cleanup and uninstall actions boring, reviewable, logged, protected by path/app rules, and dry-run capable.\n- Prefer reversible user-facing removals through Trash where the command surface expects recoverability.\n- Keep `clean`, `uninstall`, `purge`, and `installer` focused on reclaimable files, app leftovers, rebuildable caches, installer artifacts, and exact known cleanup targets.\n- 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.\n- 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.\n- Keep `optimize` focused on explicit, bounded maintenance tasks that can be explained before execution and tested without real authorization prompts.\n- Keep command UX dense and terminal-native: short labels, stable alignment, predictable shortcuts, one-screen summaries, then optional drill-down.\n- 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`.\n- Keep Mole Mac references as a cross-link or support path. The CLI and Mac app can share product values without requiring feature parity.\n\n### What Mole Should Not Do\n\n- Do not add broad system modification, privacy reset, package management, app bundle patching, or device-management features just because they are technically possible.\n- 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.\n- Do not add background agents, persistent monitoring, notifications, schedulers, menu bar behavior, or GUI-like state unless explicitly requested and justified as CLI scope.\n- Do not broaden leftover matching from exact app or bundle evidence into vendor-wide, TeamID-prefix, generic-name, or fallback wildcard deletion.\n- 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.\n- 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.\n- 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.\n\n### Product Decision Filter\n\nBefore accepting a new feature, answer these questions in the PR, issue, or review notes when the fit is not obvious:\n\n1. Does it clearly belong to clean, uninstall, analyze, optimize, status, purge, history, installer, slim, update, completion, touchid, or remove?\n2. Is it safe by default, previewable where destructive, testable without real auth, and explainable in one terminal screen?\n3. Can the user verify what will change before Mole changes it?\n4. Is the target data locally rebuildable, disposable, or backed by exact app/bundle evidence?\n5. Would this be better as Mole Mac UI, documentation, a warning, or an explicit \"not supported\" answer?\n\nIf the answer is no or unclear, decline the feature, narrow it, or park it until the product value beats the added surface area.\n\nA 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.\n\n## Repository Map\n\n- `.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.\n- `.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.\n- `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.\n- `lib/core/` - shared shell safety, UI, file operations, operation logs, app protection logic, and centralized timeout constants (`timeouts.sh`).\n- `lib/core/app_protection_data.sh` - readonly bundle ID and pattern arrays consumed by `app_protection.sh`. Data only, no logic.\n- `cmd/analyze/` and `cmd/status/` - Go TUIs (see `.claude/rules/go-tuis.md`).\n- `tests/` - Bats suites, PTY drivers and fixtures (see `.claude/rules/tests.md`).\n- `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`.\n- `docs/SECURITY_DESIGN.md` - design doc for the path validation / app protection / # SAFE annotation contract.\n- `SECURITY_AUDIT.md` - security review notes.\n\n## Commands\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\nMOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats\nMOLE_DRY_RUN=1 ./mole clean\nMOLE_TEST_NO_AUTH=1 ./mole clean --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole purge --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole installer --dry-run\nfind bin lib -name '*.sh' -print0 | xargs -0 -n1 bash -n\nmake build\ngo test ./...\n```\n\nPublic 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.\n\n## Critical Safety Rules\n\n- 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.\n- 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.\n- Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.\n- 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.\n- 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.\n- 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`.\n- 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.\n- 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.\n- 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.\n- 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`.\n- 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`.\n- 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`.\n- 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`.\n- 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.\n- **`_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.\n- **`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`.\n- **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`.\n- **`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`.\n- **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 `&amp;` last, since a key holding the literal text `&lt;` 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.\n- 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`.\n- Do not change ESC timeout behavior in `lib/core/ui.sh` unless explicitly requested.\n- 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`.\n- 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`.\n- **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.\n\n## Working Rules\n\n- 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.\n- 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.\n- `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.\n- Keep shell code formatted with `./scripts/check.sh --format`.\n- Prefer targeted Bats tests during development; run the full suite before committing.\n- Do not add AI attribution trailers to commits.\n\n## Verification\n\n- 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`).\n- `lib/core/sudo.sh` changes: run `MOLE_TEST_NO_AUTH=1 bats tests/manage_sudo.bats`.\n- Go changes: run `go test ./...`.\n- Area changes: run the test command listed for that hotspot in its `.claude/rules/` file; do not guess a narrower verifier.\n- Documentation-only changes: check links and commands.\n- **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.\n- **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.\n- **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.\n- 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.\n\n`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.\n\nIf `golangci-lint` reports issues from deleted temporary worktrees or non-existent paths, clear its local cache and rerun the linter:\n\n```bash\ngolangci-lint cache clean\ngolangci-lint run ./cmd/...\n```\n\n## Release\n\n**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.\n\nThe 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`.\n\nTag-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.\n",".claude/agents/safety-reviewer.md":"---\nname: safety-reviewer\ndescription: Audits Mole changes for destructive-action regressions across deletion, app protection, privilege boundaries, dry-run behavior, operation logging, package-manager cleanup, and exact leftover matching. Use before merging changes under lib/clean/**, lib/uninstall/**, lib/manage/**, bin/clean.sh, bin/purge.sh, bin/uninstall.sh, bin/installer.sh, lib/core/file_ops.sh, or lib/core/app_protection*.sh.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole destructive-action safety reviewer\n\nRead `AGENTS.md` sections \"Critical Safety Rules\", \"Working Rules\", and\n\"Verification\", every `.claude/rules/*.md` file whose `paths:` match the diff\n(its routing table names them), plus `docs/SECURITY_DESIGN.md`, before judging\nthe diff. Those files are the current safety contract. This profile defines the\nreview method and output shape only; it must not become a copied policy list.\n\nYou read code and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base and read the issue or PR scope.\n   A request for one leftover path is not permission to add a broader matcher.\n2. Mark every changed destructive sink and every new path source. Pay special\n   attention to `find_app_files`, `mole_delete`, `remove_file_list`, container\n   traversal, Group Containers, bundle-prefix matchers, and recursive `find`\n   branches that eventually delete.\n3. Audit each branch independently, including fallbacks. For every candidate,\n   prove exact app or bundle evidence, protected-path coverage, preview or\n   confirmation, dry-run behavior, operation logging, and the final deletion\n   helper. A safe primary branch does not make a broad fallback safe.\n4. Treat raw removal outside `lib/core/file_ops.sh` as P0 unless the call site\n   has a narrow `# SAFE:` exception for an already verified exact path and a\n   regression test that proves why the shared funnel cannot be used. Never\n   generalize one exception into a second deletion API.\n5. For new `sudo`, `osascript`, `launchctl`, package-manager, or service teardown\n   calls, verify test/auth guards, non-interactive test behavior, exact preview,\n   and failure propagation. Typed password input must not be mistaken for skip.\n6. For uninstall teardown, prove every route passes the shared-bundle-id sibling\n   guard, including volume copies, inverse names, and shared identities.\n7. Read enough surrounding production code to follow helper calls to their final\n   sink. Then map the change to the exact hotspot commands in the matching\n   `.claude/rules/` file and \"Verification\"; missing safety coverage is a finding.\n\n## Severity\n\n- **P0**: a path can escape its intended target, protection/confirmation/dry-run\n  is bypassed, a destructive failure can be reported as success, or a privileged\n  action can execute during ordinary verification.\n- **P1**: matching is broader than exact evidence, a teardown route bypasses a\n  shared guard, active user/developer state can be removed, package-manager\n  candidates are not previewed, or a safety regression lacks a direct test.\n- **P2**: the behavior is bounded but the documented targeted verification was\n  not run or the failure output is not actionable.\n\nDo not flag style, speculative refactors, or test-only panic/expect patterns.\nIf a helper or guard cannot be traced, report it as `UNVERIFIED` rather than\nassuming it is safe.\n\n## Output\n\nOrder findings by severity:\n\n```\nP0: <file>:<line> - <problem>\n  Why unsafe: <broken invariant>\n  Fix: <one concrete change>\n```\n\nUse the same shape for P1 and P2. End with `VERDICT: changes required` when any\nP0 or P1 exists, otherwise `VERDICT: safe to merge`. With no findings or\nunverified items, output only `VERDICT: safe to merge`.\n",".claude/skills/release-notes/scripts/post-reactions.sh":"#!/bin/bash\n# Add the standard six reactions (+1, laugh, hooray, heart, rocket, eyes) to a\n# tw93/Mole release. Usage: post-reactions.sh V<version>\n\nset -euo pipefail\n\nTAG=\"${1:-}\"\nif [[ -z \"$TAG\" ]]; then\n    echo \"Usage: $0 V<version>\" >&2\n    exit 1\nfi\n\nif [[ \"$TAG\" != V* ]]; then\n    echo \"Tag must start with capital V (release.yml ignores lowercase v): $TAG\" >&2\n    exit 1\nfi\n\nif ! command -v gh > /dev/null 2>&1; then\n    echo \"gh CLI is required\" >&2\n    exit 1\nfi\n\nRELEASE_ID=$(gh api \"repos/tw93/Mole/releases/tags/$TAG\" --jq '.id')\nif [[ -z \"$RELEASE_ID\" ]]; then\n    echo \"Release not found for tag: $TAG\" >&2\n    exit 1\nfi\n\nfor r in +1 laugh hooray heart rocket eyes; do\n    gh api \"repos/tw93/Mole/releases/$RELEASE_ID/reactions\" \\\n        -X POST -f content=\"$r\" --silent\ndone\n\necho \"Posted 6 reactions to $TAG (release id $RELEASE_ID)\"\n",".claude/skills/release-notes/agents/openai.yaml":"policy:\n  allow_implicit_invocation: false\n",".claude/skills/bugs/references/shell-and-test-pitfalls.md":"# Bounds, Shell, TTY, and parsing\n\nRead this reference when changing Shell code, Bats tests, update/install flows, timeout wrappers, TTY handling, plist fixtures, or macOS-version-specific CI behavior. The defect classes and repo-wide probes stay in the parent `bugs` skill.\n\n## 4. An external command or its consumer is unbounded\n\n`du`, `mdfind`, `find`, `xcrun simctl`, `system_profiler`, `ioreg`, and package tools can stall on a healthy but slow machine. Every production `du -s` route stays behind `run_with_timeout` with `MOLE_TIMEOUT_DISK_VERIFY_SEC`; `tests/core_timeout.bats` pins the class across `lib/` and `bin/`.\n\nCheck more than the obvious command:\n\n- Put checkpoints in every nested loop, not just each outer root (`edb214c0`).\n- Tune against the slowest healthy case. CoreSimulatorService needed a warm-up retry after a two-second bound falsely reported it unavailable (`35d856f1`).\n- Materialize a bounded producer completely and discard its output on nonzero status. Process substitution plus `|| true` must not feed a partial `find` prefix into deletion.\n- Keep probe and action pattern, type, age, and depth identical.\n- Time producer and consumer separately before raising a timeout. A 2.3-second `lsregister` dump followed by one command substitution per input line still becomes minutes.\n- Bound installed-binary `--version` and `--help` verification. Broken executables are the ones most likely to hang.\n- Keep install and update single-flight per target directory so one process cannot verify another generation.\n- The self-update bootstrap on a user's machine (temp file, registry, exec) is frozen at whatever version installed it, so a broken installed version cannot repair itself. That is why `_update_self_heal_reinstall` streams `install.sh` from `main` into bash with no local temp files (#1297). V1.47.1 shipped the false-success shape the bounded version check now prevents: update success read from installer output instead of from the installed binary.\n- The single-flight mutex prefers absolute `/usr/bin/lockf` because the kernel drops that lock even if the holder is killed. `lockf` only ships with newer macOS, and requiring it made install and update exit before writing a file on every older release (#1348); the `mkdir` fallback exists for those releases. The lock wrapper is not a shell array because the empty array is the fallback path, and an empty array under `set -u` is an unbound-variable error on Bash 3.2 (section 5).\n\n```bash\nfor command_name in 'du -s' mdfind xcrun system_profiler ioreg brew; do\n    printf '%-16s total=%-4s wrapped=%s\\n' \"$command_name\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | wc -l | tr -d ' ')\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | command grep -c run_with_timeout)\"\ndone\n```\n\n## 5. Bash 3.2, errexit, and pipefail change meaning\n\nmacOS ships Bash 3.2 and Mole runs with nounset.\n\n- Guard `\"${arr[@]}\"` with `[[ ${#arr[@]} -gt 0 ]]`; an empty array under `set -u` can abort a scan and orphan its spinner (`893b4e6f`, `2c06cb91`).\n- `fn || handler` disables errexit inside `fn` for the whole function. Safety-critical steps use explicit `if ! command; then return 1; fi` (`a33a0b51`). Installers must also verify the installed binary's reported version before claiming success; `tests/install_checksum.bats` covers the exact caller shape.\n- Do not rely on a caller's temporary `set +e` window for graceful degradation. Capture the status where the command runs.\n- Optional `[[ -n \"$value\" ]] && action` returns 1 when absent. Use `if/fi` inside status-sensitive blocks.\n- Fixing a branch that Bash 3.2 silently skipped is a behavior change for every caller. `read -t 0.01` fails on 3.2, so `drain_pending_input` never drained there; `ed02a6aa` made it flush the terminal queue after every menu key, and a pasted search term kept only its first character. Before enabling such a branch, list its callers and check each for text entry, call the system tool by absolute path (a version-manager `perl` shim can fail or add startup latency per key), and never fall back to a one-second integer read on a terminal.\n\n```bash\ncommand grep -rn '\\$\\{[a-z_]*\\[@\\]\\}' lib/ bin/\n```\n\n## 6. Background work steals TTY, stdin, or process groups\n\nThe Perl timeout fallback can hand the controlling terminal to its child. A background metadata worker then stopped the foreground uninstall prompt with SIGTTIN (`c93afca3`). BSD `mv` and `cp` can also prompt on stderr and read stdin when a destination is unwritable (`63030e3a`).\n\nEvery background worker that calls `run_with_timeout` closes stdin with `< /dev/null`. Commands that can prompt also use their noninteractive or force option. Menu and scan traps save and restore the caller's traps; `lib/ui/menu_paginated.sh` is the reference.\n\n## 7. System command output is treated as an API\n\nmacOS command output is localized, drifts between releases, and can print errors where data is expected.\n\n- Force `LC_ALL=C` for parsed metric subprocesses (`51b352a2`, `fa05b8cc`, `4e83743b`).\n- Validate field shape before trusting it: absolute path, numeric value, expected key, or exact enum.\n- Keep `DTSDKBuild` build identifiers separate from `DTPlatformVersion` versions (`f0896d03`).\n- Reject PlistBuddy's missing-file prose as data.\n- Use stock macOS semantics when checking flags. BSD `grep -Z` means `--decompress`; a developer alias may hide that.\n- Prefer exit codes, plist keys, and machine-readable output over prose matching.\n- Join records on the machine identifier, never on a heading or display string. `simctl runtime list` titles each image with the image version (`iOS 26.4.1`) while `simctl list devices` groups under the runtime short name (`iOS 26.4`). A name join calls every point release an orphan and offers `simctl runtime delete` for a runtime its simulators still bind (`#1505`). `mdls -name kMDItemDisplayName` returns the on-disk file name, not Finder's localized name, so it always differed from `app_name` and always won the old selection, leaving the `CFBundleDisplayName` and `CFBundleName` branches unreachable and shipping folder names like `VideoFusion-macOS` (`#1520`).\n- `plutil -p` is documented by `man plutil` as unstable and not designed for machine parsing. macOS 15 prints a JSON boolean true as `1` while macOS 26 and 27 print `true`, so a filter pinned to one spelling silently cleaned nothing on the other: `#1512` was fixed and then regressed for eight days with CI green. Where a boolean prints as `1` nothing can tell it from the integer 1. The rendering is also depth-blind, so `{\"outer\":{\"inner\":true}}` offered a directory named `inner` for deletion, a wrong deletion rather than a missed one. That is why `tests/clean_app_caches.bats` runs on the macos-15/macos-26 compatibility job.\n\nUse `command grep` when flag behavior matters, because the interactive environment may alias it.\n\n## 15. Cancellation is local unless orchestration makes it sticky\n\nClassify a timeout at its source before propagating it. Signal-derived cancellation and safety-guard timeouts that the caller treats as cancellation must remain sticky across the remaining command. Status `124` alone does not define the scope. Read the probe's product contract first:\n\n| Contract | Typical `124` | Typical `>=128` |\n|---|---|---|\n| Safety guard, owner unknown, or cancellation the caller already established | Sticky stop; later mutation and later sections must not start | Sticky stop |\n| Per-item sizing or removal of an otherwise eligible candidate | Skip or fail that item; later items may continue (`#1374`, `#1384`, `#1576`) | Sticky stop |\n| Review-only or advisory listing that never deletes | Skip that advice; later cleanup sections continue (`#1571`) | Sticky stop |\n| Cooperative section budget | Stop the rest of that section, report partial, continue later sections (`#1513`) | Sticky stop |\n\n- In `bin/clean.sh`, a final delete guard returning `124` cancels later work, while an individual `safe_remove` timeout is a reported failed removal and later items may continue. Timed-out sizing contributes an unknown/partial total, not false reclaimed bytes. Large-files Mail / Downloads / Updates rows follow the review-only skip: a size timeout omits that row, and a signal still cancels (`#1576`, `#1344`).\n- The orphaned-runtime review is advisory. A cold `simctl` that returns `124` skips the review and must not cancel later `mo clean` sections. Skip the review entirely when the unavailable-simulator listing already timed out after its warm-up retry (`#1571`, `04e658d2`).\n- Orphan leftover probe and sizing timeouts (`mdfind` in `is_bundle_orphaned` / `is_claude_vm_bundle_orphaned` / `_container_stub_app_exists`, plus `get_path_size_kb` and candidate snapshots) fail closed for that item and later leftovers plus later `mo clean` sections continue. A `safe_clean_guarded` 124 at the sink stays sticky (`#1584`). Do not cache a timed-out Spotlight miss as \"not installed\".\n- Cloud & Office uses a cooperative section deadline: stop the remaining items in that section, preserve parent counters, report partial completion, and continue later sections. Do not restore its removed outer timeout worker or the file-backed deferred-family replay that existed only for that worker.\n- Purge discovery discards incomplete root scans and marks the run incomplete. An authored-content probe returning `2` keeps and visibly reports that candidate; deletion-phase activity or removal timeouts cancel the run. Unknown evidence never permits deletion.\n\nThe inverse defect is as common as a missed sticky cancel. Treating a review-only `124` as command-level cancellation skips every later section. Treating a safety-guard `124` as a local skip deletes with unknown evidence.\n\nOnce the caller establishes cancellation, carry that decision through every boundary:\n\n- A best-effort loop must check a pending cancellation before probing or registering the next candidate.\n- A helper that reports ordinary misses as success must return a pending cancellation before starting the next family.\n- Command substitution, subshells, and background workers do not mutate the parent's shell variables. Return the status, or use an explicit parent-readable channel, then record it again in the parent.\n- A parallel coordinator stops and reaps peer workers, preserves cancellation over ordinary failures, and prevents the next rendered section from starting.\n- Dry-run uses the same cancellation contract as real cleanup. A preview ledger is still downstream work and must not continue after safety evidence becomes unavailable.\n\nThe regression shape matters. Make the first candidate's safety guard return 124 or 130 and make the second candidate succeed if reached. Assert the exact top-level status plus the absence of a positive trace from the second probe, preview registration, sink, and later section. If both candidates independently time out, the test cannot prove cancellation was sticky. Separately preserve the removal-timeout and cooperative-section-budget continuation cases in `tests/clean_core.bats`.\n\nDo not hide a cancelled safety probe behind `|| true`, a warning plus `return 0`, or a worker-local exported variable. Those shapes turn a global stop into a local skip.\n\nName the question instead of spelling the numbers: `mole_rc_timeout` and `mole_rc_timeout_or_signal` in `lib/core/timeout.sh` are the only places that compare a status against 124, and `scripts/audit_timeout_status.py` rejects a raw comparison anywhere else in `check.sh`. A per-item size whose number only feeds totals goes through `mole_item_size_continues` in `lib/core/file_ops.sh`: a timeout or failure keeps the item with an unknown size and marks the freed total partial, a signal records the cancellation and stops. Choosing between those helpers is still the caller's contract decision from the table above; the helper only makes the choice visible in review.\n\nA third predicate, `mole_rc_signal`, is true only for a status above 127 that `kill -l` names. A mutating owner command can exit with an errno-derived code such as npm's 243, which is an ordinary failure, so the owner-command wrappers (`clean_tool_cache`, `bun pm cache rm`, `simctl delete unavailable`, `brew cleanup`, the tart prune and `go clean`) cancel only on it and skip that one command on 124 (`clean_tool_cache treats non-signal high owner statuses as failures (#1695)`, `clean_tool_cache keeps valid signal statuses sticky (#1695)`, `clean_dev_go treats an errno-derived go clean exit as a failure, not an interrupt`). A timed-out `go env` lookup skips only the Go caches because nothing was attempted yet, while the `gh` probe and clear, the automation-browser `ps` listing and the clang cache resolver stay sticky (`clean_dev_cloud stops on GitHub CLI probe or clear cancellation`).\n\n## Focused pitfalls\n\n- **A PTY read loop must end on an empty read, an `OSError`, or a deadline.** After the child exits, a macOS pty master can stay readable and return `b''` forever, so a `while select(...)` drain with no exit spins until the job times out; Linux raises `EIO` instead. `tests/sudo_adoption_pty.py` hung this way on #1747 (fixed in the squash `eb531ba2`); the other `tests/*_pty.py` loops are deadline-bounded.\n\n- **Launcher signal stories behind the `lib/manage/update.sh` rules in `.claude/rules/install-update.md`**: a tty7 pane can inherit the daemon's blocked TERM mask, so a child launched without unblocking it left uninstall waiting forever for its spinner and made Ctrl-C cleanup hang. Bash prints a signalled child's job-status line (`Killed: 9 ... perl ...`) from whichever command finishes first after the child dies, so a runner that stalls the router for a few milliseconds leaked it at random until the launch, readiness wait and `wait` shared one `2> /dev/null` group. And Bash 3.2 can discard an INT received inside a USR1 trap entered from the `wait` builtin, which is why the readiness notification settles before `wait` starts.\n- **A `find` primary newer than the oldest supported macOS needs a probe, and dataless folders must not be listed**: listing a dataless CloudStorage folder makes File Provider fetch it over the network (`77050616`), an older `find` rejects `-flags +dataless` and fails the whole walk (`8dccb160`), and the capability probe itself must pass signals through (`df2f5e06`). Probe once per process: unknown means walk without the primary, a signal cancels, and never descend into a dataless folder.\n- **`BASH_SOURCE` / `$0` change meaning when a function moves files**: they name the file the code lives in, so copy-paste extraction is not behavior-preserving. `mole` captures `MOLE_ENTRY_SCRIPT=\"${BASH_SOURCE[0]}\"` before sourcing anything, and update code reads that stable entrypoint. Before extracting a function, grep it for `BASH_SOURCE`, `$0`, and `FUNCNAME`. Regression coverage lives in `tests/update.bats`.\n- **Bats heredocs share stdin with `read -n1`**: an inner `read -r -s -n1` can consume the next byte of the heredoc source. Redirect the function under test from `/dev/null`.\n- **macOS `script(1)` rejects socket-backed stdin**: PTY test helpers must redirect the wrapper's stdin from `/dev/null` or `script` can fail before starting the child. Capability probes use `/usr/bin/true`, not the absent `/bin/true`; inspect the actual failure before classifying it as unavailable TTY support, or live terminal tests silently skip.\n- **`run_with_timeout` execs the binary and bypasses shell-function mocks**: tests must use a PATH stub directory for commands such as `osascript`.\n- **CI runners may lack `/Library/PrivilegedHelperTools`**: orphan-service tests should exercise `/Library/LaunchDaemons`, which exists on GitHub macOS runners.\n- **A test can pass vacuously after an early return**: `MOLE_TEST_MODE=1` can leave `$output` empty, and a final negative assertion then passes. End assertions with `|| return 1`, override test mode when the body must run, and add a positive control proving the output path executed. In an inner heredoc script use `|| exit 1`. Confirm the bracket behavior with a minimal repro when it matters: a non-final `[[ ]]` can be swallowed while `[ ]` still gates.\n- **A large payload piped into `grep -q` leaks a broken-pipe line into user output**: `grep -q` exits on its first match, and the `printf` still writing into that closed pipe takes SIGPIPE, which bash reports as `printf: write error: Broken pipe` on stderr. The live-cache owner probe fed the whole process table that way and the message landed mid-run in `mo clean`. Pass the data by here-string instead. Small variables holding a few lines of command output finish in one write and are unaffected, so the existing `echo \"$var\" | grep -q` sites are fine.\n- **Normalize with `10#` before any numeric comparison**: `[[ a -le b ]]` evaluates arithmetically, so a leading zero is read as octal and `0123` ranks below `100`, while `08` and `09` are not valid octal at all and abort the test with a bash error on stderr. A `^[0-9]+$` guard does not prevent either. Codex build numbers compared this way could have called a newer staged build superseded.\n- **`SECONDS` advances in whole seconds, so a 1s budget is not a second**: a deadline built as `SECONDS + 1` really means \"until the next second boundary\" and can collapse to almost nothing, making `_mole_timeout_with_deadline` return 124 before the command ever runs. Every timeout constant is 2 or more for this reason. A test of clamp arithmetic invokes the helper directly after resetting `SECONDS`; an exact value asserted across command substitution still races the fork on a loaded runner even with a larger window. When the fork is the behavior under test, assert a safe range or property instead of one remaining-second literal.\n- **BSD grep has no GNU null-output `-Z` contract**: on stock macOS it means `--decompress`. Enumerate files with `find ... -print0`, then probe each file with `grep -qF`.\n- **PlistBuddy reports missing-file creation on stdout**: redirect both stdout and stderr when creating plist fixtures so diagnostic prose does not pollute Bats `$output`.\n- **macOS 14 Bash can fire errexit through an if-guarded exported mock**: a failing exported `sudo` function inside an `if fn; then` path may terminate a `set -e` script on that runner while passing locally. Around the first sudo probe, disable errexit only for the probe and restore it before validation-gate returns. CI-only failures must print exit status, output, and a mock call trace rather than a bare return-code assertion.\n- **Expected capability absence is not probe failure**: keep `ready`, `not applicable`, and `misconfigured or unknown` distinct. A Mac with standalone Command Line Tools and no Xcode app has no simulator surface, so missing `simctl` is a quiet skip; an explicit invalid `DEVELOPER_DIR` remains actionable. Tests prove the not-applicable path does not invoke the unavailable owner command or emit warning activity.\n- **Fixed-width prefixes may have a free-form remainder**: after validating numeric and enum columns, join the remaining fields instead of assigning semantic meaning to one whitespace token. Darwin process `comm` values can contain spaces; trimming them to the first word destroyed zombie-parent attribution for helper apps. Keep primary and fallback formats separate when their fields carry different semantics.\n"},"files":{".claude/skills/release-flow/SKILL.md":"---\nname: release-flow\ndescription: \"Mole CLI release runbook for distribution channels, pre-flight checks, capital-V tags, artifacts, and curated-note handoff. Use when assessing or executing a Mole release. Not for release-note copy alone or ordinary code review.\"\nmetadata:\n  internal: true\n---\n\n# Mole CLI Release Flow\n\nTag-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 opens a Homebrew core PR.\n\n## Distribution channels\n\n| Channel | What ships | Trigger | Automation |\n|---|---|---|---|\n| Nightly (`mo update --nightly`) | `main` HEAD via `install.sh` | Any commit pushed to `main` | Automatic; no tag or release involved |\n| GitHub stable release | amd64/arm64 binaries + `SHA256SUMS` | Push a capital-`V` tag | `release.yml` builds and creates the release; curated notes are a manual follow-up |\n| Homebrew core | Version-bump PR to `Homebrew/homebrew-core` | Same `V*` tag workflow | Automatic PR; merge timing is upstream's |\n| Nix flake | Source-built macOS package from the selected Git ref | `main` follows development; a release tag pins that release | Nix manages install, upgrade and removal; no separate registry publish |\n\nAt the start of any release-flavored task, restate which channels this run will touch and which it will not, and confirm with the maintainer before acting. Channel scope is specified by the maintainer, never inferred.\n\n## Pre-flight checklist\n\nResolve the latest published stable tag from GitHub before choosing the version or review range. Reconcile handoff claims against the current branch, worktree, and remote SHA; an earlier report of uncommitted work may describe commits that have already landed. Review all changes since that stable tag, not just the final fix batch.\n\n1. `grep '^VERSION=' mole` matches the new version.\n2. `SECURITY_AUDIT.md` opening line reflects the new version and date, and its CI coverage list matches the matrix in `.github/workflows/test.yml`.\n3. `git status -s` is empty or only contains intentionally staged release work.\n4. `git log origin/main..HEAD --oneline` shows only commits you intend to ship.\n5. `./scripts/check.sh --format` and `TERM=xterm-256color MOLE_TEST_NO_AUTH=1 MOLE_TEST_JOBS=2 BATS_FORMATTER=tap ./scripts/test.sh` both exit 0.\n6. `go test ./...` and `make build` both pass.\n7. `scripts/e2e_macos.py` exits 0 on a real Mac at the candidate commit. It installs IINA from its verified GitHub DMG, launches it, uninstalls it with every personal-data row selected through a real terminal, lists the DMG in `mo installer --dry-run`, deletes it from `mo analyze ~/Downloads`, and runs every other command read-only, so it changes only what it installed and never asks for a password. It refuses to start when the Mac already has IINA or any of its data. A failed run lists what it left behind; move those to the Trash before rerunning. Transcripts and `report.json` land in `~/Library/Logs/mole-e2e/<run>/`.\n\nUse the Go version declared in `go.mod` for local release builds, matching `actions/setup-go` in CI, then run `scripts/check_release_minos.sh` on both architectures. A newer local Go can raise the minimum macOS version even with `CGO_ENABLED=0`; during V1.54.0 verification, Go 1.27 produced macOS 13 binaries while the declared Go 1.25 toolchain preserved macOS 12. Rebuild with the declared toolchain instead of relaxing the minimum-OS gate. Check the `make release-amd64 release-arm64` outputs, not `make build`: the local build links `status` with cgo against the host SDK, so its `status-go` reports the host macOS as its minimum and says nothing about the release binaries.\n\nCapture the test runner's exit status and structured summary, with skipped tests reported separately. After pushing the candidate commit, wait for its required Check, Validation, CodeQL, and Nix workflows to finish successfully before tagging that exact SHA. Nix must verify both Darwin architectures and the installed/profile lifecycle. A cancelled run or a green check on another commit is not release evidence.\n\n## Tag and publish\n\n```bash\ngit push origin main\ngit tag V<version>          # capital V; release workflow ignores lowercase v\ngit push origin V<version>\n```\n\nWait for the workflow to finish. The workflow creates the release with assets but `generate_release_notes: false`, so notes must be added in a follow-up step.\n\nAfter the workflow finishes, verify the release assets before announcing anything: `gh release view V<version> --json assets --jq '.assets[].name'` must list all four `analyze-`/`status-darwin-{amd64,arm64}` binaries, both `binaries-darwin-*.tar.gz` Homebrew tarballs, AND `SHA256SUMS`. Install verification is fail-closed, so a release without a readable `SHA256SUMS` asset makes every install and `mo update` abort by design; a missing checksums file is a release blocker, not a cosmetic gap.\n\nDownload all seven assets. Verify the six payload checksums, attestations for all seven tied to the release tag, exact source commit, and `.github/workflows/release.yml`, and each archive's two expected binary members against the raw binary assets. Check Mach-O architecture and minimum macOS version on the downloaded binaries, then run the native architecture's Analyze and Status smoke probes. Successful local builds do not prove the public package contains those bytes.\n\nThen run a **script self-update smoke** before publishing notes or announcing: install the previous stable release through the script channel, run `mo update`, and confirm `mo --version` prints the candidate version. Script-installed clients execute the new tag's `install.sh`, so this is the only gate that exercises their real upgrade path; the pre-flight suite cannot cover it before the release exists. Homebrew is a separate downstream gate: verify it only after the core formula has updated, and never treat a script-channel smoke as proof that Homebrew is ready. If the script smoke fails, pull the release (see the pulling-and-re-releasing pitfall) before anyone is told to update.\n\nUse a fresh archive of the previous tag so ignored local `bin/*` builds cannot contaminate the old installation. Place the isolated prefix and config under a physical user-owned directory; `/tmp` or `/var` aliases and writable ancestors can correctly fail installer path checks. Isolate HOME, config/cache paths, and PATH, block host `brew` and `sudo`, and set `MOLE_TEST_NO_AUTH=1`. Verify `config/install_channel` retains `CHANNEL=stable`, `config/bin/{analyze,status}-go` match the verified release assets, changed installed shell sources match the tag, and a second update reports the current version. Keep the user's existing installation unchanged.\n\n## Apply curated release notes\n\nThe curated-notes flow (bilingual format, `gh release edit` instead of `create`, thanks block, and the six-reaction set) is owned by `.claude/skills/release-notes/SKILL.md`. `.agents/skills/release-notes` is a symlink to that canonical directory for Codex discovery, and its Codex-only invocation policy lives in `agents/openai.yaml`; do not replace the symlink with a copied mirror. Follow that skill; do not duplicate its format details here. Version, codename, and emoji go only in the release title; the body h1 is just `Mole`.\n\nAfter applying notes and the standard reactions through that skill, read back the published title, full body, and all six reactions. Report GitHub Stable and Homebrew availability separately; a successfully opened core PR still needs upstream tests and merge.\n\n- Issues whose reporter already verified the fix on nightly get no \"now in stable\" follow-up after the release, and none is proposed.\n\n## Release-notes craft\n\nFormat rules (impact ordering, command existence checks, icon semantics, no em dash, no inline PR refs) live in `.claude/skills/release-notes/SKILL.md` under \"Format rules\". Keep that skill as the single source of truth for notes formatting.\n\n## Release-only pitfalls\n\n- **Tag prefix is case-sensitive**: `release.yml` filters on `'V*'`. A lowercase `v1.38.0` tag will not trigger the workflow.\n- **Old clients fetch `install.sh` from the release tag, not from main**: a self-updating Mole downloads `raw.githubusercontent.com/tw93/mole/V<tag>/install.sh`, and tag content is immutable. An installer/updater bug therefore reaches existing stable users only through a new tag; fixing main changes Nightly but does not repair an already published stable updater.\n- **Never rewrite history an already published tag can reach**: the rule and the pre-rewrite tag audit are in `AGENTS.md` (Release). The incident behind it: on 2026-09-17, four days after V1.54.0 shipped, a `git filter-branch --msg-filter` run stripped a `Co-authored-by: Cursor` trailer from a commit dated 2026-05-06 that had 956 descendants. `--tag-name-filter cat` carried the tags onto the rebuilt commits, the release commit was rebuilt, and `brew upgrade mole` has failed the source checksum for every Intel user since, because Homebrew dropped Intel bottles so they all build from source (#1591). The enumeration was run afterwards: 25 published tags were reachable from that rewrite, so 25 source-tarball checksums moved, not one. Only V1.54.0 broke anything, because homebrew-core pins the checksum of the formula's current version alone, and `install.sh` anchors on the release assets' `SHA256SUMS` rather than on the tag archive it downloads. Any external consumer pinning an older tag's tarball is outside what can be checked from here. Recovery on a live release is a one-line `sha256` PR to `Homebrew/homebrew-core` with the current tarball's hash, and it takes two things that are easy to miss. The PR body must carry Homebrew's own template or a bot closes it within seconds as AI-written, editing it in reopens the same PR and opening a second one is explicitly refused. And a moved checksum is read as a possible supply-chain compromise, so a maintainer will hold the merge until the upstream author confirms in that thread that the change was benign; answer with the two commits' identical tree and the archive's pax header, which they can verify without trusting you, not with a promise. Check for an existing PR before opening one: the community usually files it first. Bottles are unaffected because BrewTestBot built them from the original download.\n- **Pulling and re-releasing a version**: `gh release delete V<old> --cleanup-tag` removes the release and remote tag. Delete the local tag, close the superseded Homebrew core PR with a one-line supersede comment before pushing the replacement tag (`release.yml` refuses to overwrite an existing `mole-<version>` fork branch and reuses, rather than recreates, an open PR for the same head), then bump `VERSION` and `SECURITY_AUDIT.md`, commit `release: V<new>`, tag, and run the normal publish flow. The Homebrew core PR regenerates on the new tag.\n\nWhen release work touches Shell code or tests, read `.claude/skills/bugs/references/shell-and-test-pitfalls.md` for Bash 3.2 arrays, heredoc input, mock bypasses, and CI-runner quirks.\n",".claude/agents/bash32-portability-reviewer.md":"---\nname: bash32-portability-reviewer\ndescription: Reviews Mole shell and Bats diffs against the current macOS Bash 3.2, errexit, timeout, TTY, BSD-tool, and CI-runner pitfalls recorded in .claude/skills/bugs/references/shell-and-test-pitfalls.md. Use after changes under mole, install.sh, bin/**, lib/**, scripts/**, or tests/*.bats.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole shell portability reviewer\n\nRead the current `.claude/skills/bugs/references/shell-and-test-pitfalls.md`\nbefore every review. It is the source of truth and grows when a new incident\nbecomes a stable invariant. Do not rely on a fixed count or a copied historical\nlist in this profile.\n\nYou read diffs, production context, and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base. Restrict findings to `mole`,\n   `install.sh`, `bin/**`, `lib/**`, `scripts/**`, and `tests/*.bats`.\n2. Turn every current pitfall bullet in that reference into a check against the\n   touched code. The list below is a search aid, not a replacement for it:\n   - moved functions using `BASH_SOURCE`, `$0`, or `FUNCNAME`;\n   - `du -s` calls outside `run_with_timeout`;\n   - possibly empty array expansion under `set -u`;\n   - functions called through `if` or `||` that rely on errexit internally;\n   - `[[ ... ]] && cmd` in exit-code-sensitive blocks;\n   - heredoc-driven tests of `read -n1` without redirected stdin;\n   - shell-function mocks hidden by timeout wrappers that exec a real binary;\n   - GNU-only command flags or CI fixtures that assume local macOS directories;\n   - PlistBuddy stdout leaking into assertions;\n   - tests that can pass on empty output or an early return;\n   - macOS-runner-specific errexit behavior around failing command mocks.\n3. Read enough surrounding code to prove the pattern actually fires. A grep hit\n   alone is not a finding.\n4. Check that the regression test reaches the intended branch and that every\n   assertion failure propagates. If local and CI behavior differ, require a\n   failure trace that exposes status, output, and mock calls.\n\n## Output\n\nFor every confirmed problem:\n\n```\nLANDMINE: <file>:<line> - <problem>\n  Pattern: <matched code>\n  Why it fires here: <context>\n  Fix: <one concrete change>\n```\n\nUse `UNVERIFIED: <file>:<line> - <missing evidence>` when context cannot resolve\na real risk. End with `VERDICT: <N> landmines, fix before merge` when findings\nexist, otherwise `VERDICT: no landmines found`. With no findings or unverified\nitems, output only `VERDICT: no landmines found`.\n",".cursor/rules/mole-test-safety.mdc":"---\ndescription: Mole test safety and verification defaults\nalwaysApply: true\n---\n\n# Mole Test Safety\n\n- Default test and verification commands must run with `MOLE_TEST_NO_AUTH=1` or through `scripts/test.sh`, which sets it automatically.\n- Tests must never trigger real `sudo`, Touch ID, password prompts, AppleScript permission dialogs, or system service changes.\n- Any new direct use of `sudo`, `osascript`, or `launchctl` must either be guarded by `MOLE_TEST_MODE` / `MOLE_TEST_NO_AUTH` or be fully mocked in the test path.\n- Prefer focused regression tests first, then run full no-auth verification before release-sensitive changes.\n",".claude/skills/release-notes/SKILL.md":"---\nname: release-notes\ndescription: Publish curated release notes for an existing Mole `V<version>` tag, including bilingual format, `gh release edit`, contributor thanks, and reactions. Use only when explicitly asked to edit or publish Mole release notes. Not for release readiness, tagging, or code review.\ndisable-model-invocation: true\nmetadata:\n  internal: true\n---\n\n# Mole release notes\n\nThis skill drives the curated-notes step that runs **after** `release.yml` has finished. The workflow creates the GitHub Release with assets but with `generate_release_notes: false`, so notes must be added in a follow-up `gh release edit` (never `gh release create`, the release already exists, and `create` will conflict).\n\n## Inputs to gather\n\nBefore drafting, confirm:\n\n1. **Version**. Capital `V`, e.g. `V1.38.0`. Lowercase `v` does not trigger the workflow and may indicate a botched tag.\n2. **CodeName + emoji**. Ask the user. The title format is `V<version> <CodeName> <emoji>`.\n3. **Release commit range**. `git log <previous-tag>..V<version> --oneline` gives the raw material.\n4. **User-visible behavior changes**. Scan the full commit message bodies (not just subjects) for narrowed detection, removed features, or controlled regressions. These belong in notes even when they are not bug-fix-shaped, because users will encounter the changed boundary in production.\n5. **Issue reporters and PR contributors in this cycle**. Derive them from what the release actually contains, never from a date window of closed issues: `git log --format='%an' <prev>..<tag>` for contributors, and the issue numbers the range's own commit messages cite for reporters, checking each is `state_reason: completed` and was fixed here rather than merely referenced as background. A closing window silently pulls in `not_planned` issues and everything the PREVIOUS release fixed, which is how V1.55.0 nearly shipped 30 names where 8 were real. Keep it short, for example `Issue reporters and PR contributors this cycle: @a · @b.` Exclude `tw93`, `youxi798` and bots.\n6. **Verify release exists**. `gh release view V<version> --repo tw93/Mole --json id,name` should return non-empty. If it doesn't, the workflow hasn't finished, wait, don't `gh release create`.\n\n## Pre-flight (published-tag evidence)\n\nUse the target tag's exact commit and the completed checks and public-asset evidence from [release-flow](../release-flow/SKILL.md). Reuse that evidence when it still matches the immutable tag; rerun only a missing or failed gate. A build of a newer working tree does not verify the release being described. The full build, test, asset, and script-update gates stay owned by `release-flow` rather than a second checklist here.\n\nIf evidence is missing, complete the applicable release-flow gate before publishing notes. Keep draft work read-only; an existing release object alone is not proof its assets or upgrade path passed.\n\n## Format\n\nStrictly follow the current compact release shape. Read the previous published stable release before the target tag as the live format reference. Exclude the candidate release: the workflow may already have made it the latest release with an empty body. Resolve the previous tag first, then read it with `gh release view <previous-tag> --repo tw93/Mole --json tagName,body`.\n\nStructure:\n\n```\n<div align=\"center\">\n  <img src=\"https://cdn.tw93.fun/pic/cole.png\" alt=\"Mole Logo\" width=\"120\" height=\"120\" style=\"border-radius:50%\" />\n  <h1 style=\"margin: 12px 0 6px;\">Mole</h1>\n  <p><em>Deep clean and optimize your Mac.</em></p>\n</div>\n\n### Changelog\n\n1. **<English headline>**: <one-sentence English elaboration>.\n2. ...\n\n### 更新日志\n\n1. **<中文 headline>**：<一句中文说明>。\n2. ...\n\n### Thanks\n\nIssue reporters and PR contributors this cycle: @handle1 · @handle2.\n\n### Mole Mac App\n\nPrefer a GUI? [Mole Mac App](https://mole.fit/) brings cleaning, app management, maintenance, disk analysis, and live system status into one native app, with review before deletion and a customizable menu bar HUD. It is $19 once, with lifetime updates and a 14-day refund. [Download and try it](https://mole.fit/download). The CLI stays free and open source.\n```\n\nNo `---` separators between sections, and no trailing repository link; the published pages end on the Mole Mac App line.\n\n### Format rules (all are documented bugs that have shipped before)\n\n- **Body h1 is just `Mole`**. Version, codename, and emoji live only in the `--title` argument (`V<version> <CodeName> <emoji>`); repeating them in the body header is redundant and has been explicitly rejected before.\n- **No em dash anywhere**. Use commas, periods, colons, semicolons, or parentheses.\n- **No sponsor list by default**. The current public release style thanks issue reporters and PR contributors for this cycle only.\n- **No emoji except the version emoji in the release title**. Body section headers stay plain, including `### Thanks` (the old `Thanks 💖` header is gone from the published pages).\n- **No inline PR refs, no inline `@handle` thanks**. PRs and people belong in the dedicated Thanks block only.\n- **English block first, 中文 block second**. Same numbered order in both blocks. Same number of items.\n- **Order items by user-perceived impact, not commit chronology**. Headline change first; internal safety hardening, performance, and bug fixes follow.\n- **Do not describe overview icons that no longer exist**. Analyze overview rows are text-only because emoji width and baselines vary across terminals. If icons return later, they must not imply that user data such as iOS Backups, Xcode Archives, or Old Downloads is safe to delete.\n- **Verify every command mentioned in the notes actually exists in HEAD**. The deleted `mo check` / `mo doctor` commands nearly shipped in notes as a \"feature\" after they were removed.\n- **An incident or troubleshooting note is one sentence of symptom plus one command**. No cause taxonomy, no command per branch; the user needs the one line that gets them unstuck. Match the previous release's language treatment for that note: if the last release carried it in one language, do not add a second.\n- **Keep the Mole Mac App cross-link to one restrained, fact-backed paragraph**. Validate the product scope, price, updates, refund window, and download URL against the current homepage before publishing.\n\n## Publish\n\nOnce the user approves the draft:\n\n```bash\ngh release edit V<version> --repo tw93/Mole \\\n  --title \"V<version> <CodeName> <emoji>\" \\\n  --notes-file <path-to-draft>\n```\n\n**Never** `gh release create`, it conflicts with the release the workflow already made.\n\nThen add the six reactions with this skill's helper (path is relative to this SKILL.md, not the repo-root `scripts/`): `bash \"$(dirname <this SKILL.md>)/scripts/post-reactions.sh\" V<version>`.\n\n## After publish\n\n- `gh release view V<version> --repo tw93/Mole --web` (open in browser) so the user can eyeball it.\n- Remind the user: the Homebrew Core PR is workflow-driven and should already be in flight; do not re-run it manually unless the workflow log shows a failure.\n\n## When NOT to act\n\nThis skill is user-invocable only. It must not run unprompted:\n\n- If the user mentions release notes in passing, draft only; do not call `gh release edit`.\n- If `gh release view` shows the release does not exist yet, wait for the workflow; do not create a competing release manually.\n- If the user has not given an explicit \"publish\" / \"提交\" signal, stop after the draft.\n\n## Helper script\n\n`scripts/post-reactions.sh <tag>` lives next to this SKILL.md and adds the six reactions (`+1`, `laugh`, `hooray`, `heart`, `rocket`, `eyes`) to the release.\n",".claude/skills/bugs/SKILL.md":"---\nname: bugs\ndescription: \"Mole incident catalog for cleanup safety, bounded probes, cancellation, dry-run parity, and actionable gates. Use for a Mole safety-sensitive diff; not for docs, release notes, or generic review.\"\nmetadata:\n  internal: true\n---\n\n# Mole bug patterns\n\nGeneric review belongs to Waza `check`; root-cause investigation of a live failure belongs to `hunt`.\n\n## Route before loading details\n\nChoose only the reference families touched by the evidence. A whole-project audit should classify surfaces first instead of loading every incident narrative.\n\n| # | Recurring shape | First probe | Read |\n|---|---|---|---|\n| 1 | Deletion candidate built from a weak name signal | Inspect name, bundle-id, fallback globs, and failed-owner shortcuts | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 2 | Existence or idleness decided by one probe | Enumerate every legitimate location and unknown outcome | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 3 | Guard present on only one branch | Diff dry-run, real, direct, fallback, and final-sink paths | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 4 | Unbounded external command | Count producer, consumer, inner-loop, and action bounds | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 5 | Bash 3.2, errexit, or pipefail trap | Check empty arrays, `fn || handler`, and optional actions | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 6 | TTY, stdin, or process-group theft | Inspect background workers and commands that may prompt | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 7 | System output parsed as a stable API | Force locale, validate shape, and join on identifiers not headings | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 8 | Persisted derived data outlives its algorithm | Trace schema, TTL, evidence fingerprint, and mutations | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 9 | Two paths compute one number differently | Find every producer and choose one definition | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 10 | Slow work looks frozen | Find operations over roughly one second outside feedback | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 11 | Regression test cannot fail | Prove positive control and pre-fix red state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 12 | Gate cannot explain why it refused | Map each reason to one cause and a next action that fails on the broken state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 13 | Mutation target is also accepted as a discovery container | Compare the recursive scan-root namespace with every purge target basename | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 14 | Owner metadata is treated as a complete, atomic inventory | Identify who writes it, whether absence is authoritative, and what locks mutation | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 15 | Cancellation stops one helper but later work continues | Classify each 124 as skip, section budget, or sticky cancel before tracing callers | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 16 | Async or cached data has no generation or freshness contract | Bind results to a request epoch and keep each sample's time, stale, and completeness fields together | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 17 | Publication gate trusts ambiguous or pre-existing state | Require exact source/tag equality, one generated target, and an expected-absence ref lease | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 18 | A sandbox well-known path is treated as app-private leftovers | Resolve Data/Downloads, Desktop, Pictures, Music, and Movies physically against $HOME | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n\nAGENTS.md and the path-scoped `.claude/rules/` files keep each rule stated fully enough to obey plus its test anchor; the incident story lives in these references or in `release-flow`, and the rule bullet points to the section that holds it. Moving a story here is a merge, never the deletion of a rule that still constrains behavior.\n\n## Trace the complete mutation lifecycle\n\nFor cleanup, purge, optimize, analyze deletion, or uninstall work, review the complete chain rather than the reported branch:\n\n```text\ndiscover or plan\n  -> cheap irreversible filters\n  -> owner and open-handle probes\n  -> size or metadata work\n  -> final owner re-probe\n  -> parent and target identity rebind\n  -> deletion or Trash sink\n  -> accounting, cancellation, and user output\n```\n\nAt every transition, answer:\n\n- Does live or unknown state fail closed?\n- Are timeouts classified by probe, sizing, removal, or section scope, with unknown evidence refusing deletion and cancellation stopping later mutation?\n- Are probe and sink bound to the same physical parent and target?\n- Do dry-run and real mode start from the same eligible plan without reusing stale authorization?\n- Are cheap missing, protected, whitelisted, and compiled-model filters ahead of recursive probes?\n- Does one cumulative deadline cover the dynamic scan scope, with checkpoints in nested loops?\n- Do refused, filtered, timed-out, or failed items stay out of cleaned counts and reclaimed bytes?\n- Can large candidates avoid per-item size work without making the reported total false?\n\nDo not trade final-sink rebinding or fail-closed owner checks for speed. Optimize absent targets, duplicated discovery probes, report-only work, and wrong-scope scans first.\n\n## Working contract\n\n- Before proposing a remedy, apply the Product Decision Filter in `AGENTS.md`: a new flag, environment variable, or visible retry message is a product change, even when the underlying fix is a safety improvement. Use the existing state/accounting reference when deciding what belongs in the normal summary.\n- A sandbox well-known directory is user data until physical resolution proves otherwise. `Data/Downloads` is usually `~/Downloads` under another name.\n- A refusal next step that succeeds on the broken state is not a next step. Do not relax the gate to make the message nicer.\n- Sweep siblings by call-site shape, not filename or helper name. Report `checked N / defective M / not applicable K`.\n- A recurring fix ships with a regression or source invariant that fails against the pre-fix code.\n- Treat tests as production consumers only after proving the production helper ran. Negative assertions require a positive trace.\n- A cancellation regression makes the next candidate otherwise eligible, then proves its probe and sink never run. Making every candidate fail for the same reason is a false sticky-cancellation test.\n- Absence-sensitive tests use an isolated `HOME` or fixture root; a shared `setup_file` home is not isolation.\n- Reproduce CI through `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh` when possible. If invoking Bats directly with jobs, preserve `--no-parallelize-within-files`; files share state and raw `bats --jobs 6 file.bats` changes semantics.\n- Treat specialist or AI reports as leads. Read the implementation, callers, fallback branches, and final sink yourself.\n\n## Verification bar\n\nUse the hotspot commands in the matching `.claude/rules/` file (routed from `AGENTS.md`); do not guess a narrower verifier. A typical Shell safety change finishes with:\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 bats tests/<area>.bats\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\ngo test ./...\nMOLE_TEST_NO_AUTH=1 MOLE_DRY_RUN=1 ./mole clean --dry-run\n```\n\nNever infer a production defect from a function name, comment, string, fixture, or `_test.go` match. Confirm the live call path and verify red-green before reporting the class fixed.\n","AGENTS.md":"# Mole Agent Guide\n\nThis 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.\n\nLayout: 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.\n\n| File | Scope |\n|---|---|\n| `.claude/rules/shell.md` | Shell portability: BSD-only tools never through PATH, duplication audit, dead-symbol checks |\n| `.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 |\n| `.claude/rules/clean.md` | `mo clean` orchestration and sections, user/app/system caches, browsers, firmware, logs, orphan services, project caches, Homebrew, whitelist |\n| `.claude/rules/clean-dev.md` | `lib/clean/dev.sh`: developer and toolchain caches, recovery contracts, Nix, simctl, AI tool caches |\n| `.claude/rules/purge-installer.md` | `mo purge` discovery and protection, worktrees, `mo installer` |\n| `.claude/rules/uninstall.md` | `mo uninstall` inventory, display names, app protection, leftovers, sibling guard, batch execution, Homebrew casks |\n| `.claude/rules/optimize.md` | `mo optimize` tasks, preference repair, Spotlight, LaunchServices |\n| `.claude/rules/slim.md` | `mo slim`: Intel slice removal scope, seal-aware thinning, verified swap |\n| `.claude/rules/go-tuis.md` | `cmd/analyze` and `cmd/status` Go TUIs |\n| `.claude/rules/install-update.md` | `mole` router pins, `install.sh`, `mo update`, self-heal, install lock, `mo remove` |\n| `.claude/rules/tests.md` | Bats and Go test validity, test lint, CI jobs |\n| `.claude/rules/github.md` | Issue, PR and Discussion replies and closeout, Mole Mac invitation, announcements, issue version preflight |\n| `.claude/rules/readme.md` | README copies in every language |\n\nThe 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.\n\n## Product synchronization\n\nUse 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.\n\nEvery 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.\n\nBefore 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.\n\nCover 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.\n\n## Project\n\nMole 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.\n\n## Product Direction\n\nMole 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.\n\n### What Mole Should Do\n\n- Make cleanup and uninstall actions boring, reviewable, logged, protected by path/app rules, and dry-run capable.\n- Prefer reversible user-facing removals through Trash where the command surface expects recoverability.\n- Keep `clean`, `uninstall`, `purge`, and `installer` focused on reclaimable files, app leftovers, rebuildable caches, installer artifacts, and exact known cleanup targets.\n- 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.\n- 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.\n- Keep `optimize` focused on explicit, bounded maintenance tasks that can be explained before execution and tested without real authorization prompts.\n- Keep command UX dense and terminal-native: short labels, stable alignment, predictable shortcuts, one-screen summaries, then optional drill-down.\n- 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`.\n- Keep Mole Mac references as a cross-link or support path. The CLI and Mac app can share product values without requiring feature parity.\n\n### What Mole Should Not Do\n\n- Do not add broad system modification, privacy reset, package management, app bundle patching, or device-management features just because they are technically possible.\n- 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.\n- Do not add background agents, persistent monitoring, notifications, schedulers, menu bar behavior, or GUI-like state unless explicitly requested and justified as CLI scope.\n- Do not broaden leftover matching from exact app or bundle evidence into vendor-wide, TeamID-prefix, generic-name, or fallback wildcard deletion.\n- 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.\n- 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.\n- 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.\n\n### Product Decision Filter\n\nBefore accepting a new feature, answer these questions in the PR, issue, or review notes when the fit is not obvious:\n\n1. Does it clearly belong to clean, uninstall, analyze, optimize, status, purge, history, installer, slim, update, completion, touchid, or remove?\n2. Is it safe by default, previewable where destructive, testable without real auth, and explainable in one terminal screen?\n3. Can the user verify what will change before Mole changes it?\n4. Is the target data locally rebuildable, disposable, or backed by exact app/bundle evidence?\n5. Would this be better as Mole Mac UI, documentation, a warning, or an explicit \"not supported\" answer?\n\nIf the answer is no or unclear, decline the feature, narrow it, or park it until the product value beats the added surface area.\n\nA 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.\n\n## Repository Map\n\n- `.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.\n- `.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.\n- `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.\n- `lib/core/` - shared shell safety, UI, file operations, operation logs, app protection logic, and centralized timeout constants (`timeouts.sh`).\n- `lib/core/app_protection_data.sh` - readonly bundle ID and pattern arrays consumed by `app_protection.sh`. Data only, no logic.\n- `cmd/analyze/` and `cmd/status/` - Go TUIs (see `.claude/rules/go-tuis.md`).\n- `tests/` - Bats suites, PTY drivers and fixtures (see `.claude/rules/tests.md`).\n- `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`.\n- `docs/SECURITY_DESIGN.md` - design doc for the path validation / app protection / # SAFE annotation contract.\n- `SECURITY_AUDIT.md` - security review notes.\n\n## Commands\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\nMOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats\nMOLE_DRY_RUN=1 ./mole clean\nMOLE_TEST_NO_AUTH=1 ./mole clean --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole purge --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole installer --dry-run\nfind bin lib -name '*.sh' -print0 | xargs -0 -n1 bash -n\nmake build\ngo test ./...\n```\n\nPublic 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.\n\n## Critical Safety Rules\n\n- 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.\n- 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.\n- Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.\n- 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.\n- 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.\n- 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`.\n- 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.\n- 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.\n- 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.\n- 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`.\n- 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`.\n- 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`.\n- 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`.\n- 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.\n- **`_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.\n- **`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`.\n- **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`.\n- **`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`.\n- **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 `&amp;` last, since a key holding the literal text `&lt;` 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.\n- 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`.\n- Do not change ESC timeout behavior in `lib/core/ui.sh` unless explicitly requested.\n- 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`.\n- 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`.\n- **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.\n\n## Working Rules\n\n- 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.\n- 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.\n- `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.\n- Keep shell code formatted with `./scripts/check.sh --format`.\n- Prefer targeted Bats tests during development; run the full suite before committing.\n- Do not add AI attribution trailers to commits.\n\n## Verification\n\n- 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`).\n- `lib/core/sudo.sh` changes: run `MOLE_TEST_NO_AUTH=1 bats tests/manage_sudo.bats`.\n- Go changes: run `go test ./...`.\n- Area changes: run the test command listed for that hotspot in its `.claude/rules/` file; do not guess a narrower verifier.\n- Documentation-only changes: check links and commands.\n- **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.\n- **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.\n- **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.\n- 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.\n\n`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.\n\nIf `golangci-lint` reports issues from deleted temporary worktrees or non-existent paths, clear its local cache and rerun the linter:\n\n```bash\ngolangci-lint cache clean\ngolangci-lint run ./cmd/...\n```\n\n## Release\n\n**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.\n\nThe 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`.\n\nTag-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.\n",".claude/agents/safety-reviewer.md":"---\nname: safety-reviewer\ndescription: Audits Mole changes for destructive-action regressions across deletion, app protection, privilege boundaries, dry-run behavior, operation logging, package-manager cleanup, and exact leftover matching. Use before merging changes under lib/clean/**, lib/uninstall/**, lib/manage/**, bin/clean.sh, bin/purge.sh, bin/uninstall.sh, bin/installer.sh, lib/core/file_ops.sh, or lib/core/app_protection*.sh.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole destructive-action safety reviewer\n\nRead `AGENTS.md` sections \"Critical Safety Rules\", \"Working Rules\", and\n\"Verification\", every `.claude/rules/*.md` file whose `paths:` match the diff\n(its routing table names them), plus `docs/SECURITY_DESIGN.md`, before judging\nthe diff. Those files are the current safety contract. This profile defines the\nreview method and output shape only; it must not become a copied policy list.\n\nYou read code and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base and read the issue or PR scope.\n   A request for one leftover path is not permission to add a broader matcher.\n2. Mark every changed destructive sink and every new path source. Pay special\n   attention to `find_app_files`, `mole_delete`, `remove_file_list`, container\n   traversal, Group Containers, bundle-prefix matchers, and recursive `find`\n   branches that eventually delete.\n3. Audit each branch independently, including fallbacks. For every candidate,\n   prove exact app or bundle evidence, protected-path coverage, preview or\n   confirmation, dry-run behavior, operation logging, and the final deletion\n   helper. A safe primary branch does not make a broad fallback safe.\n4. Treat raw removal outside `lib/core/file_ops.sh` as P0 unless the call site\n   has a narrow `# SAFE:` exception for an already verified exact path and a\n   regression test that proves why the shared funnel cannot be used. Never\n   generalize one exception into a second deletion API.\n5. For new `sudo`, `osascript`, `launchctl`, package-manager, or service teardown\n   calls, verify test/auth guards, non-interactive test behavior, exact preview,\n   and failure propagation. Typed password input must not be mistaken for skip.\n6. For uninstall teardown, prove every route passes the shared-bundle-id sibling\n   guard, including volume copies, inverse names, and shared identities.\n7. Read enough surrounding production code to follow helper calls to their final\n   sink. Then map the change to the exact hotspot commands in the matching\n   `.claude/rules/` file and \"Verification\"; missing safety coverage is a finding.\n\n## Severity\n\n- **P0**: a path can escape its intended target, protection/confirmation/dry-run\n  is bypassed, a destructive failure can be reported as success, or a privileged\n  action can execute during ordinary verification.\n- **P1**: matching is broader than exact evidence, a teardown route bypasses a\n  shared guard, active user/developer state can be removed, package-manager\n  candidates are not previewed, or a safety regression lacks a direct test.\n- **P2**: the behavior is bounded but the documented targeted verification was\n  not run or the failure output is not actionable.\n\nDo not flag style, speculative refactors, or test-only panic/expect patterns.\nIf a helper or guard cannot be traced, report it as `UNVERIFIED` rather than\nassuming it is safe.\n\n## Output\n\nOrder findings by severity:\n\n```\nP0: <file>:<line> - <problem>\n  Why unsafe: <broken invariant>\n  Fix: <one concrete change>\n```\n\nUse the same shape for P1 and P2. End with `VERDICT: changes required` when any\nP0 or P1 exists, otherwise `VERDICT: safe to merge`. With no findings or\nunverified items, output only `VERDICT: safe to merge`.\n",".claude/skills/release-notes/scripts/post-reactions.sh":"#!/bin/bash\n# Add the standard six reactions (+1, laugh, hooray, heart, rocket, eyes) to a\n# tw93/Mole release. Usage: post-reactions.sh V<version>\n\nset -euo pipefail\n\nTAG=\"${1:-}\"\nif [[ -z \"$TAG\" ]]; then\n    echo \"Usage: $0 V<version>\" >&2\n    exit 1\nfi\n\nif [[ \"$TAG\" != V* ]]; then\n    echo \"Tag must start with capital V (release.yml ignores lowercase v): $TAG\" >&2\n    exit 1\nfi\n\nif ! command -v gh > /dev/null 2>&1; then\n    echo \"gh CLI is required\" >&2\n    exit 1\nfi\n\nRELEASE_ID=$(gh api \"repos/tw93/Mole/releases/tags/$TAG\" --jq '.id')\nif [[ -z \"$RELEASE_ID\" ]]; then\n    echo \"Release not found for tag: $TAG\" >&2\n    exit 1\nfi\n\nfor r in +1 laugh hooray heart rocket eyes; do\n    gh api \"repos/tw93/Mole/releases/$RELEASE_ID/reactions\" \\\n        -X POST -f content=\"$r\" --silent\ndone\n\necho \"Posted 6 reactions to $TAG (release id $RELEASE_ID)\"\n",".claude/skills/release-notes/agents/openai.yaml":"policy:\n  allow_implicit_invocation: false\n",".claude/skills/bugs/references/shell-and-test-pitfalls.md":"# Bounds, Shell, TTY, and parsing\n\nRead this reference when changing Shell code, Bats tests, update/install flows, timeout wrappers, TTY handling, plist fixtures, or macOS-version-specific CI behavior. The defect classes and repo-wide probes stay in the parent `bugs` skill.\n\n## 4. An external command or its consumer is unbounded\n\n`du`, `mdfind`, `find`, `xcrun simctl`, `system_profiler`, `ioreg`, and package tools can stall on a healthy but slow machine. Every production `du -s` route stays behind `run_with_timeout` with `MOLE_TIMEOUT_DISK_VERIFY_SEC`; `tests/core_timeout.bats` pins the class across `lib/` and `bin/`.\n\nCheck more than the obvious command:\n\n- Put checkpoints in every nested loop, not just each outer root (`edb214c0`).\n- Tune against the slowest healthy case. CoreSimulatorService needed a warm-up retry after a two-second bound falsely reported it unavailable (`35d856f1`).\n- Materialize a bounded producer completely and discard its output on nonzero status. Process substitution plus `|| true` must not feed a partial `find` prefix into deletion.\n- Keep probe and action pattern, type, age, and depth identical.\n- Time producer and consumer separately before raising a timeout. A 2.3-second `lsregister` dump followed by one command substitution per input line still becomes minutes.\n- Bound installed-binary `--version` and `--help` verification. Broken executables are the ones most likely to hang.\n- Keep install and update single-flight per target directory so one process cannot verify another generation.\n- The self-update bootstrap on a user's machine (temp file, registry, exec) is frozen at whatever version installed it, so a broken installed version cannot repair itself. That is why `_update_self_heal_reinstall` streams `install.sh` from `main` into bash with no local temp files (#1297). V1.47.1 shipped the false-success shape the bounded version check now prevents: update success read from installer output instead of from the installed binary.\n- The single-flight mutex prefers absolute `/usr/bin/lockf` because the kernel drops that lock even if the holder is killed. `lockf` only ships with newer macOS, and requiring it made install and update exit before writing a file on every older release (#1348); the `mkdir` fallback exists for those releases. The lock wrapper is not a shell array because the empty array is the fallback path, and an empty array under `set -u` is an unbound-variable error on Bash 3.2 (section 5).\n\n```bash\nfor command_name in 'du -s' mdfind xcrun system_profiler ioreg brew; do\n    printf '%-16s total=%-4s wrapped=%s\\n' \"$command_name\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | wc -l | tr -d ' ')\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | command grep -c run_with_timeout)\"\ndone\n```\n\n## 5. Bash 3.2, errexit, and pipefail change meaning\n\nmacOS ships Bash 3.2 and Mole runs with nounset.\n\n- Guard `\"${arr[@]}\"` with `[[ ${#arr[@]} -gt 0 ]]`; an empty array under `set -u` can abort a scan and orphan its spinner (`893b4e6f`, `2c06cb91`).\n- `fn || handler` disables errexit inside `fn` for the whole function. Safety-critical steps use explicit `if ! command; then return 1; fi` (`a33a0b51`). Installers must also verify the installed binary's reported version before claiming success; `tests/install_checksum.bats` covers the exact caller shape.\n- Do not rely on a caller's temporary `set +e` window for graceful degradation. Capture the status where the command runs.\n- Optional `[[ -n \"$value\" ]] && action` returns 1 when absent. Use `if/fi` inside status-sensitive blocks.\n- Fixing a branch that Bash 3.2 silently skipped is a behavior change for every caller. `read -t 0.01` fails on 3.2, so `drain_pending_input` never drained there; `ed02a6aa` made it flush the terminal queue after every menu key, and a pasted search term kept only its first character. Before enabling such a branch, list its callers and check each for text entry, call the system tool by absolute path (a version-manager `perl` shim can fail or add startup latency per key), and never fall back to a one-second integer read on a terminal.\n\n```bash\ncommand grep -rn '\\$\\{[a-z_]*\\[@\\]\\}' lib/ bin/\n```\n\n## 6. Background work steals TTY, stdin, or process groups\n\nThe Perl timeout fallback can hand the controlling terminal to its child. A background metadata worker then stopped the foreground uninstall prompt with SIGTTIN (`c93afca3`). BSD `mv` and `cp` can also prompt on stderr and read stdin when a destination is unwritable (`63030e3a`).\n\nEvery background worker that calls `run_with_timeout` closes stdin with `< /dev/null`. Commands that can prompt also use their noninteractive or force option. Menu and scan traps save and restore the caller's traps; `lib/ui/menu_paginated.sh` is the reference.\n\n## 7. System command output is treated as an API\n\nmacOS command output is localized, drifts between releases, and can print errors where data is expected.\n\n- Force `LC_ALL=C` for parsed metric subprocesses (`51b352a2`, `fa05b8cc`, `4e83743b`).\n- Validate field shape before trusting it: absolute path, numeric value, expected key, or exact enum.\n- Keep `DTSDKBuild` build identifiers separate from `DTPlatformVersion` versions (`f0896d03`).\n- Reject PlistBuddy's missing-file prose as data.\n- Use stock macOS semantics when checking flags. BSD `grep -Z` means `--decompress`; a developer alias may hide that.\n- Prefer exit codes, plist keys, and machine-readable output over prose matching.\n- Join records on the machine identifier, never on a heading or display string. `simctl runtime list` titles each image with the image version (`iOS 26.4.1`) while `simctl list devices` groups under the runtime short name (`iOS 26.4`). A name join calls every point release an orphan and offers `simctl runtime delete` for a runtime its simulators still bind (`#1505`). `mdls -name kMDItemDisplayName` returns the on-disk file name, not Finder's localized name, so it always differed from `app_name` and always won the old selection, leaving the `CFBundleDisplayName` and `CFBundleName` branches unreachable and shipping folder names like `VideoFusion-macOS` (`#1520`).\n- `plutil -p` is documented by `man plutil` as unstable and not designed for machine parsing. macOS 15 prints a JSON boolean true as `1` while macOS 26 and 27 print `true`, so a filter pinned to one spelling silently cleaned nothing on the other: `#1512` was fixed and then regressed for eight days with CI green. Where a boolean prints as `1` nothing can tell it from the integer 1. The rendering is also depth-blind, so `{\"outer\":{\"inner\":true}}` offered a directory named `inner` for deletion, a wrong deletion rather than a missed one. That is why `tests/clean_app_caches.bats` runs on the macos-15/macos-26 compatibility job.\n\nUse `command grep` when flag behavior matters, because the interactive environment may alias it.\n\n## 15. Cancellation is local unless orchestration makes it sticky\n\nClassify a timeout at its source before propagating it. Signal-derived cancellation and safety-guard timeouts that the caller treats as cancellation must remain sticky across the remaining command. Status `124` alone does not define the scope. Read the probe's product contract first:\n\n| Contract | Typical `124` | Typical `>=128` |\n|---|---|---|\n| Safety guard, owner unknown, or cancellation the caller already established | Sticky stop; later mutation and later sections must not start | Sticky stop |\n| Per-item sizing or removal of an otherwise eligible candidate | Skip or fail that item; later items may continue (`#1374`, `#1384`, `#1576`) | Sticky stop |\n| Review-only or advisory listing that never deletes | Skip that advice; later cleanup sections continue (`#1571`) | Sticky stop |\n| Cooperative section budget | Stop the rest of that section, report partial, continue later sections (`#1513`) | Sticky stop |\n\n- In `bin/clean.sh`, a final delete guard returning `124` cancels later work, while an individual `safe_remove` timeout is a reported failed removal and later items may continue. Timed-out sizing contributes an unknown/partial total, not false reclaimed bytes. Large-files Mail / Downloads / Updates rows follow the review-only skip: a size timeout omits that row, and a signal still cancels (`#1576`, `#1344`).\n- The orphaned-runtime review is advisory. A cold `simctl` that returns `124` skips the review and must not cancel later `mo clean` sections. Skip the review entirely when the unavailable-simulator listing already timed out after its warm-up retry (`#1571`, `04e658d2`).\n- Orphan leftover probe and sizing timeouts (`mdfind` in `is_bundle_orphaned` / `is_claude_vm_bundle_orphaned` / `_container_stub_app_exists`, plus `get_path_size_kb` and candidate snapshots) fail closed for that item and later leftovers plus later `mo clean` sections continue. A `safe_clean_guarded` 124 at the sink stays sticky (`#1584`). Do not cache a timed-out Spotlight miss as \"not installed\".\n- Cloud & Office uses a cooperative section deadline: stop the remaining items in that section, preserve parent counters, report partial completion, and continue later sections. Do not restore its removed outer timeout worker or the file-backed deferred-family replay that existed only for that worker.\n- Purge discovery discards incomplete root scans and marks the run incomplete. An authored-content probe returning `2` keeps and visibly reports that candidate; deletion-phase activity or removal timeouts cancel the run. Unknown evidence never permits deletion.\n\nThe inverse defect is as common as a missed sticky cancel. Treating a review-only `124` as command-level cancellation skips every later section. Treating a safety-guard `124` as a local skip deletes with unknown evidence.\n\nOnce the caller establishes cancellation, carry that decision through every boundary:\n\n- A best-effort loop must check a pending cancellation before probing or registering the next candidate.\n- A helper that reports ordinary misses as success must return a pending cancellation before starting the next family.\n- Command substitution, subshells, and background workers do not mutate the parent's shell variables. Return the status, or use an explicit parent-readable channel, then record it again in the parent.\n- A parallel coordinator stops and reaps peer workers, preserves cancellation over ordinary failures, and prevents the next rendered section from starting.\n- Dry-run uses the same cancellation contract as real cleanup. A preview ledger is still downstream work and must not continue after safety evidence becomes unavailable.\n\nThe regression shape matters. Make the first candidate's safety guard return 124 or 130 and make the second candidate succeed if reached. Assert the exact top-level status plus the absence of a positive trace from the second probe, preview registration, sink, and later section. If both candidates independently time out, the test cannot prove cancellation was sticky. Separately preserve the removal-timeout and cooperative-section-budget continuation cases in `tests/clean_core.bats`.\n\nDo not hide a cancelled safety probe behind `|| true`, a warning plus `return 0`, or a worker-local exported variable. Those shapes turn a global stop into a local skip.\n\nName the question instead of spelling the numbers: `mole_rc_timeout` and `mole_rc_timeout_or_signal` in `lib/core/timeout.sh` are the only places that compare a status against 124, and `scripts/audit_timeout_status.py` rejects a raw comparison anywhere else in `check.sh`. A per-item size whose number only feeds totals goes through `mole_item_size_continues` in `lib/core/file_ops.sh`: a timeout or failure keeps the item with an unknown size and marks the freed total partial, a signal records the cancellation and stops. Choosing between those helpers is still the caller's contract decision from the table above; the helper only makes the choice visible in review.\n\nA third predicate, `mole_rc_signal`, is true only for a status above 127 that `kill -l` names. A mutating owner command can exit with an errno-derived code such as npm's 243, which is an ordinary failure, so the owner-command wrappers (`clean_tool_cache`, `bun pm cache rm`, `simctl delete unavailable`, `brew cleanup`, the tart prune and `go clean`) cancel only on it and skip that one command on 124 (`clean_tool_cache treats non-signal high owner statuses as failures (#1695)`, `clean_tool_cache keeps valid signal statuses sticky (#1695)`, `clean_dev_go treats an errno-derived go clean exit as a failure, not an interrupt`). A timed-out `go env` lookup skips only the Go caches because nothing was attempted yet, while the `gh` probe and clear, the automation-browser `ps` listing and the clang cache resolver stay sticky (`clean_dev_cloud stops on GitHub CLI probe or clear cancellation`).\n\n## Focused pitfalls\n\n- **A PTY read loop must end on an empty read, an `OSError`, or a deadline.** After the child exits, a macOS pty master can stay readable and return `b''` forever, so a `while select(...)` drain with no exit spins until the job times out; Linux raises `EIO` instead. `tests/sudo_adoption_pty.py` hung this way on #1747 (fixed in the squash `eb531ba2`); the other `tests/*_pty.py` loops are deadline-bounded.\n\n- **Launcher signal stories behind the `lib/manage/update.sh` rules in `.claude/rules/install-update.md`**: a tty7 pane can inherit the daemon's blocked TERM mask, so a child launched without unblocking it left uninstall waiting forever for its spinner and made Ctrl-C cleanup hang. Bash prints a signalled child's job-status line (`Killed: 9 ... perl ...`) from whichever command finishes first after the child dies, so a runner that stalls the router for a few milliseconds leaked it at random until the launch, readiness wait and `wait` shared one `2> /dev/null` group. And Bash 3.2 can discard an INT received inside a USR1 trap entered from the `wait` builtin, which is why the readiness notification settles before `wait` starts.\n- **A `find` primary newer than the oldest supported macOS needs a probe, and dataless folders must not be listed**: listing a dataless CloudStorage folder makes File Provider fetch it over the network (`77050616`), an older `find` rejects `-flags +dataless` and fails the whole walk (`8dccb160`), and the capability probe itself must pass signals through (`df2f5e06`). Probe once per process: unknown means walk without the primary, a signal cancels, and never descend into a dataless folder.\n- **`BASH_SOURCE` / `$0` change meaning when a function moves files**: they name the file the code lives in, so copy-paste extraction is not behavior-preserving. `mole` captures `MOLE_ENTRY_SCRIPT=\"${BASH_SOURCE[0]}\"` before sourcing anything, and update code reads that stable entrypoint. Before extracting a function, grep it for `BASH_SOURCE`, `$0`, and `FUNCNAME`. Regression coverage lives in `tests/update.bats`.\n- **Bats heredocs share stdin with `read -n1`**: an inner `read -r -s -n1` can consume the next byte of the heredoc source. Redirect the function under test from `/dev/null`.\n- **macOS `script(1)` rejects socket-backed stdin**: PTY test helpers must redirect the wrapper's stdin from `/dev/null` or `script` can fail before starting the child. Capability probes use `/usr/bin/true`, not the absent `/bin/true`; inspect the actual failure before classifying it as unavailable TTY support, or live terminal tests silently skip.\n- **`run_with_timeout` execs the binary and bypasses shell-function mocks**: tests must use a PATH stub directory for commands such as `osascript`.\n- **CI runners may lack `/Library/PrivilegedHelperTools`**: orphan-service tests should exercise `/Library/LaunchDaemons`, which exists on GitHub macOS runners.\n- **A test can pass vacuously after an early return**: `MOLE_TEST_MODE=1` can leave `$output` empty, and a final negative assertion then passes. End assertions with `|| return 1`, override test mode when the body must run, and add a positive control proving the output path executed. In an inner heredoc script use `|| exit 1`. Confirm the bracket behavior with a minimal repro when it matters: a non-final `[[ ]]` can be swallowed while `[ ]` still gates.\n- **A large payload piped into `grep -q` leaks a broken-pipe line into user output**: `grep -q` exits on its first match, and the `printf` still writing into that closed pipe takes SIGPIPE, which bash reports as `printf: write error: Broken pipe` on stderr. The live-cache owner probe fed the whole process table that way and the message landed mid-run in `mo clean`. Pass the data by here-string instead. Small variables holding a few lines of command output finish in one write and are unaffected, so the existing `echo \"$var\" | grep -q` sites are fine.\n- **Normalize with `10#` before any numeric comparison**: `[[ a -le b ]]` evaluates arithmetically, so a leading zero is read as octal and `0123` ranks below `100`, while `08` and `09` are not valid octal at all and abort the test with a bash error on stderr. A `^[0-9]+$` guard does not prevent either. Codex build numbers compared this way could have called a newer staged build superseded.\n- **`SECONDS` advances in whole seconds, so a 1s budget is not a second**: a deadline built as `SECONDS + 1` really means \"until the next second boundary\" and can collapse to almost nothing, making `_mole_timeout_with_deadline` return 124 before the command ever runs. Every timeout constant is 2 or more for this reason. A test of clamp arithmetic invokes the helper directly after resetting `SECONDS`; an exact value asserted across command substitution still races the fork on a loaded runner even with a larger window. When the fork is the behavior under test, assert a safe range or property instead of one remaining-second literal.\n- **BSD grep has no GNU null-output `-Z` contract**: on stock macOS it means `--decompress`. Enumerate files with `find ... -print0`, then probe each file with `grep -qF`.\n- **PlistBuddy reports missing-file creation on stdout**: redirect both stdout and stderr when creating plist fixtures so diagnostic prose does not pollute Bats `$output`.\n- **macOS 14 Bash can fire errexit through an if-guarded exported mock**: a failing exported `sudo` function inside an `if fn; then` path may terminate a `set -e` script on that runner while passing locally. Around the first sudo probe, disable errexit only for the probe and restore it before validation-gate returns. CI-only failures must print exit status, output, and a mock call trace rather than a bare return-code assertion.\n- **Expected capability absence is not probe failure**: keep `ready`, `not applicable`, and `misconfigured or unknown` distinct. A Mac with standalone Command Line Tools and no Xcode app has no simulator surface, so missing `simctl` is a quiet skip; an explicit invalid `DEVELOPER_DIR` remains actionable. Tests prove the not-applicable path does not invoke the unavailable owner command or emit warning activity.\n- **Fixed-width prefixes may have a free-form remainder**: after validating numeric and enum columns, join the remaining fields instead of assigning semantic meaning to one whitespace token. Darwin process `comm` values can contain spaces; trimming them to the first word destroyed zombie-parent attribution for helper apps. Keep primary and fallback formats separate when their fields carry different semantics.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Mole Agent Guide\n\nThis 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.\n\nLayout: 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.\n\n| File | Scope |\n|---|---|\n| `.claude/rules/shell.md` | Shell portability: BSD-only tools never through PATH, duplication audit, dead-symbol checks |\n| `.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 |\n| `.claude/rules/clean.md` | `mo clean` orchestration and sections, user/app/system caches, browsers, firmware, logs, orphan services, project caches, Homebrew, whitelist |\n| `.claude/rules/clean-dev.md` | `lib/clean/dev.sh`: developer and toolchain caches, recovery contracts, Nix, simctl, AI tool caches |\n| `.claude/rules/purge-installer.md` | `mo purge` discovery and protection, worktrees, `mo installer` |\n| `.claude/rules/uninstall.md` | `mo uninstall` inventory, display names, app protection, leftovers, sibling guard, batch execution, Homebrew casks |\n| `.claude/rules/optimize.md` | `mo optimize` tasks, preference repair, Spotlight, LaunchServices |\n| `.claude/rules/slim.md` | `mo slim`: Intel slice removal scope, seal-aware thinning, verified swap |\n| `.claude/rules/go-tuis.md` | `cmd/analyze` and `cmd/status` Go TUIs |\n| `.claude/rules/install-update.md` | `mole` router pins, `install.sh`, `mo update`, self-heal, install lock, `mo remove` |\n| `.claude/rules/tests.md` | Bats and Go test validity, test lint, CI jobs |\n| `.claude/rules/github.md` | Issue, PR and Discussion replies and closeout, Mole Mac invitation, announcements, issue version preflight |\n| `.claude/rules/readme.md` | README copies in every language |\n\nThe 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.\n\n## Product synchronization\n\nUse 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.\n\nEvery 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.\n\nBefore 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.\n\nCover 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.\n\n## Project\n\nMole 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.\n\n## Product Direction\n\nMole 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.\n\n### What Mole Should Do\n\n- Make cleanup and uninstall actions boring, reviewable, logged, protected by path/app rules, and dry-run capable.\n- Prefer reversible user-facing removals through Trash where the command surface expects recoverability.\n- Keep `clean`, `uninstall`, `purge`, and `installer` focused on reclaimable files, app leftovers, rebuildable caches, installer artifacts, and exact known cleanup targets.\n- 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.\n- 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.\n- Keep `optimize` focused on explicit, bounded maintenance tasks that can be explained before execution and tested without real authorization prompts.\n- Keep command UX dense and terminal-native: short labels, stable alignment, predictable shortcuts, one-screen summaries, then optional drill-down.\n- 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`.\n- Keep Mole Mac references as a cross-link or support path. The CLI and Mac app can share product values without requiring feature parity.\n\n### What Mole Should Not Do\n\n- Do not add broad system modification, privacy reset, package management, app bundle patching, or device-management features just because they are technically possible.\n- 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.\n- Do not add background agents, persistent monitoring, notifications, schedulers, menu bar behavior, or GUI-like state unless explicitly requested and justified as CLI scope.\n- Do not broaden leftover matching from exact app or bundle evidence into vendor-wide, TeamID-prefix, generic-name, or fallback wildcard deletion.\n- 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.\n- 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.\n- 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.\n\n### Product Decision Filter\n\nBefore accepting a new feature, answer these questions in the PR, issue, or review notes when the fit is not obvious:\n\n1. Does it clearly belong to clean, uninstall, analyze, optimize, status, purge, history, installer, slim, update, completion, touchid, or remove?\n2. Is it safe by default, previewable where destructive, testable without real auth, and explainable in one terminal screen?\n3. Can the user verify what will change before Mole changes it?\n4. Is the target data locally rebuildable, disposable, or backed by exact app/bundle evidence?\n5. Would this be better as Mole Mac UI, documentation, a warning, or an explicit \"not supported\" answer?\n\nIf the answer is no or unclear, decline the feature, narrow it, or park it until the product value beats the added surface area.\n\nA 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.\n\n## Repository Map\n\n- `.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.\n- `.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.\n- `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.\n- `lib/core/` - shared shell safety, UI, file operations, operation logs, app protection logic, and centralized timeout constants (`timeouts.sh`).\n- `lib/core/app_protection_data.sh` - readonly bundle ID and pattern arrays consumed by `app_protection.sh`. Data only, no logic.\n- `cmd/analyze/` and `cmd/status/` - Go TUIs (see `.claude/rules/go-tuis.md`).\n- `tests/` - Bats suites, PTY drivers and fixtures (see `.claude/rules/tests.md`).\n- `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`.\n- `docs/SECURITY_DESIGN.md` - design doc for the path validation / app protection / # SAFE annotation contract.\n- `SECURITY_AUDIT.md` - security review notes.\n\n## Commands\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\nMOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats\nMOLE_DRY_RUN=1 ./mole clean\nMOLE_TEST_NO_AUTH=1 ./mole clean --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole purge --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole installer --dry-run\nfind bin lib -name '*.sh' -print0 | xargs -0 -n1 bash -n\nmake build\ngo test ./...\n```\n\nPublic 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.\n\n## Critical Safety Rules\n\n- 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.\n- 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.\n- Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.\n- 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.\n- 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.\n- 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`.\n- 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.\n- 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.\n- 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.\n- 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`.\n- 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`.\n- 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`.\n- 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`.\n- 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.\n- **`_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.\n- **`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`.\n- **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`.\n- **`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`.\n- **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 `&amp;` last, since a key holding the literal text `&lt;` 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.\n- 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`.\n- Do not change ESC timeout behavior in `lib/core/ui.sh` unless explicitly requested.\n- 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`.\n- 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`.\n- **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.\n\n## Working Rules\n\n- 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.\n- 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.\n- `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.\n- Keep shell code formatted with `./scripts/check.sh --format`.\n- Prefer targeted Bats tests during development; run the full suite before committing.\n- Do not add AI attribution trailers to commits.\n\n## Verification\n\n- 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`).\n- `lib/core/sudo.sh` changes: run `MOLE_TEST_NO_AUTH=1 bats tests/manage_sudo.bats`.\n- Go changes: run `go test ./...`.\n- Area changes: run the test command listed for that hotspot in its `.claude/rules/` file; do not guess a narrower verifier.\n- Documentation-only changes: check links and commands.\n- **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.\n- **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.\n- **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.\n- 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.\n\n`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.\n\nIf `golangci-lint` reports issues from deleted temporary worktrees or non-existent paths, clear its local cache and rerun the linter:\n\n```bash\ngolangci-lint cache clean\ngolangci-lint run ./cmd/...\n```\n\n## Release\n\n**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.\n\nThe 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`.\n\nTag-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.\n","isInternal":false,"tokens":7285,"sizeBytes":32555},{"name":"shell-and-test-pitfalls.md","path":".claude/skills/bugs/references/shell-and-test-pitfalls.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/skills/bugs/references/shell-and-test-pitfalls.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Bounds, Shell, TTY, and parsing\n\nRead this reference when changing Shell code, Bats tests, update/install flows, timeout wrappers, TTY handling, plist fixtures, or macOS-version-specific CI behavior. The defect classes and repo-wide probes stay in the parent `bugs` skill.\n\n## 4. An external command or its consumer is unbounded\n\n`du`, `mdfind`, `find`, `xcrun simctl`, `system_profiler`, `ioreg`, and package tools can stall on a healthy but slow machine. Every production `du -s` route stays behind `run_with_timeout` with `MOLE_TIMEOUT_DISK_VERIFY_SEC`; `tests/core_timeout.bats` pins the class across `lib/` and `bin/`.\n\nCheck more than the obvious command:\n\n- Put checkpoints in every nested loop, not just each outer root (`edb214c0`).\n- Tune against the slowest healthy case. CoreSimulatorService needed a warm-up retry after a two-second bound falsely reported it unavailable (`35d856f1`).\n- Materialize a bounded producer completely and discard its output on nonzero status. Process substitution plus `|| true` must not feed a partial `find` prefix into deletion.\n- Keep probe and action pattern, type, age, and depth identical.\n- Time producer and consumer separately before raising a timeout. A 2.3-second `lsregister` dump followed by one command substitution per input line still becomes minutes.\n- Bound installed-binary `--version` and `--help` verification. Broken executables are the ones most likely to hang.\n- Keep install and update single-flight per target directory so one process cannot verify another generation.\n- The self-update bootstrap on a user's machine (temp file, registry, exec) is frozen at whatever version installed it, so a broken installed version cannot repair itself. That is why `_update_self_heal_reinstall` streams `install.sh` from `main` into bash with no local temp files (#1297). V1.47.1 shipped the false-success shape the bounded version check now prevents: update success read from installer output instead of from the installed binary.\n- The single-flight mutex prefers absolute `/usr/bin/lockf` because the kernel drops that lock even if the holder is killed. `lockf` only ships with newer macOS, and requiring it made install and update exit before writing a file on every older release (#1348); the `mkdir` fallback exists for those releases. The lock wrapper is not a shell array because the empty array is the fallback path, and an empty array under `set -u` is an unbound-variable error on Bash 3.2 (section 5).\n\n```bash\nfor command_name in 'du -s' mdfind xcrun system_profiler ioreg brew; do\n    printf '%-16s total=%-4s wrapped=%s\\n' \"$command_name\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | wc -l | tr -d ' ')\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | command grep -c run_with_timeout)\"\ndone\n```\n\n## 5. Bash 3.2, errexit, and pipefail change meaning\n\nmacOS ships Bash 3.2 and Mole runs with nounset.\n\n- Guard `\"${arr[@]}\"` with `[[ ${#arr[@]} -gt 0 ]]`; an empty array under `set -u` can abort a scan and orphan its spinner (`893b4e6f`, `2c06cb91`).\n- `fn || handler` disables errexit inside `fn` for the whole function. Safety-critical steps use explicit `if ! command; then return 1; fi` (`a33a0b51`). Installers must also verify the installed binary's reported version before claiming success; `tests/install_checksum.bats` covers the exact caller shape.\n- Do not rely on a caller's temporary `set +e` window for graceful degradation. Capture the status where the command runs.\n- Optional `[[ -n \"$value\" ]] && action` returns 1 when absent. Use `if/fi` inside status-sensitive blocks.\n- Fixing a branch that Bash 3.2 silently skipped is a behavior change for every caller. `read -t 0.01` fails on 3.2, so `drain_pending_input` never drained there; `ed02a6aa` made it flush the terminal queue after every menu key, and a pasted search term kept only its first character. Before enabling such a branch, list its callers and check each for text entry, call the system tool by absolute path (a version-manager `perl` shim can fail or add startup latency per key), and never fall back to a one-second integer read on a terminal.\n\n```bash\ncommand grep -rn '\\$\\{[a-z_]*\\[@\\]\\}' lib/ bin/\n```\n\n## 6. Background work steals TTY, stdin, or process groups\n\nThe Perl timeout fallback can hand the controlling terminal to its child. A background metadata worker then stopped the foreground uninstall prompt with SIGTTIN (`c93afca3`). BSD `mv` and `cp` can also prompt on stderr and read stdin when a destination is unwritable (`63030e3a`).\n\nEvery background worker that calls `run_with_timeout` closes stdin with `< /dev/null`. Commands that can prompt also use their noninteractive or force option. Menu and scan traps save and restore the caller's traps; `lib/ui/menu_paginated.sh` is the reference.\n\n## 7. System command output is treated as an API\n\nmacOS command output is localized, drifts between releases, and can print errors where data is expected.\n\n- Force `LC_ALL=C` for parsed metric subprocesses (`51b352a2`, `fa05b8cc`, `4e83743b`).\n- Validate field shape before trusting it: absolute path, numeric value, expected key, or exact enum.\n- Keep `DTSDKBuild` build identifiers separate from `DTPlatformVersion` versions (`f0896d03`).\n- Reject PlistBuddy's missing-file prose as data.\n- Use stock macOS semantics when checking flags. BSD `grep -Z` means `--decompress`; a developer alias may hide that.\n- Prefer exit codes, plist keys, and machine-readable output over prose matching.\n- Join records on the machine identifier, never on a heading or display string. `simctl runtime list` titles each image with the image version (`iOS 26.4.1`) while `simctl list devices` groups under the runtime short name (`iOS 26.4`). A name join calls every point release an orphan and offers `simctl runtime delete` for a runtime its simulators still bind (`#1505`). `mdls -name kMDItemDisplayName` returns the on-disk file name, not Finder's localized name, so it always differed from `app_name` and always won the old selection, leaving the `CFBundleDisplayName` and `CFBundleName` branches unreachable and shipping folder names like `VideoFusion-macOS` (`#1520`).\n- `plutil -p` is documented by `man plutil` as unstable and not designed for machine parsing. macOS 15 prints a JSON boolean true as `1` while macOS 26 and 27 print `true`, so a filter pinned to one spelling silently cleaned nothing on the other: `#1512` was fixed and then regressed for eight days with CI green. Where a boolean prints as `1` nothing can tell it from the integer 1. The rendering is also depth-blind, so `{\"outer\":{\"inner\":true}}` offered a directory named `inner` for deletion, a wrong deletion rather than a missed one. That is why `tests/clean_app_caches.bats` runs on the macos-15/macos-26 compatibility job.\n\nUse `command grep` when flag behavior matters, because the interactive environment may alias it.\n\n## 15. Cancellation is local unless orchestration makes it sticky\n\nClassify a timeout at its source before propagating it. Signal-derived cancellation and safety-guard timeouts that the caller treats as cancellation must remain sticky across the remaining command. Status `124` alone does not define the scope. Read the probe's product contract first:\n\n| Contract | Typical `124` | Typical `>=128` |\n|---|---|---|\n| Safety guard, owner unknown, or cancellation the caller already established | Sticky stop; later mutation and later sections must not start | Sticky stop |\n| Per-item sizing or removal of an otherwise eligible candidate | Skip or fail that item; later items may continue (`#1374`, `#1384`, `#1576`) | Sticky stop |\n| Review-only or advisory listing that never deletes | Skip that advice; later cleanup sections continue (`#1571`) | Sticky stop |\n| Cooperative section budget | Stop the rest of that section, report partial, continue later sections (`#1513`) | Sticky stop |\n\n- In `bin/clean.sh`, a final delete guard returning `124` cancels later work, while an individual `safe_remove` timeout is a reported failed removal and later items may continue. Timed-out sizing contributes an unknown/partial total, not false reclaimed bytes. Large-files Mail / Downloads / Updates rows follow the review-only skip: a size timeout omits that row, and a signal still cancels (`#1576`, `#1344`).\n- The orphaned-runtime review is advisory. A cold `simctl` that returns `124` skips the review and must not cancel later `mo clean` sections. Skip the review entirely when the unavailable-simulator listing already timed out after its warm-up retry (`#1571`, `04e658d2`).\n- Orphan leftover probe and sizing timeouts (`mdfind` in `is_bundle_orphaned` / `is_claude_vm_bundle_orphaned` / `_container_stub_app_exists`, plus `get_path_size_kb` and candidate snapshots) fail closed for that item and later leftovers plus later `mo clean` sections continue. A `safe_clean_guarded` 124 at the sink stays sticky (`#1584`). Do not cache a timed-out Spotlight miss as \"not installed\".\n- Cloud & Office uses a cooperative section deadline: stop the remaining items in that section, preserve parent counters, report partial completion, and continue later sections. Do not restore its removed outer timeout worker or the file-backed deferred-family replay that existed only for that worker.\n- Purge discovery discards incomplete root scans and marks the run incomplete. An authored-content probe returning `2` keeps and visibly reports that candidate; deletion-phase activity or removal timeouts cancel the run. Unknown evidence never permits deletion.\n\nThe inverse defect is as common as a missed sticky cancel. Treating a review-only `124` as command-level cancellation skips every later section. Treating a safety-guard `124` as a local skip deletes with unknown evidence.\n\nOnce the caller establishes cancellation, carry that decision through every boundary:\n\n- A best-effort loop must check a pending cancellation before probing or registering the next candidate.\n- A helper that reports ordinary misses as success must return a pending cancellation before starting the next family.\n- Command substitution, subshells, and background workers do not mutate the parent's shell variables. Return the status, or use an explicit parent-readable channel, then record it again in the parent.\n- A parallel coordinator stops and reaps peer workers, preserves cancellation over ordinary failures, and prevents the next rendered section from starting.\n- Dry-run uses the same cancellation contract as real cleanup. A preview ledger is still downstream work and must not continue after safety evidence becomes unavailable.\n\nThe regression shape matters. Make the first candidate's safety guard return 124 or 130 and make the second candidate succeed if reached. Assert the exact top-level status plus the absence of a positive trace from the second probe, preview registration, sink, and later section. If both candidates independently time out, the test cannot prove cancellation was sticky. Separately preserve the removal-timeout and cooperative-section-budget continuation cases in `tests/clean_core.bats`.\n\nDo not hide a cancelled safety probe behind `|| true`, a warning plus `return 0`, or a worker-local exported variable. Those shapes turn a global stop into a local skip.\n\nName the question instead of spelling the numbers: `mole_rc_timeout` and `mole_rc_timeout_or_signal` in `lib/core/timeout.sh` are the only places that compare a status against 124, and `scripts/audit_timeout_status.py` rejects a raw comparison anywhere else in `check.sh`. A per-item size whose number only feeds totals goes through `mole_item_size_continues` in `lib/core/file_ops.sh`: a timeout or failure keeps the item with an unknown size and marks the freed total partial, a signal records the cancellation and stops. Choosing between those helpers is still the caller's contract decision from the table above; the helper only makes the choice visible in review.\n\nA third predicate, `mole_rc_signal`, is true only for a status above 127 that `kill -l` names. A mutating owner command can exit with an errno-derived code such as npm's 243, which is an ordinary failure, so the owner-command wrappers (`clean_tool_cache`, `bun pm cache rm`, `simctl delete unavailable`, `brew cleanup`, the tart prune and `go clean`) cancel only on it and skip that one command on 124 (`clean_tool_cache treats non-signal high owner statuses as failures (#1695)`, `clean_tool_cache keeps valid signal statuses sticky (#1695)`, `clean_dev_go treats an errno-derived go clean exit as a failure, not an interrupt`). A timed-out `go env` lookup skips only the Go caches because nothing was attempted yet, while the `gh` probe and clear, the automation-browser `ps` listing and the clang cache resolver stay sticky (`clean_dev_cloud stops on GitHub CLI probe or clear cancellation`).\n\n## Focused pitfalls\n\n- **A PTY read loop must end on an empty read, an `OSError`, or a deadline.** After the child exits, a macOS pty master can stay readable and return `b''` forever, so a `while select(...)` drain with no exit spins until the job times out; Linux raises `EIO` instead. `tests/sudo_adoption_pty.py` hung this way on #1747 (fixed in the squash `eb531ba2`); the other `tests/*_pty.py` loops are deadline-bounded.\n\n- **Launcher signal stories behind the `lib/manage/update.sh` rules in `.claude/rules/install-update.md`**: a tty7 pane can inherit the daemon's blocked TERM mask, so a child launched without unblocking it left uninstall waiting forever for its spinner and made Ctrl-C cleanup hang. Bash prints a signalled child's job-status line (`Killed: 9 ... perl ...`) from whichever command finishes first after the child dies, so a runner that stalls the router for a few milliseconds leaked it at random until the launch, readiness wait and `wait` shared one `2> /dev/null` group. And Bash 3.2 can discard an INT received inside a USR1 trap entered from the `wait` builtin, which is why the readiness notification settles before `wait` starts.\n- **A `find` primary newer than the oldest supported macOS needs a probe, and dataless folders must not be listed**: listing a dataless CloudStorage folder makes File Provider fetch it over the network (`77050616`), an older `find` rejects `-flags +dataless` and fails the whole walk (`8dccb160`), and the capability probe itself must pass signals through (`df2f5e06`). Probe once per process: unknown means walk without the primary, a signal cancels, and never descend into a dataless folder.\n- **`BASH_SOURCE` / `$0` change meaning when a function moves files**: they name the file the code lives in, so copy-paste extraction is not behavior-preserving. `mole` captures `MOLE_ENTRY_SCRIPT=\"${BASH_SOURCE[0]}\"` before sourcing anything, and update code reads that stable entrypoint. Before extracting a function, grep it for `BASH_SOURCE`, `$0`, and `FUNCNAME`. Regression coverage lives in `tests/update.bats`.\n- **Bats heredocs share stdin with `read -n1`**: an inner `read -r -s -n1` can consume the next byte of the heredoc source. Redirect the function under test from `/dev/null`.\n- **macOS `script(1)` rejects socket-backed stdin**: PTY test helpers must redirect the wrapper's stdin from `/dev/null` or `script` can fail before starting the child. Capability probes use `/usr/bin/true`, not the absent `/bin/true`; inspect the actual failure before classifying it as unavailable TTY support, or live terminal tests silently skip.\n- **`run_with_timeout` execs the binary and bypasses shell-function mocks**: tests must use a PATH stub directory for commands such as `osascript`.\n- **CI runners may lack `/Library/PrivilegedHelperTools`**: orphan-service tests should exercise `/Library/LaunchDaemons`, which exists on GitHub macOS runners.\n- **A test can pass vacuously after an early return**: `MOLE_TEST_MODE=1` can leave `$output` empty, and a final negative assertion then passes. End assertions with `|| return 1`, override test mode when the body must run, and add a positive control proving the output path executed. In an inner heredoc script use `|| exit 1`. Confirm the bracket behavior with a minimal repro when it matters: a non-final `[[ ]]` can be swallowed while `[ ]` still gates.\n- **A large payload piped into `grep -q` leaks a broken-pipe line into user output**: `grep -q` exits on its first match, and the `printf` still writing into that closed pipe takes SIGPIPE, which bash reports as `printf: write error: Broken pipe` on stderr. The live-cache owner probe fed the whole process table that way and the message landed mid-run in `mo clean`. Pass the data by here-string instead. Small variables holding a few lines of command output finish in one write and are unaffected, so the existing `echo \"$var\" | grep -q` sites are fine.\n- **Normalize with `10#` before any numeric comparison**: `[[ a -le b ]]` evaluates arithmetically, so a leading zero is read as octal and `0123` ranks below `100`, while `08` and `09` are not valid octal at all and abort the test with a bash error on stderr. A `^[0-9]+$` guard does not prevent either. Codex build numbers compared this way could have called a newer staged build superseded.\n- **`SECONDS` advances in whole seconds, so a 1s budget is not a second**: a deadline built as `SECONDS + 1` really means \"until the next second boundary\" and can collapse to almost nothing, making `_mole_timeout_with_deadline` return 124 before the command ever runs. Every timeout constant is 2 or more for this reason. A test of clamp arithmetic invokes the helper directly after resetting `SECONDS`; an exact value asserted across command substitution still races the fork on a loaded runner even with a larger window. When the fork is the behavior under test, assert a safe range or property instead of one remaining-second literal.\n- **BSD grep has no GNU null-output `-Z` contract**: on stock macOS it means `--decompress`. Enumerate files with `find ... -print0`, then probe each file with `grep -qF`.\n- **PlistBuddy reports missing-file creation on stdout**: redirect both stdout and stderr when creating plist fixtures so diagnostic prose does not pollute Bats `$output`.\n- **macOS 14 Bash can fire errexit through an if-guarded exported mock**: a failing exported `sudo` function inside an `if fn; then` path may terminate a `set -e` script on that runner while passing locally. Around the first sudo probe, disable errexit only for the probe and restore it before validation-gate returns. CI-only failures must print exit status, output, and a mock call trace rather than a bare return-code assertion.\n- **Expected capability absence is not probe failure**: keep `ready`, `not applicable`, and `misconfigured or unknown` distinct. A Mac with standalone Command Line Tools and no Xcode app has no simulator surface, so missing `simctl` is a quiet skip; an explicit invalid `DEVELOPER_DIR` remains actionable. Tests prove the not-applicable path does not invoke the unavailable owner command or emit warning activity.\n- **Fixed-width prefixes may have a free-form remainder**: after validating numeric and enum columns, join the remaining fields instead of assigning semantic meaning to one whitespace token. Darwin process `comm` values can contain spaces; trimming them to the first word destroyed zombie-parent attribution for helper apps. Keep primary and fallback formats separate when their fields carry different semantics.\n","isInternal":false,"tokens":4421,"sizeBytes":19378},{"name":"SKILL.md","path":".claude/skills/bugs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/skills/bugs/SKILL.md","title":"bugs","category":"anthropic-skill","format":"markdown","content":"---\nname: bugs\ndescription: \"Mole incident catalog for cleanup safety, bounded probes, cancellation, dry-run parity, and actionable gates. Use for a Mole safety-sensitive diff; not for docs, release notes, or generic review.\"\nmetadata:\n  internal: true\n---\n\n# Mole bug patterns\n\nGeneric review belongs to Waza `check`; root-cause investigation of a live failure belongs to `hunt`.\n\n## Route before loading details\n\nChoose only the reference families touched by the evidence. A whole-project audit should classify surfaces first instead of loading every incident narrative.\n\n| # | Recurring shape | First probe | Read |\n|---|---|---|---|\n| 1 | Deletion candidate built from a weak name signal | Inspect name, bundle-id, fallback globs, and failed-owner shortcuts | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 2 | Existence or idleness decided by one probe | Enumerate every legitimate location and unknown outcome | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 3 | Guard present on only one branch | Diff dry-run, real, direct, fallback, and final-sink paths | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 4 | Unbounded external command | Count producer, consumer, inner-loop, and action bounds | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 5 | Bash 3.2, errexit, or pipefail trap | Check empty arrays, `fn || handler`, and optional actions | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 6 | TTY, stdin, or process-group theft | Inspect background workers and commands that may prompt | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 7 | System output parsed as a stable API | Force locale, validate shape, and join on identifiers not headings | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 8 | Persisted derived data outlives its algorithm | Trace schema, TTL, evidence fingerprint, and mutations | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 9 | Two paths compute one number differently | Find every producer and choose one definition | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 10 | Slow work looks frozen | Find operations over roughly one second outside feedback | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 11 | Regression test cannot fail | Prove positive control and pre-fix red state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 12 | Gate cannot explain why it refused | Map each reason to one cause and a next action that fails on the broken state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 13 | Mutation target is also accepted as a discovery container | Compare the recursive scan-root namespace with every purge target basename | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 14 | Owner metadata is treated as a complete, atomic inventory | Identify who writes it, whether absence is authoritative, and what locks mutation | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 15 | Cancellation stops one helper but later work continues | Classify each 124 as skip, section budget, or sticky cancel before tracing callers | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 16 | Async or cached data has no generation or freshness contract | Bind results to a request epoch and keep each sample's time, stale, and completeness fields together | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 17 | Publication gate trusts ambiguous or pre-existing state | Require exact source/tag equality, one generated target, and an expected-absence ref lease | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 18 | A sandbox well-known path is treated as app-private leftovers | Resolve Data/Downloads, Desktop, Pictures, Music, and Movies physically against $HOME | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n\nAGENTS.md and the path-scoped `.claude/rules/` files keep each rule stated fully enough to obey plus its test anchor; the incident story lives in these references or in `release-flow`, and the rule bullet points to the section that holds it. Moving a story here is a merge, never the deletion of a rule that still constrains behavior.\n\n## Trace the complete mutation lifecycle\n\nFor cleanup, purge, optimize, analyze deletion, or uninstall work, review the complete chain rather than the reported branch:\n\n```text\ndiscover or plan\n  -> cheap irreversible filters\n  -> owner and open-handle probes\n  -> size or metadata work\n  -> final owner re-probe\n  -> parent and target identity rebind\n  -> deletion or Trash sink\n  -> accounting, cancellation, and user output\n```\n\nAt every transition, answer:\n\n- Does live or unknown state fail closed?\n- Are timeouts classified by probe, sizing, removal, or section scope, with unknown evidence refusing deletion and cancellation stopping later mutation?\n- Are probe and sink bound to the same physical parent and target?\n- Do dry-run and real mode start from the same eligible plan without reusing stale authorization?\n- Are cheap missing, protected, whitelisted, and compiled-model filters ahead of recursive probes?\n- Does one cumulative deadline cover the dynamic scan scope, with checkpoints in nested loops?\n- Do refused, filtered, timed-out, or failed items stay out of cleaned counts and reclaimed bytes?\n- Can large candidates avoid per-item size work without making the reported total false?\n\nDo not trade final-sink rebinding or fail-closed owner checks for speed. Optimize absent targets, duplicated discovery probes, report-only work, and wrong-scope scans first.\n\n## Working contract\n\n- Before proposing a remedy, apply the Product Decision Filter in `AGENTS.md`: a new flag, environment variable, or visible retry message is a product change, even when the underlying fix is a safety improvement. Use the existing state/accounting reference when deciding what belongs in the normal summary.\n- A sandbox well-known directory is user data until physical resolution proves otherwise. `Data/Downloads` is usually `~/Downloads` under another name.\n- A refusal next step that succeeds on the broken state is not a next step. Do not relax the gate to make the message nicer.\n- Sweep siblings by call-site shape, not filename or helper name. Report `checked N / defective M / not applicable K`.\n- A recurring fix ships with a regression or source invariant that fails against the pre-fix code.\n- Treat tests as production consumers only after proving the production helper ran. Negative assertions require a positive trace.\n- A cancellation regression makes the next candidate otherwise eligible, then proves its probe and sink never run. Making every candidate fail for the same reason is a false sticky-cancellation test.\n- Absence-sensitive tests use an isolated `HOME` or fixture root; a shared `setup_file` home is not isolation.\n- Reproduce CI through `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh` when possible. If invoking Bats directly with jobs, preserve `--no-parallelize-within-files`; files share state and raw `bats --jobs 6 file.bats` changes semantics.\n- Treat specialist or AI reports as leads. Read the implementation, callers, fallback branches, and final sink yourself.\n\n## Verification bar\n\nUse the hotspot commands in the matching `.claude/rules/` file (routed from `AGENTS.md`); do not guess a narrower verifier. A typical Shell safety change finishes with:\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 bats tests/<area>.bats\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\ngo test ./...\nMOLE_TEST_NO_AUTH=1 MOLE_DRY_RUN=1 ./mole clean --dry-run\n```\n\nNever infer a production defect from a function name, comment, string, fixture, or `_test.go` match. Confirm the live call path and verify red-green before reporting the class fixed.\n","frontmatter":{"name":"bugs","description":"Mole incident catalog for cleanup safety, bounded probes, cancellation, dry-run parity, and actionable gates. Use for a Mole safety-sensitive diff; not for docs, release notes, or generic review.","metadata":{"internal":true}},"isInternal":true,"tokens":1800,"sizeBytes":8221},{"name":"SKILL.md","path":".claude/skills/release-flow/SKILL.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/skills/release-flow/SKILL.md","title":"release-flow","category":"anthropic-skill","format":"markdown","content":"---\nname: release-flow\ndescription: \"Mole CLI release runbook for distribution channels, pre-flight checks, capital-V tags, artifacts, and curated-note handoff. Use when assessing or executing a Mole release. Not for release-note copy alone or ordinary code review.\"\nmetadata:\n  internal: true\n---\n\n# Mole CLI Release Flow\n\nTag-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 opens a Homebrew core PR.\n\n## Distribution channels\n\n| Channel | What ships | Trigger | Automation |\n|---|---|---|---|\n| Nightly (`mo update --nightly`) | `main` HEAD via `install.sh` | Any commit pushed to `main` | Automatic; no tag or release involved |\n| GitHub stable release | amd64/arm64 binaries + `SHA256SUMS` | Push a capital-`V` tag | `release.yml` builds and creates the release; curated notes are a manual follow-up |\n| Homebrew core | Version-bump PR to `Homebrew/homebrew-core` | Same `V*` tag workflow | Automatic PR; merge timing is upstream's |\n| Nix flake | Source-built macOS package from the selected Git ref | `main` follows development; a release tag pins that release | Nix manages install, upgrade and removal; no separate registry publish |\n\nAt the start of any release-flavored task, restate which channels this run will touch and which it will not, and confirm with the maintainer before acting. Channel scope is specified by the maintainer, never inferred.\n\n## Pre-flight checklist\n\nResolve the latest published stable tag from GitHub before choosing the version or review range. Reconcile handoff claims against the current branch, worktree, and remote SHA; an earlier report of uncommitted work may describe commits that have already landed. Review all changes since that stable tag, not just the final fix batch.\n\n1. `grep '^VERSION=' mole` matches the new version.\n2. `SECURITY_AUDIT.md` opening line reflects the new version and date, and its CI coverage list matches the matrix in `.github/workflows/test.yml`.\n3. `git status -s` is empty or only contains intentionally staged release work.\n4. `git log origin/main..HEAD --oneline` shows only commits you intend to ship.\n5. `./scripts/check.sh --format` and `TERM=xterm-256color MOLE_TEST_NO_AUTH=1 MOLE_TEST_JOBS=2 BATS_FORMATTER=tap ./scripts/test.sh` both exit 0.\n6. `go test ./...` and `make build` both pass.\n7. `scripts/e2e_macos.py` exits 0 on a real Mac at the candidate commit. It installs IINA from its verified GitHub DMG, launches it, uninstalls it with every personal-data row selected through a real terminal, lists the DMG in `mo installer --dry-run`, deletes it from `mo analyze ~/Downloads`, and runs every other command read-only, so it changes only what it installed and never asks for a password. It refuses to start when the Mac already has IINA or any of its data. A failed run lists what it left behind; move those to the Trash before rerunning. Transcripts and `report.json` land in `~/Library/Logs/mole-e2e/<run>/`.\n\nUse the Go version declared in `go.mod` for local release builds, matching `actions/setup-go` in CI, then run `scripts/check_release_minos.sh` on both architectures. A newer local Go can raise the minimum macOS version even with `CGO_ENABLED=0`; during V1.54.0 verification, Go 1.27 produced macOS 13 binaries while the declared Go 1.25 toolchain preserved macOS 12. Rebuild with the declared toolchain instead of relaxing the minimum-OS gate. Check the `make release-amd64 release-arm64` outputs, not `make build`: the local build links `status` with cgo against the host SDK, so its `status-go` reports the host macOS as its minimum and says nothing about the release binaries.\n\nCapture the test runner's exit status and structured summary, with skipped tests reported separately. After pushing the candidate commit, wait for its required Check, Validation, CodeQL, and Nix workflows to finish successfully before tagging that exact SHA. Nix must verify both Darwin architectures and the installed/profile lifecycle. A cancelled run or a green check on another commit is not release evidence.\n\n## Tag and publish\n\n```bash\ngit push origin main\ngit tag V<version>          # capital V; release workflow ignores lowercase v\ngit push origin V<version>\n```\n\nWait for the workflow to finish. The workflow creates the release with assets but `generate_release_notes: false`, so notes must be added in a follow-up step.\n\nAfter the workflow finishes, verify the release assets before announcing anything: `gh release view V<version> --json assets --jq '.assets[].name'` must list all four `analyze-`/`status-darwin-{amd64,arm64}` binaries, both `binaries-darwin-*.tar.gz` Homebrew tarballs, AND `SHA256SUMS`. Install verification is fail-closed, so a release without a readable `SHA256SUMS` asset makes every install and `mo update` abort by design; a missing checksums file is a release blocker, not a cosmetic gap.\n\nDownload all seven assets. Verify the six payload checksums, attestations for all seven tied to the release tag, exact source commit, and `.github/workflows/release.yml`, and each archive's two expected binary members against the raw binary assets. Check Mach-O architecture and minimum macOS version on the downloaded binaries, then run the native architecture's Analyze and Status smoke probes. Successful local builds do not prove the public package contains those bytes.\n\nThen run a **script self-update smoke** before publishing notes or announcing: install the previous stable release through the script channel, run `mo update`, and confirm `mo --version` prints the candidate version. Script-installed clients execute the new tag's `install.sh`, so this is the only gate that exercises their real upgrade path; the pre-flight suite cannot cover it before the release exists. Homebrew is a separate downstream gate: verify it only after the core formula has updated, and never treat a script-channel smoke as proof that Homebrew is ready. If the script smoke fails, pull the release (see the pulling-and-re-releasing pitfall) before anyone is told to update.\n\nUse a fresh archive of the previous tag so ignored local `bin/*` builds cannot contaminate the old installation. Place the isolated prefix and config under a physical user-owned directory; `/tmp` or `/var` aliases and writable ancestors can correctly fail installer path checks. Isolate HOME, config/cache paths, and PATH, block host `brew` and `sudo`, and set `MOLE_TEST_NO_AUTH=1`. Verify `config/install_channel` retains `CHANNEL=stable`, `config/bin/{analyze,status}-go` match the verified release assets, changed installed shell sources match the tag, and a second update reports the current version. Keep the user's existing installation unchanged.\n\n## Apply curated release notes\n\nThe curated-notes flow (bilingual format, `gh release edit` instead of `create`, thanks block, and the six-reaction set) is owned by `.claude/skills/release-notes/SKILL.md`. `.agents/skills/release-notes` is a symlink to that canonical directory for Codex discovery, and its Codex-only invocation policy lives in `agents/openai.yaml`; do not replace the symlink with a copied mirror. Follow that skill; do not duplicate its format details here. Version, codename, and emoji go only in the release title; the body h1 is just `Mole`.\n\nAfter applying notes and the standard reactions through that skill, read back the published title, full body, and all six reactions. Report GitHub Stable and Homebrew availability separately; a successfully opened core PR still needs upstream tests and merge.\n\n- Issues whose reporter already verified the fix on nightly get no \"now in stable\" follow-up after the release, and none is proposed.\n\n## Release-notes craft\n\nFormat rules (impact ordering, command existence checks, icon semantics, no em dash, no inline PR refs) live in `.claude/skills/release-notes/SKILL.md` under \"Format rules\". Keep that skill as the single source of truth for notes formatting.\n\n## Release-only pitfalls\n\n- **Tag prefix is case-sensitive**: `release.yml` filters on `'V*'`. A lowercase `v1.38.0` tag will not trigger the workflow.\n- **Old clients fetch `install.sh` from the release tag, not from main**: a self-updating Mole downloads `raw.githubusercontent.com/tw93/mole/V<tag>/install.sh`, and tag content is immutable. An installer/updater bug therefore reaches existing stable users only through a new tag; fixing main changes Nightly but does not repair an already published stable updater.\n- **Never rewrite history an already published tag can reach**: the rule and the pre-rewrite tag audit are in `AGENTS.md` (Release). The incident behind it: on 2026-09-17, four days after V1.54.0 shipped, a `git filter-branch --msg-filter` run stripped a `Co-authored-by: Cursor` trailer from a commit dated 2026-05-06 that had 956 descendants. `--tag-name-filter cat` carried the tags onto the rebuilt commits, the release commit was rebuilt, and `brew upgrade mole` has failed the source checksum for every Intel user since, because Homebrew dropped Intel bottles so they all build from source (#1591). The enumeration was run afterwards: 25 published tags were reachable from that rewrite, so 25 source-tarball checksums moved, not one. Only V1.54.0 broke anything, because homebrew-core pins the checksum of the formula's current version alone, and `install.sh` anchors on the release assets' `SHA256SUMS` rather than on the tag archive it downloads. Any external consumer pinning an older tag's tarball is outside what can be checked from here. Recovery on a live release is a one-line `sha256` PR to `Homebrew/homebrew-core` with the current tarball's hash, and it takes two things that are easy to miss. The PR body must carry Homebrew's own template or a bot closes it within seconds as AI-written, editing it in reopens the same PR and opening a second one is explicitly refused. And a moved checksum is read as a possible supply-chain compromise, so a maintainer will hold the merge until the upstream author confirms in that thread that the change was benign; answer with the two commits' identical tree and the archive's pax header, which they can verify without trusting you, not with a promise. Check for an existing PR before opening one: the community usually files it first. Bottles are unaffected because BrewTestBot built them from the original download.\n- **Pulling and re-releasing a version**: `gh release delete V<old> --cleanup-tag` removes the release and remote tag. Delete the local tag, close the superseded Homebrew core PR with a one-line supersede comment before pushing the replacement tag (`release.yml` refuses to overwrite an existing `mole-<version>` fork branch and reuses, rather than recreates, an open PR for the same head), then bump `VERSION` and `SECURITY_AUDIT.md`, commit `release: V<new>`, tag, and run the normal publish flow. The Homebrew core PR regenerates on the new tag.\n\nWhen release work touches Shell code or tests, read `.claude/skills/bugs/references/shell-and-test-pitfalls.md` for Bash 3.2 arrays, heredoc input, mock bypasses, and CI-runner quirks.\n","frontmatter":{"name":"release-flow","description":"Mole CLI release runbook for distribution channels, pre-flight checks, capital-V tags, artifacts, and curated-note handoff. Use when assessing or executing a Mole release. Not for release-note copy alone or ordinary code review.","metadata":{"internal":true}},"isInternal":true,"tokens":2521,"sizeBytes":11164},{"name":"openai.yaml","path":".claude/skills/release-notes/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/skills/release-notes/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"policy:\n  allow_implicit_invocation: false\n","isInternal":false,"tokens":11,"sizeBytes":43},{"name":"SKILL.md","path":".claude/skills/release-notes/SKILL.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/skills/release-notes/SKILL.md","title":"release-notes","category":"anthropic-skill","format":"markdown","content":"---\nname: release-notes\ndescription: Publish curated release notes for an existing Mole `V<version>` tag, including bilingual format, `gh release edit`, contributor thanks, and reactions. Use only when explicitly asked to edit or publish Mole release notes. Not for release readiness, tagging, or code review.\ndisable-model-invocation: true\nmetadata:\n  internal: true\n---\n\n# Mole release notes\n\nThis skill drives the curated-notes step that runs **after** `release.yml` has finished. The workflow creates the GitHub Release with assets but with `generate_release_notes: false`, so notes must be added in a follow-up `gh release edit` (never `gh release create`, the release already exists, and `create` will conflict).\n\n## Inputs to gather\n\nBefore drafting, confirm:\n\n1. **Version**. Capital `V`, e.g. `V1.38.0`. Lowercase `v` does not trigger the workflow and may indicate a botched tag.\n2. **CodeName + emoji**. Ask the user. The title format is `V<version> <CodeName> <emoji>`.\n3. **Release commit range**. `git log <previous-tag>..V<version> --oneline` gives the raw material.\n4. **User-visible behavior changes**. Scan the full commit message bodies (not just subjects) for narrowed detection, removed features, or controlled regressions. These belong in notes even when they are not bug-fix-shaped, because users will encounter the changed boundary in production.\n5. **Issue reporters and PR contributors in this cycle**. Derive them from what the release actually contains, never from a date window of closed issues: `git log --format='%an' <prev>..<tag>` for contributors, and the issue numbers the range's own commit messages cite for reporters, checking each is `state_reason: completed` and was fixed here rather than merely referenced as background. A closing window silently pulls in `not_planned` issues and everything the PREVIOUS release fixed, which is how V1.55.0 nearly shipped 30 names where 8 were real. Keep it short, for example `Issue reporters and PR contributors this cycle: @a · @b.` Exclude `tw93`, `youxi798` and bots.\n6. **Verify release exists**. `gh release view V<version> --repo tw93/Mole --json id,name` should return non-empty. If it doesn't, the workflow hasn't finished, wait, don't `gh release create`.\n\n## Pre-flight (published-tag evidence)\n\nUse the target tag's exact commit and the completed checks and public-asset evidence from [release-flow](../release-flow/SKILL.md). Reuse that evidence when it still matches the immutable tag; rerun only a missing or failed gate. A build of a newer working tree does not verify the release being described. The full build, test, asset, and script-update gates stay owned by `release-flow` rather than a second checklist here.\n\nIf evidence is missing, complete the applicable release-flow gate before publishing notes. Keep draft work read-only; an existing release object alone is not proof its assets or upgrade path passed.\n\n## Format\n\nStrictly follow the current compact release shape. Read the previous published stable release before the target tag as the live format reference. Exclude the candidate release: the workflow may already have made it the latest release with an empty body. Resolve the previous tag first, then read it with `gh release view <previous-tag> --repo tw93/Mole --json tagName,body`.\n\nStructure:\n\n```\n<div align=\"center\">\n  <img src=\"https://cdn.tw93.fun/pic/cole.png\" alt=\"Mole Logo\" width=\"120\" height=\"120\" style=\"border-radius:50%\" />\n  <h1 style=\"margin: 12px 0 6px;\">Mole</h1>\n  <p><em>Deep clean and optimize your Mac.</em></p>\n</div>\n\n### Changelog\n\n1. **<English headline>**: <one-sentence English elaboration>.\n2. ...\n\n### 更新日志\n\n1. **<中文 headline>**：<一句中文说明>。\n2. ...\n\n### Thanks\n\nIssue reporters and PR contributors this cycle: @handle1 · @handle2.\n\n### Mole Mac App\n\nPrefer a GUI? [Mole Mac App](https://mole.fit/) brings cleaning, app management, maintenance, disk analysis, and live system status into one native app, with review before deletion and a customizable menu bar HUD. It is $19 once, with lifetime updates and a 14-day refund. [Download and try it](https://mole.fit/download). The CLI stays free and open source.\n```\n\nNo `---` separators between sections, and no trailing repository link; the published pages end on the Mole Mac App line.\n\n### Format rules (all are documented bugs that have shipped before)\n\n- **Body h1 is just `Mole`**. Version, codename, and emoji live only in the `--title` argument (`V<version> <CodeName> <emoji>`); repeating them in the body header is redundant and has been explicitly rejected before.\n- **No em dash anywhere**. Use commas, periods, colons, semicolons, or parentheses.\n- **No sponsor list by default**. The current public release style thanks issue reporters and PR contributors for this cycle only.\n- **No emoji except the version emoji in the release title**. Body section headers stay plain, including `### Thanks` (the old `Thanks 💖` header is gone from the published pages).\n- **No inline PR refs, no inline `@handle` thanks**. PRs and people belong in the dedicated Thanks block only.\n- **English block first, 中文 block second**. Same numbered order in both blocks. Same number of items.\n- **Order items by user-perceived impact, not commit chronology**. Headline change first; internal safety hardening, performance, and bug fixes follow.\n- **Do not describe overview icons that no longer exist**. Analyze overview rows are text-only because emoji width and baselines vary across terminals. If icons return later, they must not imply that user data such as iOS Backups, Xcode Archives, or Old Downloads is safe to delete.\n- **Verify every command mentioned in the notes actually exists in HEAD**. The deleted `mo check` / `mo doctor` commands nearly shipped in notes as a \"feature\" after they were removed.\n- **An incident or troubleshooting note is one sentence of symptom plus one command**. No cause taxonomy, no command per branch; the user needs the one line that gets them unstuck. Match the previous release's language treatment for that note: if the last release carried it in one language, do not add a second.\n- **Keep the Mole Mac App cross-link to one restrained, fact-backed paragraph**. Validate the product scope, price, updates, refund window, and download URL against the current homepage before publishing.\n\n## Publish\n\nOnce the user approves the draft:\n\n```bash\ngh release edit V<version> --repo tw93/Mole \\\n  --title \"V<version> <CodeName> <emoji>\" \\\n  --notes-file <path-to-draft>\n```\n\n**Never** `gh release create`, it conflicts with the release the workflow already made.\n\nThen add the six reactions with this skill's helper (path is relative to this SKILL.md, not the repo-root `scripts/`): `bash \"$(dirname <this SKILL.md>)/scripts/post-reactions.sh\" V<version>`.\n\n## After publish\n\n- `gh release view V<version> --repo tw93/Mole --web` (open in browser) so the user can eyeball it.\n- Remind the user: the Homebrew Core PR is workflow-driven and should already be in flight; do not re-run it manually unless the workflow log shows a failure.\n\n## When NOT to act\n\nThis skill is user-invocable only. It must not run unprompted:\n\n- If the user mentions release notes in passing, draft only; do not call `gh release edit`.\n- If `gh release view` shows the release does not exist yet, wait for the workflow; do not create a competing release manually.\n- If the user has not given an explicit \"publish\" / \"提交\" signal, stop after the draft.\n\n## Helper script\n\n`scripts/post-reactions.sh <tag>` lives next to this SKILL.md and adds the six reactions (`+1`, `laugh`, `hooray`, `heart`, `rocket`, `eyes`) to the release.\n","frontmatter":{"name":"release-notes","description":"Publish curated release notes for an existing Mole `V<version>` tag, including bilingual format, `gh release edit`, contributor thanks, and reactions. Use only when explicitly asked to edit or publish Mole release notes. Not for release readiness, tagging, or code review.","disable-model-invocation":true,"metadata":{"internal":true},"disableModelInvocation":true},"isInternal":true,"tokens":1807,"sizeBytes":7661},{"name":"mole-test-safety.mdc","path":".cursor/rules/mole-test-safety.mdc","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.cursor/rules/mole-test-safety.mdc","title":"Cursor Rule: mole-test-safety","category":"cursor-rule","format":"mdc","content":"---\ndescription: Mole test safety and verification defaults\nalwaysApply: true\n---\n\n# Mole Test Safety\n\n- Default test and verification commands must run with `MOLE_TEST_NO_AUTH=1` or through `scripts/test.sh`, which sets it automatically.\n- Tests must never trigger real `sudo`, Touch ID, password prompts, AppleScript permission dialogs, or system service changes.\n- Any new direct use of `sudo`, `osascript`, or `launchctl` must either be guarded by `MOLE_TEST_MODE` / `MOLE_TEST_NO_AUTH` or be fully mocked in the test path.\n- Prefer focused regression tests first, then run full no-auth verification before release-sensitive changes.\n","frontmatter":{"description":"Mole test safety and verification defaults","alwaysApply":true},"isInternal":false,"tokens":142,"sizeBytes":638},{"name":"post-reactions.sh","path":".claude/skills/release-notes/scripts/post-reactions.sh","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/skills/release-notes/scripts/post-reactions.sh","title":"Claude Directive: post-reactions.sh","category":"claude-rule","format":"markdown","content":"#!/bin/bash\n# Add the standard six reactions (+1, laugh, hooray, heart, rocket, eyes) to a\n# tw93/Mole release. Usage: post-reactions.sh V<version>\n\nset -euo pipefail\n\nTAG=\"${1:-}\"\nif [[ -z \"$TAG\" ]]; then\n    echo \"Usage: $0 V<version>\" >&2\n    exit 1\nfi\n\nif [[ \"$TAG\" != V* ]]; then\n    echo \"Tag must start with capital V (release.yml ignores lowercase v): $TAG\" >&2\n    exit 1\nfi\n\nif ! command -v gh > /dev/null 2>&1; then\n    echo \"gh CLI is required\" >&2\n    exit 1\nfi\n\nRELEASE_ID=$(gh api \"repos/tw93/Mole/releases/tags/$TAG\" --jq '.id')\nif [[ -z \"$RELEASE_ID\" ]]; then\n    echo \"Release not found for tag: $TAG\" >&2\n    exit 1\nfi\n\nfor r in +1 laugh hooray heart rocket eyes; do\n    gh api \"repos/tw93/Mole/releases/$RELEASE_ID/reactions\" \\\n        -X POST -f content=\"$r\" --silent\ndone\n\necho \"Posted 6 reactions to $TAG (release id $RELEASE_ID)\"\n","isInternal":false,"tokens":282,"sizeBytes":854},{"name":"bash32-portability-reviewer.md","path":".claude/agents/bash32-portability-reviewer.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/agents/bash32-portability-reviewer.md","title":"bash32-portability-reviewer","category":"subagent-persona","format":"markdown","content":"---\nname: bash32-portability-reviewer\ndescription: Reviews Mole shell and Bats diffs against the current macOS Bash 3.2, errexit, timeout, TTY, BSD-tool, and CI-runner pitfalls recorded in .claude/skills/bugs/references/shell-and-test-pitfalls.md. Use after changes under mole, install.sh, bin/**, lib/**, scripts/**, or tests/*.bats.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole shell portability reviewer\n\nRead the current `.claude/skills/bugs/references/shell-and-test-pitfalls.md`\nbefore every review. It is the source of truth and grows when a new incident\nbecomes a stable invariant. Do not rely on a fixed count or a copied historical\nlist in this profile.\n\nYou read diffs, production context, and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base. Restrict findings to `mole`,\n   `install.sh`, `bin/**`, `lib/**`, `scripts/**`, and `tests/*.bats`.\n2. Turn every current pitfall bullet in that reference into a check against the\n   touched code. The list below is a search aid, not a replacement for it:\n   - moved functions using `BASH_SOURCE`, `$0`, or `FUNCNAME`;\n   - `du -s` calls outside `run_with_timeout`;\n   - possibly empty array expansion under `set -u`;\n   - functions called through `if` or `||` that rely on errexit internally;\n   - `[[ ... ]] && cmd` in exit-code-sensitive blocks;\n   - heredoc-driven tests of `read -n1` without redirected stdin;\n   - shell-function mocks hidden by timeout wrappers that exec a real binary;\n   - GNU-only command flags or CI fixtures that assume local macOS directories;\n   - PlistBuddy stdout leaking into assertions;\n   - tests that can pass on empty output or an early return;\n   - macOS-runner-specific errexit behavior around failing command mocks.\n3. Read enough surrounding code to prove the pattern actually fires. A grep hit\n   alone is not a finding.\n4. Check that the regression test reaches the intended branch and that every\n   assertion failure propagates. If local and CI behavior differ, require a\n   failure trace that exposes status, output, and mock calls.\n\n## Output\n\nFor every confirmed problem:\n\n```\nLANDMINE: <file>:<line> - <problem>\n  Pattern: <matched code>\n  Why it fires here: <context>\n  Fix: <one concrete change>\n```\n\nUse `UNVERIFIED: <file>:<line> - <missing evidence>` when context cannot resolve\na real risk. End with `VERDICT: <N> landmines, fix before merge` when findings\nexist, otherwise `VERDICT: no landmines found`. With no findings or unverified\nitems, output only `VERDICT: no landmines found`.\n","frontmatter":{"name":"bash32-portability-reviewer","description":"Reviews Mole shell and Bats diffs against the current macOS Bash 3.2, errexit, timeout, TTY, BSD-tool, and CI-runner pitfalls recorded in .claude/skills/bugs/references/shell-and-test-pitfalls.md. Use after changes under mole, install.sh, bin/**, lib/**, scripts/**, or tests/*.bats.","tools":"Read, Grep, Glob, Bash"},"isInternal":false,"tokens":626,"sizeBytes":2514},{"name":"safety-reviewer.md","path":".claude/agents/safety-reviewer.md","rawUrl":"https://raw.githubusercontent.com/tw93/Mole/HEAD/.claude/agents/safety-reviewer.md","title":"safety-reviewer","category":"subagent-persona","format":"markdown","content":"---\nname: safety-reviewer\ndescription: Audits Mole changes for destructive-action regressions across deletion, app protection, privilege boundaries, dry-run behavior, operation logging, package-manager cleanup, and exact leftover matching. Use before merging changes under lib/clean/**, lib/uninstall/**, lib/manage/**, bin/clean.sh, bin/purge.sh, bin/uninstall.sh, bin/installer.sh, lib/core/file_ops.sh, or lib/core/app_protection*.sh.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole destructive-action safety reviewer\n\nRead `AGENTS.md` sections \"Critical Safety Rules\", \"Working Rules\", and\n\"Verification\", every `.claude/rules/*.md` file whose `paths:` match the diff\n(its routing table names them), plus `docs/SECURITY_DESIGN.md`, before judging\nthe diff. Those files are the current safety contract. This profile defines the\nreview method and output shape only; it must not become a copied policy list.\n\nYou read code and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base and read the issue or PR scope.\n   A request for one leftover path is not permission to add a broader matcher.\n2. Mark every changed destructive sink and every new path source. Pay special\n   attention to `find_app_files`, `mole_delete`, `remove_file_list`, container\n   traversal, Group Containers, bundle-prefix matchers, and recursive `find`\n   branches that eventually delete.\n3. Audit each branch independently, including fallbacks. For every candidate,\n   prove exact app or bundle evidence, protected-path coverage, preview or\n   confirmation, dry-run behavior, operation logging, and the final deletion\n   helper. A safe primary branch does not make a broad fallback safe.\n4. Treat raw removal outside `lib/core/file_ops.sh` as P0 unless the call site\n   has a narrow `# SAFE:` exception for an already verified exact path and a\n   regression test that proves why the shared funnel cannot be used. Never\n   generalize one exception into a second deletion API.\n5. For new `sudo`, `osascript`, `launchctl`, package-manager, or service teardown\n   calls, verify test/auth guards, non-interactive test behavior, exact preview,\n   and failure propagation. Typed password input must not be mistaken for skip.\n6. For uninstall teardown, prove every route passes the shared-bundle-id sibling\n   guard, including volume copies, inverse names, and shared identities.\n7. Read enough surrounding production code to follow helper calls to their final\n   sink. Then map the change to the exact hotspot commands in the matching\n   `.claude/rules/` file and \"Verification\"; missing safety coverage is a finding.\n\n## Severity\n\n- **P0**: a path can escape its intended target, protection/confirmation/dry-run\n  is bypassed, a destructive failure can be reported as success, or a privileged\n  action can execute during ordinary verification.\n- **P1**: matching is broader than exact evidence, a teardown route bypasses a\n  shared guard, active user/developer state can be removed, package-manager\n  candidates are not previewed, or a safety regression lacks a direct test.\n- **P2**: the behavior is bounded but the documented targeted verification was\n  not run or the failure output is not actionable.\n\nDo not flag style, speculative refactors, or test-only panic/expect patterns.\nIf a helper or guard cannot be traced, report it as `UNVERIFIED` rather than\nassuming it is safe.\n\n## Output\n\nOrder findings by severity:\n\n```\nP0: <file>:<line> - <problem>\n  Why unsafe: <broken invariant>\n  Fix: <one concrete change>\n```\n\nUse the same shape for P1 and P2. End with `VERDICT: changes required` when any\nP0 or P1 exists, otherwise `VERDICT: safe to merge`. With no findings or\nunverified items, output only `VERDICT: safe to merge`.\n","frontmatter":{"name":"safety-reviewer","description":"Audits Mole changes for destructive-action regressions across deletion, app protection, privilege boundaries, dry-run behavior, operation logging, package-manager cleanup, and exact leftover matching. Use before merging changes under lib/clean/**, lib/uninstall/**, lib/manage/**, bin/clean.sh, bin/purge.sh, bin/uninstall.sh, bin/installer.sh, lib/core/file_ops.sh, or lib/core/app_protection*.sh.","tools":"Read, Grep, Glob, Bash"},"isInternal":false,"tokens":840,"sizeBytes":3714}],"systemPromptSnippet":"<yakaai_skills repo=\"tw93/Mole\">\n<!-- File: AGENTS.md (Tokens: ~7285 | Category: root-instruction) -->\n# Mole Agent Guide\n\nThis 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.\n\nLayout: 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.\n\n| File | Scope |\n|---|---|\n| `.claude/rules/shell.md` | Shell portability: BSD-only tools never through PATH, duplication audit, dead-symbol checks |\n| `.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 |\n| `.claude/rules/clean.md` | `mo clean` orchestration and sections, user/app/system caches, browsers, firmware, logs, orphan services, project caches, Homebrew, whitelist |\n| `.claude/rules/clean-dev.md` | `lib/clean/dev.sh`: developer and toolchain caches, recovery contracts, Nix, simctl, AI tool caches |\n| `.claude/rules/purge-installer.md` | `mo purge` discovery and protection, worktrees, `mo installer` |\n| `.claude/rules/uninstall.md` | `mo uninstall` inventory, display names, app protection, leftovers, sibling guard, batch execution, Homebrew casks |\n| `.claude/rules/optimize.md` | `mo optimize` tasks, preference repair, Spotlight, LaunchServices |\n| `.claude/rules/slim.md` | `mo slim`: Intel slice removal scope, seal-aware thinning, verified swap |\n| `.claude/rules/go-tuis.md` | `cmd/analyze` and `cmd/status` Go TUIs |\n| `.claude/rules/install-update.md` | `mole` router pins, `install.sh`, `mo update`, self-heal, install lock, `mo remove` |\n| `.claude/rules/tests.md` | Bats and Go test validity, test lint, CI jobs |\n| `.claude/rules/github.md` | Issue, PR and Discussion replies and closeout, Mole Mac invitation, announcements, issue version preflight |\n| `.claude/rules/readme.md` | README copies in every language |\n\nThe 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.\n\n## Product synchronization\n\nUse 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.\n\nEvery 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.\n\nBefore 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.\n\nCover 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.\n\n## Project\n\nMole 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.\n\n## Product Direction\n\nMole 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.\n\n### What Mole Should Do\n\n- Make cleanup and uninstall actions boring, reviewable, logged, protected by path/app rules, and dry-run capable.\n- Prefer reversible user-facing removals through Trash where the command surface expects recoverability.\n- Keep `clean`, `uninstall`, `purge`, and `installer` focused on reclaimable files, app leftovers, rebuildable caches, installer artifacts, and exact known cleanup targets.\n- 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.\n- 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.\n- Keep `optimize` focused on explicit, bounded maintenance tasks that can be explained before execution and tested without real authorization prompts.\n- Keep command UX dense and terminal-native: short labels, stable alignment, predictable shortcuts, one-screen summaries, then optional drill-down.\n- 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`.\n- Keep Mole Mac references as a cross-link or support path. The CLI and Mac app can share product values without requiring feature parity.\n\n### What Mole Should Not Do\n\n- Do not add broad system modification, privacy reset, package management, app bundle patching, or device-management features just because they are technically possible.\n- 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.\n- Do not add background agents, persistent monitoring, notifications, schedulers, menu bar behavior, or GUI-like state unless explicitly requested and justified as CLI scope.\n- Do not broaden leftover matching from exact app or bundle evidence into vendor-wide, TeamID-prefix, generic-name, or fallback wildcard deletion.\n- 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.\n- 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.\n- 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.\n\n### Product Decision Filter\n\nBefore accepting a new feature, answer these questions in the PR, issue, or review notes when the fit is not obvious:\n\n1. Does it clearly belong to clean, uninstall, analyze, optimize, status, purge, history, installer, slim, update, completion, touchid, or remove?\n2. Is it safe by default, previewable where destructive, testable without real auth, and explainable in one terminal screen?\n3. Can the user verify what will change before Mole changes it?\n4. Is the target data locally rebuildable, disposable, or backed by exact app/bundle evidence?\n5. Would this be better as Mole Mac UI, documentation, a warning, or an explicit \"not supported\" answer?\n\nIf the answer is no or unclear, decline the feature, narrow it, or park it until the product value beats the added surface area.\n\nA 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.\n\n## Repository Map\n\n- `.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.\n- `.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.\n- `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.\n- `lib/core/` - shared shell safety, UI, file operations, operation logs, app protection logic, and centralized timeout constants (`timeouts.sh`).\n- `lib/core/app_protection_data.sh` - readonly bundle ID and pattern arrays consumed by `app_protection.sh`. Data only, no logic.\n- `cmd/analyze/` and `cmd/status/` - Go TUIs (see `.claude/rules/go-tuis.md`).\n- `tests/` - Bats suites, PTY drivers and fixtures (see `.claude/rules/tests.md`).\n- `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`.\n- `docs/SECURITY_DESIGN.md` - design doc for the path validation / app protection / # SAFE annotation contract.\n- `SECURITY_AUDIT.md` - security review notes.\n\n## Commands\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\nMOLE_TEST_NO_AUTH=1 bats tests/clean_core.bats\nMOLE_DRY_RUN=1 ./mole clean\nMOLE_TEST_NO_AUTH=1 ./mole clean --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole purge --dry-run\nMOLE_TEST_NO_AUTH=1 ./mole installer --dry-run\nfind bin lib -name '*.sh' -print0 | xargs -0 -n1 bash -n\nmake build\ngo test ./...\n```\n\nPublic 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.\n\n## Critical Safety Rules\n\n- 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.\n- 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.\n- Route user-facing cleanup through Trash where the project expects recoverability, especially for analyze-driven ad hoc cleanup.\n- 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.\n- 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.\n- 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`.\n- 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.\n- 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.\n- 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.\n- 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`.\n- 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`.\n- 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`.\n- 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`.\n- 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.\n- **`_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.\n- **`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`.\n- **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`.\n- **`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`.\n- **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 `&amp;` last, since a key holding the literal text `&lt;` 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.\n- 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`.\n- Do not change ESC timeout behavior in `lib/core/ui.sh` unless explicitly requested.\n- 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`.\n- 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`.\n- **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.\n\n## Working Rules\n\n- 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.\n- 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.\n- `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.\n- Keep shell code formatted with `./scripts/check.sh --format`.\n- Prefer targeted Bats tests during development; run the full suite before committing.\n- Do not add AI attribution trailers to commits.\n\n## Verification\n\n- 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`).\n- `lib/core/sudo.sh` changes: run `MOLE_TEST_NO_AUTH=1 bats tests/manage_sudo.bats`.\n- Go changes: run `go test ./...`.\n- Area changes: run the test command listed for that hotspot in its `.claude/rules/` file; do not guess a narrower verifier.\n- Documentation-only changes: check links and commands.\n- **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.\n- **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.\n- **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.\n- 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.\n\n`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.\n\nIf `golangci-lint` reports issues from deleted temporary worktrees or non-existent paths, clear its local cache and rerun the linter:\n\n```bash\ngolangci-lint cache clean\ngolangci-lint run ./cmd/...\n```\n\n## Release\n\n**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.\n\nThe 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`.\n\nTag-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.\n\n<!-- File: .claude/skills/bugs/references/shell-and-test-pitfalls.md (Tokens: ~4421 | Category: anthropic-skill) -->\n# Bounds, Shell, TTY, and parsing\n\nRead this reference when changing Shell code, Bats tests, update/install flows, timeout wrappers, TTY handling, plist fixtures, or macOS-version-specific CI behavior. The defect classes and repo-wide probes stay in the parent `bugs` skill.\n\n## 4. An external command or its consumer is unbounded\n\n`du`, `mdfind`, `find`, `xcrun simctl`, `system_profiler`, `ioreg`, and package tools can stall on a healthy but slow machine. Every production `du -s` route stays behind `run_with_timeout` with `MOLE_TIMEOUT_DISK_VERIFY_SEC`; `tests/core_timeout.bats` pins the class across `lib/` and `bin/`.\n\nCheck more than the obvious command:\n\n- Put checkpoints in every nested loop, not just each outer root (`edb214c0`).\n- Tune against the slowest healthy case. CoreSimulatorService needed a warm-up retry after a two-second bound falsely reported it unavailable (`35d856f1`).\n- Materialize a bounded producer completely and discard its output on nonzero status. Process substitution plus `|| true` must not feed a partial `find` prefix into deletion.\n- Keep probe and action pattern, type, age, and depth identical.\n- Time producer and consumer separately before raising a timeout. A 2.3-second `lsregister` dump followed by one command substitution per input line still becomes minutes.\n- Bound installed-binary `--version` and `--help` verification. Broken executables are the ones most likely to hang.\n- Keep install and update single-flight per target directory so one process cannot verify another generation.\n- The self-update bootstrap on a user's machine (temp file, registry, exec) is frozen at whatever version installed it, so a broken installed version cannot repair itself. That is why `_update_self_heal_reinstall` streams `install.sh` from `main` into bash with no local temp files (#1297). V1.47.1 shipped the false-success shape the bounded version check now prevents: update success read from installer output instead of from the installed binary.\n- The single-flight mutex prefers absolute `/usr/bin/lockf` because the kernel drops that lock even if the holder is killed. `lockf` only ships with newer macOS, and requiring it made install and update exit before writing a file on every older release (#1348); the `mkdir` fallback exists for those releases. The lock wrapper is not a shell array because the empty array is the fallback path, and an empty array under `set -u` is an unbound-variable error on Bash 3.2 (section 5).\n\n```bash\nfor command_name in 'du -s' mdfind xcrun system_profiler ioreg brew; do\n    printf '%-16s total=%-4s wrapped=%s\\n' \"$command_name\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | wc -l | tr -d ' ')\" \\\n        \"$(command grep -rn -- \"$command_name\" lib/ bin/ | command grep -c run_with_timeout)\"\ndone\n```\n\n## 5. Bash 3.2, errexit, and pipefail change meaning\n\nmacOS ships Bash 3.2 and Mole runs with nounset.\n\n- Guard `\"${arr[@]}\"` with `[[ ${#arr[@]} -gt 0 ]]`; an empty array under `set -u` can abort a scan and orphan its spinner (`893b4e6f`, `2c06cb91`).\n- `fn || handler` disables errexit inside `fn` for the whole function. Safety-critical steps use explicit `if ! command; then return 1; fi` (`a33a0b51`). Installers must also verify the installed binary's reported version before claiming success; `tests/install_checksum.bats` covers the exact caller shape.\n- Do not rely on a caller's temporary `set +e` window for graceful degradation. Capture the status where the command runs.\n- Optional `[[ -n \"$value\" ]] && action` returns 1 when absent. Use `if/fi` inside status-sensitive blocks.\n- Fixing a branch that Bash 3.2 silently skipped is a behavior change for every caller. `read -t 0.01` fails on 3.2, so `drain_pending_input` never drained there; `ed02a6aa` made it flush the terminal queue after every menu key, and a pasted search term kept only its first character. Before enabling such a branch, list its callers and check each for text entry, call the system tool by absolute path (a version-manager `perl` shim can fail or add startup latency per key), and never fall back to a one-second integer read on a terminal.\n\n```bash\ncommand grep -rn '\\$\\{[a-z_]*\\[@\\]\\}' lib/ bin/\n```\n\n## 6. Background work steals TTY, stdin, or process groups\n\nThe Perl timeout fallback can hand the controlling terminal to its child. A background metadata worker then stopped the foreground uninstall prompt with SIGTTIN (`c93afca3`). BSD `mv` and `cp` can also prompt on stderr and read stdin when a destination is unwritable (`63030e3a`).\n\nEvery background worker that calls `run_with_timeout` closes stdin with `< /dev/null`. Commands that can prompt also use their noninteractive or force option. Menu and scan traps save and restore the caller's traps; `lib/ui/menu_paginated.sh` is the reference.\n\n## 7. System command output is treated as an API\n\nmacOS command output is localized, drifts between releases, and can print errors where data is expected.\n\n- Force `LC_ALL=C` for parsed metric subprocesses (`51b352a2`, `fa05b8cc`, `4e83743b`).\n- Validate field shape before trusting it: absolute path, numeric value, expected key, or exact enum.\n- Keep `DTSDKBuild` build identifiers separate from `DTPlatformVersion` versions (`f0896d03`).\n- Reject PlistBuddy's missing-file prose as data.\n- Use stock macOS semantics when checking flags. BSD `grep -Z` means `--decompress`; a developer alias may hide that.\n- Prefer exit codes, plist keys, and machine-readable output over prose matching.\n- Join records on the machine identifier, never on a heading or display string. `simctl runtime list` titles each image with the image version (`iOS 26.4.1`) while `simctl list devices` groups under the runtime short name (`iOS 26.4`). A name join calls every point release an orphan and offers `simctl runtime delete` for a runtime its simulators still bind (`#1505`). `mdls -name kMDItemDisplayName` returns the on-disk file name, not Finder's localized name, so it always differed from `app_name` and always won the old selection, leaving the `CFBundleDisplayName` and `CFBundleName` branches unreachable and shipping folder names like `VideoFusion-macOS` (`#1520`).\n- `plutil -p` is documented by `man plutil` as unstable and not designed for machine parsing. macOS 15 prints a JSON boolean true as `1` while macOS 26 and 27 print `true`, so a filter pinned to one spelling silently cleaned nothing on the other: `#1512` was fixed and then regressed for eight days with CI green. Where a boolean prints as `1` nothing can tell it from the integer 1. The rendering is also depth-blind, so `{\"outer\":{\"inner\":true}}` offered a directory named `inner` for deletion, a wrong deletion rather than a missed one. That is why `tests/clean_app_caches.bats` runs on the macos-15/macos-26 compatibility job.\n\nUse `command grep` when flag behavior matters, because the interactive environment may alias it.\n\n## 15. Cancellation is local unless orchestration makes it sticky\n\nClassify a timeout at its source before propagating it. Signal-derived cancellation and safety-guard timeouts that the caller treats as cancellation must remain sticky across the remaining command. Status `124` alone does not define the scope. Read the probe's product contract first:\n\n| Contract | Typical `124` | Typical `>=128` |\n|---|---|---|\n| Safety guard, owner unknown, or cancellation the caller already established | Sticky stop; later mutation and later sections must not start | Sticky stop |\n| Per-item sizing or removal of an otherwise eligible candidate | Skip or fail that item; later items may continue (`#1374`, `#1384`, `#1576`) | Sticky stop |\n| Review-only or advisory listing that never deletes | Skip that advice; later cleanup sections continue (`#1571`) | Sticky stop |\n| Cooperative section budget | Stop the rest of that section, report partial, continue later sections (`#1513`) | Sticky stop |\n\n- In `bin/clean.sh`, a final delete guard returning `124` cancels later work, while an individual `safe_remove` timeout is a reported failed removal and later items may continue. Timed-out sizing contributes an unknown/partial total, not false reclaimed bytes. Large-files Mail / Downloads / Updates rows follow the review-only skip: a size timeout omits that row, and a signal still cancels (`#1576`, `#1344`).\n- The orphaned-runtime review is advisory. A cold `simctl` that returns `124` skips the review and must not cancel later `mo clean` sections. Skip the review entirely when the unavailable-simulator listing already timed out after its warm-up retry (`#1571`, `04e658d2`).\n- Orphan leftover probe and sizing timeouts (`mdfind` in `is_bundle_orphaned` / `is_claude_vm_bundle_orphaned` / `_container_stub_app_exists`, plus `get_path_size_kb` and candidate snapshots) fail closed for that item and later leftovers plus later `mo clean` sections continue. A `safe_clean_guarded` 124 at the sink stays sticky (`#1584`). Do not cache a timed-out Spotlight miss as \"not installed\".\n- Cloud & Office uses a cooperative section deadline: stop the remaining items in that section, preserve parent counters, report partial completion, and continue later sections. Do not restore its removed outer timeout worker or the file-backed deferred-family replay that existed only for that worker.\n- Purge discovery discards incomplete root scans and marks the run incomplete. An authored-content probe returning `2` keeps and visibly reports that candidate; deletion-phase activity or removal timeouts cancel the run. Unknown evidence never permits deletion.\n\nThe inverse defect is as common as a missed sticky cancel. Treating a review-only `124` as command-level cancellation skips every later section. Treating a safety-guard `124` as a local skip deletes with unknown evidence.\n\nOnce the caller establishes cancellation, carry that decision through every boundary:\n\n- A best-effort loop must check a pending cancellation before probing or registering the next candidate.\n- A helper that reports ordinary misses as success must return a pending cancellation before starting the next family.\n- Command substitution, subshells, and background workers do not mutate the parent's shell variables. Return the status, or use an explicit parent-readable channel, then record it again in the parent.\n- A parallel coordinator stops and reaps peer workers, preserves cancellation over ordinary failures, and prevents the next rendered section from starting.\n- Dry-run uses the same cancellation contract as real cleanup. A preview ledger is still downstream work and must not continue after safety evidence becomes unavailable.\n\nThe regression shape matters. Make the first candidate's safety guard return 124 or 130 and make the second candidate succeed if reached. Assert the exact top-level status plus the absence of a positive trace from the second probe, preview registration, sink, and later section. If both candidates independently time out, the test cannot prove cancellation was sticky. Separately preserve the removal-timeout and cooperative-section-budget continuation cases in `tests/clean_core.bats`.\n\nDo not hide a cancelled safety probe behind `|| true`, a warning plus `return 0`, or a worker-local exported variable. Those shapes turn a global stop into a local skip.\n\nName the question instead of spelling the numbers: `mole_rc_timeout` and `mole_rc_timeout_or_signal` in `lib/core/timeout.sh` are the only places that compare a status against 124, and `scripts/audit_timeout_status.py` rejects a raw comparison anywhere else in `check.sh`. A per-item size whose number only feeds totals goes through `mole_item_size_continues` in `lib/core/file_ops.sh`: a timeout or failure keeps the item with an unknown size and marks the freed total partial, a signal records the cancellation and stops. Choosing between those helpers is still the caller's contract decision from the table above; the helper only makes the choice visible in review.\n\nA third predicate, `mole_rc_signal`, is true only for a status above 127 that `kill -l` names. A mutating owner command can exit with an errno-derived code such as npm's 243, which is an ordinary failure, so the owner-command wrappers (`clean_tool_cache`, `bun pm cache rm`, `simctl delete unavailable`, `brew cleanup`, the tart prune and `go clean`) cancel only on it and skip that one command on 124 (`clean_tool_cache treats non-signal high owner statuses as failures (#1695)`, `clean_tool_cache keeps valid signal statuses sticky (#1695)`, `clean_dev_go treats an errno-derived go clean exit as a failure, not an interrupt`). A timed-out `go env` lookup skips only the Go caches because nothing was attempted yet, while the `gh` probe and clear, the automation-browser `ps` listing and the clang cache resolver stay sticky (`clean_dev_cloud stops on GitHub CLI probe or clear cancellation`).\n\n## Focused pitfalls\n\n- **A PTY read loop must end on an empty read, an `OSError`, or a deadline.** After the child exits, a macOS pty master can stay readable and return `b''` forever, so a `while select(...)` drain with no exit spins until the job times out; Linux raises `EIO` instead. `tests/sudo_adoption_pty.py` hung this way on #1747 (fixed in the squash `eb531ba2`); the other `tests/*_pty.py` loops are deadline-bounded.\n\n- **Launcher signal stories behind the `lib/manage/update.sh` rules in `.claude/rules/install-update.md`**: a tty7 pane can inherit the daemon's blocked TERM mask, so a child launched without unblocking it left uninstall waiting forever for its spinner and made Ctrl-C cleanup hang. Bash prints a signalled child's job-status line (`Killed: 9 ... perl ...`) from whichever command finishes first after the child dies, so a runner that stalls the router for a few milliseconds leaked it at random until the launch, readiness wait and `wait` shared one `2> /dev/null` group. And Bash 3.2 can discard an INT received inside a USR1 trap entered from the `wait` builtin, which is why the readiness notification settles before `wait` starts.\n- **A `find` primary newer than the oldest supported macOS needs a probe, and dataless folders must not be listed**: listing a dataless CloudStorage folder makes File Provider fetch it over the network (`77050616`), an older `find` rejects `-flags +dataless` and fails the whole walk (`8dccb160`), and the capability probe itself must pass signals through (`df2f5e06`). Probe once per process: unknown means walk without the primary, a signal cancels, and never descend into a dataless folder.\n- **`BASH_SOURCE` / `$0` change meaning when a function moves files**: they name the file the code lives in, so copy-paste extraction is not behavior-preserving. `mole` captures `MOLE_ENTRY_SCRIPT=\"${BASH_SOURCE[0]}\"` before sourcing anything, and update code reads that stable entrypoint. Before extracting a function, grep it for `BASH_SOURCE`, `$0`, and `FUNCNAME`. Regression coverage lives in `tests/update.bats`.\n- **Bats heredocs share stdin with `read -n1`**: an inner `read -r -s -n1` can consume the next byte of the heredoc source. Redirect the function under test from `/dev/null`.\n- **macOS `script(1)` rejects socket-backed stdin**: PTY test helpers must redirect the wrapper's stdin from `/dev/null` or `script` can fail before starting the child. Capability probes use `/usr/bin/true`, not the absent `/bin/true`; inspect the actual failure before classifying it as unavailable TTY support, or live terminal tests silently skip.\n- **`run_with_timeout` execs the binary and bypasses shell-function mocks**: tests must use a PATH stub directory for commands such as `osascript`.\n- **CI runners may lack `/Library/PrivilegedHelperTools`**: orphan-service tests should exercise `/Library/LaunchDaemons`, which exists on GitHub macOS runners.\n- **A test can pass vacuously after an early return**: `MOLE_TEST_MODE=1` can leave `$output` empty, and a final negative assertion then passes. End assertions with `|| return 1`, override test mode when the body must run, and add a positive control proving the output path executed. In an inner heredoc script use `|| exit 1`. Confirm the bracket behavior with a minimal repro when it matters: a non-final `[[ ]]` can be swallowed while `[ ]` still gates.\n- **A large payload piped into `grep -q` leaks a broken-pipe line into user output**: `grep -q` exits on its first match, and the `printf` still writing into that closed pipe takes SIGPIPE, which bash reports as `printf: write error: Broken pipe` on stderr. The live-cache owner probe fed the whole process table that way and the message landed mid-run in `mo clean`. Pass the data by here-string instead. Small variables holding a few lines of command output finish in one write and are unaffected, so the existing `echo \"$var\" | grep -q` sites are fine.\n- **Normalize with `10#` before any numeric comparison**: `[[ a -le b ]]` evaluates arithmetically, so a leading zero is read as octal and `0123` ranks below `100`, while `08` and `09` are not valid octal at all and abort the test with a bash error on stderr. A `^[0-9]+$` guard does not prevent either. Codex build numbers compared this way could have called a newer staged build superseded.\n- **`SECONDS` advances in whole seconds, so a 1s budget is not a second**: a deadline built as `SECONDS + 1` really means \"until the next second boundary\" and can collapse to almost nothing, making `_mole_timeout_with_deadline` return 124 before the command ever runs. Every timeout constant is 2 or more for this reason. A test of clamp arithmetic invokes the helper directly after resetting `SECONDS`; an exact value asserted across command substitution still races the fork on a loaded runner even with a larger window. When the fork is the behavior under test, assert a safe range or property instead of one remaining-second literal.\n- **BSD grep has no GNU null-output `-Z` contract**: on stock macOS it means `--decompress`. Enumerate files with `find ... -print0`, then probe each file with `grep -qF`.\n- **PlistBuddy reports missing-file creation on stdout**: redirect both stdout and stderr when creating plist fixtures so diagnostic prose does not pollute Bats `$output`.\n- **macOS 14 Bash can fire errexit through an if-guarded exported mock**: a failing exported `sudo` function inside an `if fn; then` path may terminate a `set -e` script on that runner while passing locally. Around the first sudo probe, disable errexit only for the probe and restore it before validation-gate returns. CI-only failures must print exit status, output, and a mock call trace rather than a bare return-code assertion.\n- **Expected capability absence is not probe failure**: keep `ready`, `not applicable`, and `misconfigured or unknown` distinct. A Mac with standalone Command Line Tools and no Xcode app has no simulator surface, so missing `simctl` is a quiet skip; an explicit invalid `DEVELOPER_DIR` remains actionable. Tests prove the not-applicable path does not invoke the unavailable owner command or emit warning activity.\n- **Fixed-width prefixes may have a free-form remainder**: after validating numeric and enum columns, join the remaining fields instead of assigning semantic meaning to one whitespace token. Darwin process `comm` values can contain spaces; trimming them to the first word destroyed zombie-parent attribution for helper apps. Keep primary and fallback formats separate when their fields carry different semantics.\n\n<!-- File: .claude/skills/bugs/SKILL.md (Tokens: ~1800 | Category: anthropic-skill) -->\n---\nname: bugs\ndescription: \"Mole incident catalog for cleanup safety, bounded probes, cancellation, dry-run parity, and actionable gates. Use for a Mole safety-sensitive diff; not for docs, release notes, or generic review.\"\nmetadata:\n  internal: true\n---\n\n# Mole bug patterns\n\nGeneric review belongs to Waza `check`; root-cause investigation of a live failure belongs to `hunt`.\n\n## Route before loading details\n\nChoose only the reference families touched by the evidence. A whole-project audit should classify surfaces first instead of loading every incident narrative.\n\n| # | Recurring shape | First probe | Read |\n|---|---|---|---|\n| 1 | Deletion candidate built from a weak name signal | Inspect name, bundle-id, fallback globs, and failed-owner shortcuts | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 2 | Existence or idleness decided by one probe | Enumerate every legitimate location and unknown outcome | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 3 | Guard present on only one branch | Diff dry-run, real, direct, fallback, and final-sink paths | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 4 | Unbounded external command | Count producer, consumer, inner-loop, and action bounds | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 5 | Bash 3.2, errexit, or pipefail trap | Check empty arrays, `fn || handler`, and optional actions | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 6 | TTY, stdin, or process-group theft | Inspect background workers and commands that may prompt | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 7 | System output parsed as a stable API | Force locale, validate shape, and join on identifiers not headings | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 8 | Persisted derived data outlives its algorithm | Trace schema, TTL, evidence fingerprint, and mutations | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 9 | Two paths compute one number differently | Find every producer and choose one definition | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 10 | Slow work looks frozen | Find operations over roughly one second outside feedback | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 11 | Regression test cannot fail | Prove positive control and pre-fix red state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 12 | Gate cannot explain why it refused | Map each reason to one cause and a next action that fails on the broken state | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 13 | Mutation target is also accepted as a discovery container | Compare the recursive scan-root namespace with every purge target basename | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 14 | Owner metadata is treated as a complete, atomic inventory | Identify who writes it, whether absence is authoritative, and what locks mutation | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n| 15 | Cancellation stops one helper but later work continues | Classify each 124 as skip, section budget, or sticky cancel before tracing callers | [Bounds, Shell, TTY, and parsing](references/shell-and-test-pitfalls.md) |\n| 16 | Async or cached data has no generation or freshness contract | Bind results to a request epoch and keep each sample's time, stale, and completeness fields together | [State, accounting, and progress](references/state-accounting-and-progress.md) |\n| 17 | Publication gate trusts ambiguous or pre-existing state | Require exact source/tag equality, one generated target, and an expected-absence ref lease | [Test validity and refusal diagnostics](references/test-validity-and-refusal-diagnostics.md) |\n| 18 | A sandbox well-known path is treated as app-private leftovers | Resolve Data/Downloads, Desktop, Pictures, Music, and Movies physically against $HOME | [Deletion evidence and final sink](references/deletion-evidence-and-final-sink.md) |\n\nAGENTS.md and the path-scoped `.claude/rules/` files keep each rule stated fully enough to obey plus its test anchor; the incident story lives in these references or in `release-flow`, and the rule bullet points to the section that holds it. Moving a story here is a merge, never the deletion of a rule that still constrains behavior.\n\n## Trace the complete mutation lifecycle\n\nFor cleanup, purge, optimize, analyze deletion, or uninstall work, review the complete chain rather than the reported branch:\n\n```text\ndiscover or plan\n  -> cheap irreversible filters\n  -> owner and open-handle probes\n  -> size or metadata work\n  -> final owner re-probe\n  -> parent and target identity rebind\n  -> deletion or Trash sink\n  -> accounting, cancellation, and user output\n```\n\nAt every transition, answer:\n\n- Does live or unknown state fail closed?\n- Are timeouts classified by probe, sizing, removal, or section scope, with unknown evidence refusing deletion and cancellation stopping later mutation?\n- Are probe and sink bound to the same physical parent and target?\n- Do dry-run and real mode start from the same eligible plan without reusing stale authorization?\n- Are cheap missing, protected, whitelisted, and compiled-model filters ahead of recursive probes?\n- Does one cumulative deadline cover the dynamic scan scope, with checkpoints in nested loops?\n- Do refused, filtered, timed-out, or failed items stay out of cleaned counts and reclaimed bytes?\n- Can large candidates avoid per-item size work without making the reported total false?\n\nDo not trade final-sink rebinding or fail-closed owner checks for speed. Optimize absent targets, duplicated discovery probes, report-only work, and wrong-scope scans first.\n\n## Working contract\n\n- Before proposing a remedy, apply the Product Decision Filter in `AGENTS.md`: a new flag, environment variable, or visible retry message is a product change, even when the underlying fix is a safety improvement. Use the existing state/accounting reference when deciding what belongs in the normal summary.\n- A sandbox well-known directory is user data until physical resolution proves otherwise. `Data/Downloads` is usually `~/Downloads` under another name.\n- A refusal next step that succeeds on the broken state is not a next step. Do not relax the gate to make the message nicer.\n- Sweep siblings by call-site shape, not filename or helper name. Report `checked N / defective M / not applicable K`.\n- A recurring fix ships with a regression or source invariant that fails against the pre-fix code.\n- Treat tests as production consumers only after proving the production helper ran. Negative assertions require a positive trace.\n- A cancellation regression makes the next candidate otherwise eligible, then proves its probe and sink never run. Making every candidate fail for the same reason is a false sticky-cancellation test.\n- Absence-sensitive tests use an isolated `HOME` or fixture root; a shared `setup_file` home is not isolation.\n- Reproduce CI through `MOLE_TEST_NO_AUTH=1 ./scripts/test.sh` when possible. If invoking Bats directly with jobs, preserve `--no-parallelize-within-files`; files share state and raw `bats --jobs 6 file.bats` changes semantics.\n- Treat specialist or AI reports as leads. Read the implementation, callers, fallback branches, and final sink yourself.\n\n## Verification bar\n\nUse the hotspot commands in the matching `.claude/rules/` file (routed from `AGENTS.md`); do not guess a narrower verifier. A typical Shell safety change finishes with:\n\n```bash\n./scripts/check.sh --format\nMOLE_TEST_NO_AUTH=1 bats tests/<area>.bats\nMOLE_TEST_NO_AUTH=1 ./scripts/test.sh\ngo test ./...\nMOLE_TEST_NO_AUTH=1 MOLE_DRY_RUN=1 ./mole clean --dry-run\n```\n\nNever infer a production defect from a function name, comment, string, fixture, or `_test.go` match. Confirm the live call path and verify red-green before reporting the class fixed.\n\n<!-- File: .claude/skills/release-flow/SKILL.md (Tokens: ~2521 | Category: anthropic-skill) -->\n---\nname: release-flow\ndescription: \"Mole CLI release runbook for distribution channels, pre-flight checks, capital-V tags, artifacts, and curated-note handoff. Use when assessing or executing a Mole release. Not for release-note copy alone or ordinary code review.\"\nmetadata:\n  internal: true\n---\n\n# Mole CLI Release Flow\n\nTag-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 opens a Homebrew core PR.\n\n## Distribution channels\n\n| Channel | What ships | Trigger | Automation |\n|---|---|---|---|\n| Nightly (`mo update --nightly`) | `main` HEAD via `install.sh` | Any commit pushed to `main` | Automatic; no tag or release involved |\n| GitHub stable release | amd64/arm64 binaries + `SHA256SUMS` | Push a capital-`V` tag | `release.yml` builds and creates the release; curated notes are a manual follow-up |\n| Homebrew core | Version-bump PR to `Homebrew/homebrew-core` | Same `V*` tag workflow | Automatic PR; merge timing is upstream's |\n| Nix flake | Source-built macOS package from the selected Git ref | `main` follows development; a release tag pins that release | Nix manages install, upgrade and removal; no separate registry publish |\n\nAt the start of any release-flavored task, restate which channels this run will touch and which it will not, and confirm with the maintainer before acting. Channel scope is specified by the maintainer, never inferred.\n\n## Pre-flight checklist\n\nResolve the latest published stable tag from GitHub before choosing the version or review range. Reconcile handoff claims against the current branch, worktree, and remote SHA; an earlier report of uncommitted work may describe commits that have already landed. Review all changes since that stable tag, not just the final fix batch.\n\n1. `grep '^VERSION=' mole` matches the new version.\n2. `SECURITY_AUDIT.md` opening line reflects the new version and date, and its CI coverage list matches the matrix in `.github/workflows/test.yml`.\n3. `git status -s` is empty or only contains intentionally staged release work.\n4. `git log origin/main..HEAD --oneline` shows only commits you intend to ship.\n5. `./scripts/check.sh --format` and `TERM=xterm-256color MOLE_TEST_NO_AUTH=1 MOLE_TEST_JOBS=2 BATS_FORMATTER=tap ./scripts/test.sh` both exit 0.\n6. `go test ./...` and `make build` both pass.\n7. `scripts/e2e_macos.py` exits 0 on a real Mac at the candidate commit. It installs IINA from its verified GitHub DMG, launches it, uninstalls it with every personal-data row selected through a real terminal, lists the DMG in `mo installer --dry-run`, deletes it from `mo analyze ~/Downloads`, and runs every other command read-only, so it changes only what it installed and never asks for a password. It refuses to start when the Mac already has IINA or any of its data. A failed run lists what it left behind; move those to the Trash before rerunning. Transcripts and `report.json` land in `~/Library/Logs/mole-e2e/<run>/`.\n\nUse the Go version declared in `go.mod` for local release builds, matching `actions/setup-go` in CI, then run `scripts/check_release_minos.sh` on both architectures. A newer local Go can raise the minimum macOS version even with `CGO_ENABLED=0`; during V1.54.0 verification, Go 1.27 produced macOS 13 binaries while the declared Go 1.25 toolchain preserved macOS 12. Rebuild with the declared toolchain instead of relaxing the minimum-OS gate. Check the `make release-amd64 release-arm64` outputs, not `make build`: the local build links `status` with cgo against the host SDK, so its `status-go` reports the host macOS as its minimum and says nothing about the release binaries.\n\nCapture the test runner's exit status and structured summary, with skipped tests reported separately. After pushing the candidate commit, wait for its required Check, Validation, CodeQL, and Nix workflows to finish successfully before tagging that exact SHA. Nix must verify both Darwin architectures and the installed/profile lifecycle. A cancelled run or a green check on another commit is not release evidence.\n\n## Tag and publish\n\n```bash\ngit push origin main\ngit tag V<version>          # capital V; release workflow ignores lowercase v\ngit push origin V<version>\n```\n\nWait for the workflow to finish. The workflow creates the release with assets but `generate_release_notes: false`, so notes must be added in a follow-up step.\n\nAfter the workflow finishes, verify the release assets before announcing anything: `gh release view V<version> --json assets --jq '.assets[].name'` must list all four `analyze-`/`status-darwin-{amd64,arm64}` binaries, both `binaries-darwin-*.tar.gz` Homebrew tarballs, AND `SHA256SUMS`. Install verification is fail-closed, so a release without a readable `SHA256SUMS` asset makes every install and `mo update` abort by design; a missing checksums file is a release blocker, not a cosmetic gap.\n\nDownload all seven assets. Verify the six payload checksums, attestations for all seven tied to the release tag, exact source commit, and `.github/workflows/release.yml`, and each archive's two expected binary members against the raw binary assets. Check Mach-O architecture and minimum macOS version on the downloaded binaries, then run the native architecture's Analyze and Status smoke probes. Successful local builds do not prove the public package contains those bytes.\n\nThen run a **script self-update smoke** before publishing notes or announcing: install the previous stable release through the script channel, run `mo update`, and confirm `mo --version` prints the candidate version. Script-installed clients execute the new tag's `install.sh`, so this is the only gate that exercises their real upgrade path; the pre-flight suite cannot cover it before the release exists. Homebrew is a separate downstream gate: verify it only after the core formula has updated, and never treat a script-channel smoke as proof that Homebrew is ready. If the script smoke fails, pull the release (see the pulling-and-re-releasing pitfall) before anyone is told to update.\n\nUse a fresh archive of the previous tag so ignored local `bin/*` builds cannot contaminate the old installation. Place the isolated prefix and config under a physical user-owned directory; `/tmp` or `/var` aliases and writable ancestors can correctly fail installer path checks. Isolate HOME, config/cache paths, and PATH, block host `brew` and `sudo`, and set `MOLE_TEST_NO_AUTH=1`. Verify `config/install_channel` retains `CHANNEL=stable`, `config/bin/{analyze,status}-go` match the verified release assets, changed installed shell sources match the tag, and a second update reports the current version. Keep the user's existing installation unchanged.\n\n## Apply curated release notes\n\nThe curated-notes flow (bilingual format, `gh release edit` instead of `create`, thanks block, and the six-reaction set) is owned by `.claude/skills/release-notes/SKILL.md`. `.agents/skills/release-notes` is a symlink to that canonical directory for Codex discovery, and its Codex-only invocation policy lives in `agents/openai.yaml`; do not replace the symlink with a copied mirror. Follow that skill; do not duplicate its format details here. Version, codename, and emoji go only in the release title; the body h1 is just `Mole`.\n\nAfter applying notes and the standard reactions through that skill, read back the published title, full body, and all six reactions. Report GitHub Stable and Homebrew availability separately; a successfully opened core PR still needs upstream tests and merge.\n\n- Issues whose reporter already verified the fix on nightly get no \"now in stable\" follow-up after the release, and none is proposed.\n\n## Release-notes craft\n\nFormat rules (impact ordering, command existence checks, icon semantics, no em dash, no inline PR refs) live in `.claude/skills/release-notes/SKILL.md` under \"Format rules\". Keep that skill as the single source of truth for notes formatting.\n\n## Release-only pitfalls\n\n- **Tag prefix is case-sensitive**: `release.yml` filters on `'V*'`. A lowercase `v1.38.0` tag will not trigger the workflow.\n- **Old clients fetch `install.sh` from the release tag, not from main**: a self-updating Mole downloads `raw.githubusercontent.com/tw93/mole/V<tag>/install.sh`, and tag content is immutable. An installer/updater bug therefore reaches existing stable users only through a new tag; fixing main changes Nightly but does not repair an already published stable updater.\n- **Never rewrite history an already published tag can reach**: the rule and the pre-rewrite tag audit are in `AGENTS.md` (Release). The incident behind it: on 2026-09-17, four days after V1.54.0 shipped, a `git filter-branch --msg-filter` run stripped a `Co-authored-by: Cursor` trailer from a commit dated 2026-05-06 that had 956 descendants. `--tag-name-filter cat` carried the tags onto the rebuilt commits, the release commit was rebuilt, and `brew upgrade mole` has failed the source checksum for every Intel user since, because Homebrew dropped Intel bottles so they all build from source (#1591). The enumeration was run afterwards: 25 published tags were reachable from that rewrite, so 25 source-tarball checksums moved, not one. Only V1.54.0 broke anything, because homebrew-core pins the checksum of the formula's current version alone, and `install.sh` anchors on the release assets' `SHA256SUMS` rather than on the tag archive it downloads. Any external consumer pinning an older tag's tarball is outside what can be checked from here. Recovery on a live release is a one-line `sha256` PR to `Homebrew/homebrew-core` with the current tarball's hash, and it takes two things that are easy to miss. The PR body must carry Homebrew's own template or a bot closes it within seconds as AI-written, editing it in reopens the same PR and opening a second one is explicitly refused. And a moved checksum is read as a possible supply-chain compromise, so a maintainer will hold the merge until the upstream author confirms in that thread that the change was benign; answer with the two commits' identical tree and the archive's pax header, which they can verify without trusting you, not with a promise. Check for an existing PR before opening one: the community usually files it first. Bottles are unaffected because BrewTestBot built them from the original download.\n- **Pulling and re-releasing a version**: `gh release delete V<old> --cleanup-tag` removes the release and remote tag. Delete the local tag, close the superseded Homebrew core PR with a one-line supersede comment before pushing the replacement tag (`release.yml` refuses to overwrite an existing `mole-<version>` fork branch and reuses, rather than recreates, an open PR for the same head), then bump `VERSION` and `SECURITY_AUDIT.md`, commit `release: V<new>`, tag, and run the normal publish flow. The Homebrew core PR regenerates on the new tag.\n\nWhen release work touches Shell code or tests, read `.claude/skills/bugs/references/shell-and-test-pitfalls.md` for Bash 3.2 arrays, heredoc input, mock bypasses, and CI-runner quirks.\n\n<!-- File: .claude/skills/release-notes/agents/openai.yaml (Tokens: ~11 | Category: anthropic-skill) -->\npolicy:\n  allow_implicit_invocation: false\n\n<!-- File: .claude/skills/release-notes/SKILL.md (Tokens: ~1807 | Category: anthropic-skill) -->\n---\nname: release-notes\ndescription: Publish curated release notes for an existing Mole `V<version>` tag, including bilingual format, `gh release edit`, contributor thanks, and reactions. Use only when explicitly asked to edit or publish Mole release notes. Not for release readiness, tagging, or code review.\ndisable-model-invocation: true\nmetadata:\n  internal: true\n---\n\n# Mole release notes\n\nThis skill drives the curated-notes step that runs **after** `release.yml` has finished. The workflow creates the GitHub Release with assets but with `generate_release_notes: false`, so notes must be added in a follow-up `gh release edit` (never `gh release create`, the release already exists, and `create` will conflict).\n\n## Inputs to gather\n\nBefore drafting, confirm:\n\n1. **Version**. Capital `V`, e.g. `V1.38.0`. Lowercase `v` does not trigger the workflow and may indicate a botched tag.\n2. **CodeName + emoji**. Ask the user. The title format is `V<version> <CodeName> <emoji>`.\n3. **Release commit range**. `git log <previous-tag>..V<version> --oneline` gives the raw material.\n4. **User-visible behavior changes**. Scan the full commit message bodies (not just subjects) for narrowed detection, removed features, or controlled regressions. These belong in notes even when they are not bug-fix-shaped, because users will encounter the changed boundary in production.\n5. **Issue reporters and PR contributors in this cycle**. Derive them from what the release actually contains, never from a date window of closed issues: `git log --format='%an' <prev>..<tag>` for contributors, and the issue numbers the range's own commit messages cite for reporters, checking each is `state_reason: completed` and was fixed here rather than merely referenced as background. A closing window silently pulls in `not_planned` issues and everything the PREVIOUS release fixed, which is how V1.55.0 nearly shipped 30 names where 8 were real. Keep it short, for example `Issue reporters and PR contributors this cycle: @a · @b.` Exclude `tw93`, `youxi798` and bots.\n6. **Verify release exists**. `gh release view V<version> --repo tw93/Mole --json id,name` should return non-empty. If it doesn't, the workflow hasn't finished, wait, don't `gh release create`.\n\n## Pre-flight (published-tag evidence)\n\nUse the target tag's exact commit and the completed checks and public-asset evidence from [release-flow](../release-flow/SKILL.md). Reuse that evidence when it still matches the immutable tag; rerun only a missing or failed gate. A build of a newer working tree does not verify the release being described. The full build, test, asset, and script-update gates stay owned by `release-flow` rather than a second checklist here.\n\nIf evidence is missing, complete the applicable release-flow gate before publishing notes. Keep draft work read-only; an existing release object alone is not proof its assets or upgrade path passed.\n\n## Format\n\nStrictly follow the current compact release shape. Read the previous published stable release before the target tag as the live format reference. Exclude the candidate release: the workflow may already have made it the latest release with an empty body. Resolve the previous tag first, then read it with `gh release view <previous-tag> --repo tw93/Mole --json tagName,body`.\n\nStructure:\n\n```\n<div align=\"center\">\n  <img src=\"https://cdn.tw93.fun/pic/cole.png\" alt=\"Mole Logo\" width=\"120\" height=\"120\" style=\"border-radius:50%\" />\n  <h1 style=\"margin: 12px 0 6px;\">Mole</h1>\n  <p><em>Deep clean and optimize your Mac.</em></p>\n</div>\n\n### Changelog\n\n1. **<English headline>**: <one-sentence English elaboration>.\n2. ...\n\n### 更新日志\n\n1. **<中文 headline>**：<一句中文说明>。\n2. ...\n\n### Thanks\n\nIssue reporters and PR contributors this cycle: @handle1 · @handle2.\n\n### Mole Mac App\n\nPrefer a GUI? [Mole Mac App](https://mole.fit/) brings cleaning, app management, maintenance, disk analysis, and live system status into one native app, with review before deletion and a customizable menu bar HUD. It is $19 once, with lifetime updates and a 14-day refund. [Download and try it](https://mole.fit/download). The CLI stays free and open source.\n```\n\nNo `---` separators between sections, and no trailing repository link; the published pages end on the Mole Mac App line.\n\n### Format rules (all are documented bugs that have shipped before)\n\n- **Body h1 is just `Mole`**. Version, codename, and emoji live only in the `--title` argument (`V<version> <CodeName> <emoji>`); repeating them in the body header is redundant and has been explicitly rejected before.\n- **No em dash anywhere**. Use commas, periods, colons, semicolons, or parentheses.\n- **No sponsor list by default**. The current public release style thanks issue reporters and PR contributors for this cycle only.\n- **No emoji except the version emoji in the release title**. Body section headers stay plain, including `### Thanks` (the old `Thanks 💖` header is gone from the published pages).\n- **No inline PR refs, no inline `@handle` thanks**. PRs and people belong in the dedicated Thanks block only.\n- **English block first, 中文 block second**. Same numbered order in both blocks. Same number of items.\n- **Order items by user-perceived impact, not commit chronology**. Headline change first; internal safety hardening, performance, and bug fixes follow.\n- **Do not describe overview icons that no longer exist**. Analyze overview rows are text-only because emoji width and baselines vary across terminals. If icons return later, they must not imply that user data such as iOS Backups, Xcode Archives, or Old Downloads is safe to delete.\n- **Verify every command mentioned in the notes actually exists in HEAD**. The deleted `mo check` / `mo doctor` commands nearly shipped in notes as a \"feature\" after they were removed.\n- **An incident or troubleshooting note is one sentence of symptom plus one command**. No cause taxonomy, no command per branch; the user needs the one line that gets them unstuck. Match the previous release's language treatment for that note: if the last release carried it in one language, do not add a second.\n- **Keep the Mole Mac App cross-link to one restrained, fact-backed paragraph**. Validate the product scope, price, updates, refund window, and download URL against the current homepage before publishing.\n\n## Publish\n\nOnce the user approves the draft:\n\n```bash\ngh release edit V<version> --repo tw93/Mole \\\n  --title \"V<version> <CodeName> <emoji>\" \\\n  --notes-file <path-to-draft>\n```\n\n**Never** `gh release create`, it conflicts with the release the workflow already made.\n\nThen add the six reactions with this skill's helper (path is relative to this SKILL.md, not the repo-root `scripts/`): `bash \"$(dirname <this SKILL.md>)/scripts/post-reactions.sh\" V<version>`.\n\n## After publish\n\n- `gh release view V<version> --repo tw93/Mole --web` (open in browser) so the user can eyeball it.\n- Remind the user: the Homebrew Core PR is workflow-driven and should already be in flight; do not re-run it manually unless the workflow log shows a failure.\n\n## When NOT to act\n\nThis skill is user-invocable only. It must not run unprompted:\n\n- If the user mentions release notes in passing, draft only; do not call `gh release edit`.\n- If `gh release view` shows the release does not exist yet, wait for the workflow; do not create a competing release manually.\n- If the user has not given an explicit \"publish\" / \"提交\" signal, stop after the draft.\n\n## Helper script\n\n`scripts/post-reactions.sh <tag>` lives next to this SKILL.md and adds the six reactions (`+1`, `laugh`, `hooray`, `heart`, `rocket`, `eyes`) to the release.\n\n<!-- File: .cursor/rules/mole-test-safety.mdc (Tokens: ~142 | Category: cursor-rule) -->\n---\ndescription: Mole test safety and verification defaults\nalwaysApply: true\n---\n\n# Mole Test Safety\n\n- Default test and verification commands must run with `MOLE_TEST_NO_AUTH=1` or through `scripts/test.sh`, which sets it automatically.\n- Tests must never trigger real `sudo`, Touch ID, password prompts, AppleScript permission dialogs, or system service changes.\n- Any new direct use of `sudo`, `osascript`, or `launchctl` must either be guarded by `MOLE_TEST_MODE` / `MOLE_TEST_NO_AUTH` or be fully mocked in the test path.\n- Prefer focused regression tests first, then run full no-auth verification before release-sensitive changes.\n\n<!-- File: .claude/skills/release-notes/scripts/post-reactions.sh (Tokens: ~282 | Category: claude-rule) -->\n#!/bin/bash\n# Add the standard six reactions (+1, laugh, hooray, heart, rocket, eyes) to a\n# tw93/Mole release. Usage: post-reactions.sh V<version>\n\nset -euo pipefail\n\nTAG=\"${1:-}\"\nif [[ -z \"$TAG\" ]]; then\n    echo \"Usage: $0 V<version>\" >&2\n    exit 1\nfi\n\nif [[ \"$TAG\" != V* ]]; then\n    echo \"Tag must start with capital V (release.yml ignores lowercase v): $TAG\" >&2\n    exit 1\nfi\n\nif ! command -v gh > /dev/null 2>&1; then\n    echo \"gh CLI is required\" >&2\n    exit 1\nfi\n\nRELEASE_ID=$(gh api \"repos/tw93/Mole/releases/tags/$TAG\" --jq '.id')\nif [[ -z \"$RELEASE_ID\" ]]; then\n    echo \"Release not found for tag: $TAG\" >&2\n    exit 1\nfi\n\nfor r in +1 laugh hooray heart rocket eyes; do\n    gh api \"repos/tw93/Mole/releases/$RELEASE_ID/reactions\" \\\n        -X POST -f content=\"$r\" --silent\ndone\n\necho \"Posted 6 reactions to $TAG (release id $RELEASE_ID)\"\n\n<!-- File: .claude/agents/bash32-portability-reviewer.md (Tokens: ~626 | Category: subagent-persona) -->\n---\nname: bash32-portability-reviewer\ndescription: Reviews Mole shell and Bats diffs against the current macOS Bash 3.2, errexit, timeout, TTY, BSD-tool, and CI-runner pitfalls recorded in .claude/skills/bugs/references/shell-and-test-pitfalls.md. Use after changes under mole, install.sh, bin/**, lib/**, scripts/**, or tests/*.bats.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole shell portability reviewer\n\nRead the current `.claude/skills/bugs/references/shell-and-test-pitfalls.md`\nbefore every review. It is the source of truth and grows when a new incident\nbecomes a stable invariant. Do not rely on a fixed count or a copied historical\nlist in this profile.\n\nYou read diffs, production context, and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base. Restrict findings to `mole`,\n   `install.sh`, `bin/**`, `lib/**`, `scripts/**`, and `tests/*.bats`.\n2. Turn every current pitfall bullet in that reference into a check against the\n   touched code. The list below is a search aid, not a replacement for it:\n   - moved functions using `BASH_SOURCE`, `$0`, or `FUNCNAME`;\n   - `du -s` calls outside `run_with_timeout`;\n   - possibly empty array expansion under `set -u`;\n   - functions called through `if` or `||` that rely on errexit internally;\n   - `[[ ... ]] && cmd` in exit-code-sensitive blocks;\n   - heredoc-driven tests of `read -n1` without redirected stdin;\n   - shell-function mocks hidden by timeout wrappers that exec a real binary;\n   - GNU-only command flags or CI fixtures that assume local macOS directories;\n   - PlistBuddy stdout leaking into assertions;\n   - tests that can pass on empty output or an early return;\n   - macOS-runner-specific errexit behavior around failing command mocks.\n3. Read enough surrounding code to prove the pattern actually fires. A grep hit\n   alone is not a finding.\n4. Check that the regression test reaches the intended branch and that every\n   assertion failure propagates. If local and CI behavior differ, require a\n   failure trace that exposes status, output, and mock calls.\n\n## Output\n\nFor every confirmed problem:\n\n```\nLANDMINE: <file>:<line> - <problem>\n  Pattern: <matched code>\n  Why it fires here: <context>\n  Fix: <one concrete change>\n```\n\nUse `UNVERIFIED: <file>:<line> - <missing evidence>` when context cannot resolve\na real risk. End with `VERDICT: <N> landmines, fix before merge` when findings\nexist, otherwise `VERDICT: no landmines found`. With no findings or unverified\nitems, output only `VERDICT: no landmines found`.\n\n<!-- File: .claude/agents/safety-reviewer.md (Tokens: ~840 | Category: subagent-persona) -->\n---\nname: safety-reviewer\ndescription: Audits Mole changes for destructive-action regressions across deletion, app protection, privilege boundaries, dry-run behavior, operation logging, package-manager cleanup, and exact leftover matching. Use before merging changes under lib/clean/**, lib/uninstall/**, lib/manage/**, bin/clean.sh, bin/purge.sh, bin/uninstall.sh, bin/installer.sh, lib/core/file_ops.sh, or lib/core/app_protection*.sh.\ntools: Read, Grep, Glob, Bash\n---\n\n# Mole destructive-action safety reviewer\n\nRead `AGENTS.md` sections \"Critical Safety Rules\", \"Working Rules\", and\n\"Verification\", every `.claude/rules/*.md` file whose `paths:` match the diff\n(its routing table names them), plus `docs/SECURITY_DESIGN.md`, before judging\nthe diff. Those files are the current safety contract. This profile defines the\nreview method and output shape only; it must not become a copied policy list.\n\nYou read code and tests.\n\n## Review method\n\n1. Compare the full diff with its branch base and read the issue or PR scope.\n   A request for one leftover path is not permission to add a broader matcher.\n2. Mark every changed destructive sink and every new path source. Pay special\n   attention to `find_app_files`, `mole_delete`, `remove_file_list`, container\n   traversal, Group Containers, bundle-prefix matchers, and recursive `find`\n   branches that eventually delete.\n3. Audit each branch independently, including fallbacks. For every candidate,\n   prove exact app or bundle evidence, protected-path coverage, preview or\n   confirmation, dry-run behavior, operation logging, and the final deletion\n   helper. A safe primary branch does not make a broad fallback safe.\n4. Treat raw removal outside `lib/core/file_ops.sh` as P0 unless the call site\n   has a narrow `# SAFE:` exception for an already verified exact path and a\n   regression test that proves why the shared funnel cannot be used. Never\n   generalize one exception into a second deletion API.\n5. For new `sudo`, `osascript`, `launchctl`, package-manager, or service teardown\n   calls, verify test/auth guards, non-interactive test behavior, exact preview,\n   and failure propagation. Typed password input must not be mistaken for skip.\n6. For uninstall teardown, prove every route passes the shared-bundle-id sibling\n   guard, including volume copies, inverse names, and shared identities.\n7. Read enough surrounding production code to follow helper calls to their final\n   sink. Then map the change to the exact hotspot commands in the matching\n   `.claude/rules/` file and \"Verification\"; missing safety coverage is a finding.\n\n## Severity\n\n- **P0**: a path can escape its intended target, protection/confirmation/dry-run\n  is bypassed, a destructive failure can be reported as success, or a privileged\n  action can execute during ordinary verification.\n- **P1**: matching is broader than exact evidence, a teardown route bypasses a\n  shared guard, active user/developer state can be removed, package-manager\n  candidates are not previewed, or a safety regression lacks a direct test.\n- **P2**: the behavior is bounded but the documented targeted verification was\n  not run or the failure output is not actionable.\n\nDo not flag style, speculative refactors, or test-only panic/expect patterns.\nIf a helper or guard cannot be traced, report it as `UNVERIFIED` rather than\nassuming it is safe.\n\n## Output\n\nOrder findings by severity:\n\n```\nP0: <file>:<line> - <problem>\n  Why unsafe: <broken invariant>\n  Fix: <one concrete change>\n```\n\nUse the same shape for P1 and P2. End with `VERDICT: changes required` when any\nP0 or P1 exists, otherwise `VERDICT: safe to merge`. With no findings or\nunverified items, output only `VERDICT: safe to merge`.\n</yakaai_skills>"}