|
1 | | -# SqueakyClean |
| 1 | +<div align="center"> |
| 2 | + <h1>SqueakyClean</h1> |
| 3 | + <p><strong>A macOS cleaner that treats uncertainty as a stop sign.</strong></p> |
| 4 | + <p>Inspect known leftover locations, review the evidence, and quarantine only what you approve.</p> |
| 5 | + <p> |
| 6 | + <a href="https://github.com/YKKULTRA/squeakyclean/actions/workflows/ci.yml"><img alt="macOS CI" src="https://github.com/YKKULTRA/squeakyclean/actions/workflows/ci.yml/badge.svg"></a> |
| 7 | + <img alt="macOS 14 or newer" src="https://img.shields.io/badge/macOS-14%2B-111827?logo=apple&logoColor=white"> |
| 8 | + <img alt="Swift 6.3" src="https://img.shields.io/badge/Swift-6.3-F05138?logo=swift&logoColor=white"> |
| 9 | + </p> |
| 10 | + <p> |
| 11 | + <a href="#quick-start">Build from source</a> · |
| 12 | + <a href="#safety-by-design">Safety model</a> · |
| 13 | + <a href="CHANGELOG.md">Changelog</a> |
| 14 | + </p> |
| 15 | +</div> |
| 16 | + |
| 17 | +<p align="center"> |
| 18 | + <img src="Docs/images/scan-workspace.png" alt="SqueakyClean scan workspace in dark mode" width="100%"> |
| 19 | +</p> |
| 20 | + |
| 21 | +SqueakyClean is a native SwiftUI utility for careful macOS cleanup. It scans an allowlisted set of leftover locations, explains why a finding is actionable, and keeps cleanup reversible until you explicitly purge it. Scanning and cleanup happen locally. |
| 22 | + |
| 23 | +> [!NOTE] |
| 24 | +> SqueakyClean is an early, deliberately narrow preview. Today it offers cleanup only for user LaunchAgent plists whose configured absolute target is definitively missing. It is not yet a general cache cleaner or app uninstaller. |
| 25 | +
|
| 26 | +## Why SqueakyClean |
| 27 | + |
| 28 | +| Evidence first | Reversible by default | Local and transparent | |
| 29 | +|---|---|---| |
| 30 | +| Uncertain ownership is ignored, never treated as proof that data is abandoned. | Approved items enter an app-managed quarantine and can be restored before purge. | Every candidate includes a reason and evidence. There are no third-party Swift package dependencies. | |
| 31 | + |
| 32 | +Most cleaners optimize for how much they can find. SqueakyClean optimizes for how confidently it can leave the right things alone. |
| 33 | + |
| 34 | +## How it works |
| 35 | + |
| 36 | +```mermaid |
| 37 | +flowchart LR |
| 38 | + A["Read-only scan"] --> B["Explain the evidence"] |
| 39 | + B --> C["You review and approve"] |
| 40 | + C --> D["Revalidate path, identity, and evidence"] |
| 41 | + D --> E["Journal and quarantine"] |
| 42 | + E --> F["Restore"] |
| 43 | + E --> G["Explicit purge"] |
| 44 | +``` |
2 | 45 |
|
3 | | -`SqueakyClean` is a native macOS cleanup app built with `SwiftUI` and a safety-first scanning engine. |
| 46 | +1. **Scan:** Standard mode inventories selected user locations. Deep mode adds system-wide locations for analysis only. |
| 47 | +2. **Classify:** The rules fail closed. Protected, installed, Apple-managed, inaccessible, unresolved, and unknown data is not offered for cleanup. |
| 48 | +3. **Review:** Search, filter, inspect evidence, select one or many candidates, and approve the exact items you want moved. |
| 49 | +4. **Revalidate:** Immediately before a move, SqueakyClean checks the canonical path, filesystem identity, modification state, ownership, and missing-target evidence again. |
| 50 | +5. **Quarantine:** The move is journaled and fingerprinted. Restore remains available until a separately confirmed purge. |
4 | 51 |
|
5 | | -## What It Does |
| 52 | +> [!IMPORTANT] |
| 53 | +> Quarantine normally does not free disk space because the payload remains on the same disk. Space is reclaimed only after a separate, explicitly confirmed purge. Displayed sizes are allocated-byte estimates. |
6 | 54 |
|
7 | | -- Scans only allowlisted locations that commonly accumulate technical leftovers. |
8 | | -- Treats installed-app data as blocked and unmatched app-like data as unknown, not orphaned. |
9 | | -- Offers cleanup only when the scan has positive evidence, currently a user launch item whose resolved target is missing. |
10 | | -- Inventories `/Library` in Deep mode for analysis, but never offers cleanup there. |
11 | | -- Requires explicit user approval before cleanup. |
12 | | -- Supports async scans, cancellation, search, kind filters, multi-select, and batch quarantine. |
13 | | -- Shows an allocated-size estimate for candidates, including hidden files and package contents. |
14 | | -- Moves approved items into an app-managed quarantine area with restore support. Quarantine itself does not free disk space. |
15 | | -- Can permanently purge quarantined items through explicit destructive confirmation. |
16 | | -- Records scan snapshots and approval history in the app's own storage. |
| 55 | +## Current cleanup scope |
17 | 56 |
|
18 | | -## Current Scope |
| 57 | +| Finding | Current behavior | |
| 58 | +|---|---| |
| 59 | +| User LaunchAgent with an absolute target confirmed missing | Reviewable cleanup candidate | |
| 60 | +| Unmatched app-like data | Ignored as unknown | |
| 61 | +| Installed-app or Apple-managed data | Blocked | |
| 62 | +| Relative, malformed, unresolved, or inaccessible launch target | Left untouched | |
| 63 | +| Any `/Library` finding from Deep mode | Analysis only | |
| 64 | +| Cache, log, preference, Application Support, temporary, or receipt data | Inventoried, but not actionable in this release | |
19 | 65 |
|
20 | | -Milestone 1 deliberately keeps its actionable scope narrow. In the production scan profile, a user LaunchAgent becomes reviewable only when its configured target resolves to an absolute path and that path is confirmed missing. A relative command, unresolved path, malformed plist, or still-valid launch item is not offered for cleanup. |
| 66 | +This narrow scope is intentional. A catalog miss or an old file is not enough evidence that something is safe to remove. |
21 | 67 |
|
22 | | -An app-like folder or preference that fails to match the installed-app catalog is treated as unknown and ignored. A missing catalog match is not proof that an app was removed. Orphaned caches, logs, Application Support folders, preferences, stale scripts, and installer remnants are therefore not production cleanup categories today. |
| 68 | +## Safety by design |
23 | 69 |
|
24 | | -Deep mode inventories selected `/Library` locations, including shared launch items and installer receipts, but every system-wide finding is analysis-only. The app deliberately does **not** propose cleanup for installed-app data, Apple-managed paths such as CloudKit, Mobile Documents, iCloud, Containers, or Group Containers, its own data, or any `/Library` item. |
| 70 | +- **Read-only until approval.** Scanning never mutates discovered files. |
| 71 | +- **Fail-closed decisions.** Unknown or incomplete evidence stops the cleanup path. |
| 72 | +- **Approval-time revalidation.** Allowed roots, protected paths, filesystem identity, modification state, installed-app ownership, and launch-target evidence are checked again around the move. |
| 73 | +- **Crash-aware transactions.** An atomically written journal and cross-process lock serialize quarantine, restore, and purge operations. Ambiguous interrupted states remain visible instead of being guessed away. |
| 74 | +- **Verified restore.** Type-separated SHA-256 fingerprints cover full file contents and deterministic directory entries, including hidden files and package contents. Restore verifies the payload before and after moving it. |
25 | 75 |
|
26 | | -## Safety Model |
| 76 | +The detailed invariants and current trust boundaries are documented in [the safety model](Docs/SAFETY.md). |
27 | 77 |
|
28 | | -- Scans are read-only until the user explicitly approves a candidate. |
29 | | -- Classification fails closed: unknown ownership is ignored, while installed, Apple-managed, protected, and system-wide data is blocked. |
30 | | -- Immediately before quarantine, an action-time gate requires a recorded filesystem identity, verifies the canonical path is inside an allowed user root, outside protected roots, is not a symbolic link, and still has the identity and modification state recorded by the scan. It also refreshes the installed-app catalog and, for dead launch items, confirms that the target is still definitively missing. It rechecks the candidate after fingerprinting and before moving it. |
31 | | -- Quarantine writes a pending operation to a serialized, atomically saved manifest before moving the source. Pending quarantine, restore, and purge states are reconciled after interrupted operations; ambiguous states are preserved as interrupted instead of guessed or deleted. |
32 | | -- A filesystem lock serializes complete quarantine, restore, and purge transactions across app processes. Managed payload and restore paths are revalidated immediately around mutations. |
33 | | -- Version 2 fingerprints domain-separate files, directories, and symbolic links, then hash full file contents and deterministic directory entries, including hidden files and package contents. Restore requires a supported fingerprint and verifies it both before moving and at the destination before finalizing. |
34 | | -- Candidate sizes are allocated-byte estimates, not guarantees of space that will be reclaimed. Filesystem compression, clones, metadata, and later changes can affect the result. |
35 | | -- Quarantine preserves restore support but normally moves data on the same disk, so it frees no space. Only permanent purge reclaims the payload's space, and purge requires separate confirmation. |
36 | | -- Malformed launch agent plists, unreadable files, and unreadable roots are skipped or surfaced as skipped roots instead of aborting the whole scan. |
37 | | -- Launch agent target checks expand `~`, `$HOME`, and `${HOME}`-style paths before deciding whether a target is missing. |
38 | | -- Large files are hashed with streaming SHA256 to avoid loading the whole payload into memory. |
39 | | -- Directory sizing and fingerprinting fail closed if any descendant cannot be enumerated; partial traversals are never treated as complete evidence. |
| 78 | +## Quick start |
40 | 79 |
|
41 | | -## Project Layout |
| 80 | +### Requirements |
42 | 81 |
|
43 | | -- `Sources/SqueakyCleanCore`: scanning, ownership resolution, rule engine, quarantine, restore, audit persistence |
44 | | -- `Sources/SqueakyCleanApp`: `SwiftUI` app shell and user flows |
45 | | -- `Resources/Info.plist`: bundle metadata used by the local app build script |
46 | | -- `Scripts/build-app.sh`: builds, signs, and verifies `build/SqueakyClean.app` |
47 | | -- `Tests/SqueakyCleanCoreTests`: unit tests for the core safety and cleanup behavior |
| 82 | +- macOS 14 or newer |
| 83 | +- A Swift 6.3-capable toolchain |
| 84 | +- Xcode Command Line Tools for building and code signing |
48 | 85 |
|
49 | | -## Run It |
| 86 | +### Build the app |
50 | 87 |
|
51 | 88 | ```bash |
| 89 | +git clone https://github.com/YKKULTRA/squeakyclean.git |
| 90 | +cd squeakyclean |
52 | 91 | Scripts/build-app.sh |
53 | 92 | open build/SqueakyClean.app |
54 | 93 | ``` |
55 | 94 |
|
56 | | -The default build targets the current Mac architecture and uses an ad hoc signature, which is suitable for local development. To create a universal app containing both Apple silicon and Intel executables: |
| 95 | +The default local build targets the current Mac architecture and uses an ad hoc signature. To build a universal app for Apple silicon and Intel Macs: |
57 | 96 |
|
58 | 97 | ```bash |
59 | 98 | ARCHS="arm64 x86_64" Scripts/build-app.sh |
60 | 99 | ``` |
61 | 100 |
|
62 | | -For a distribution build, select an installed signing identity. Non-ad-hoc identities automatically enable the hardened runtime and request a secure timestamp: |
| 101 | +For Developer ID signing, select an installed identity: |
63 | 102 |
|
64 | 103 | ```bash |
65 | 104 | ARCHS="arm64 x86_64" \ |
66 | 105 | CODE_SIGN_IDENTITY="Developer ID Application: Example, Inc. (TEAMID)" \ |
67 | 106 | Scripts/build-app.sh |
68 | 107 | ``` |
69 | 108 |
|
70 | | -`BUILD_DIR` can select another output directory, `MACOSX_DEPLOYMENT_TARGET` can raise the plist default, and `WARNINGS_AS_ERRORS=1` enables strict compiler warnings during packaging. Running the raw SwiftPM executable can make Finder open a Terminal window. Use the `.app` bundle above for normal macOS launching. |
| 109 | +Real signing identities automatically enable the hardened runtime and request a secure timestamp. `BUILD_DIR`, `MACOSX_DEPLOYMENT_TARGET`, and `WARNINGS_AS_ERRORS=1` provide additional build controls. |
71 | 110 |
|
72 | | -## Verify It |
| 111 | +## Development |
73 | 112 |
|
74 | 113 | ```bash |
75 | | -swift build |
76 | | -swift test |
77 | | -WARNINGS_AS_ERRORS=1 Scripts/build-app.sh |
78 | | -plutil -lint build/SqueakyClean.app/Contents/Info.plist |
79 | | -codesign --verify --deep --strict --verbose=2 build/SqueakyClean.app |
| 114 | +swift build -Xswiftc -warnings-as-errors |
| 115 | +swift test -Xswiftc -warnings-as-errors |
| 116 | +ARCHS="arm64 x86_64" WARNINGS_AS_ERRORS=1 Scripts/build-app.sh |
80 | 117 | ``` |
81 | 118 |
|
82 | | -GitHub Actions runs release builds and tests with warnings treated as errors, then packages and verifies a universal app bundle. |
| 119 | +GitHub Actions performs strict release builds and tests, packages a universal app bundle, verifies its architectures and signature, and uploads the archive as a workflow artifact. |
| 120 | + |
| 121 | +### Project layout |
| 122 | + |
| 123 | +| Path | Purpose | |
| 124 | +|---|---| |
| 125 | +| `Sources/SqueakyCleanCore` | Inventory, classification, path policy, quarantine, restore, and audit logic | |
| 126 | +| `Sources/SqueakyCleanApp` | SwiftUI interface and user workflows | |
| 127 | +| `Tests/SqueakyCleanCoreTests` | Core correctness and transaction-safety coverage | |
| 128 | +| `Resources/Info.plist` | App bundle metadata | |
| 129 | +| `Scripts/build-app.sh` | Native or universal app packaging, signing, and verification | |
| 130 | + |
| 131 | +## FAQ |
| 132 | + |
| 133 | +<details> |
| 134 | +<summary><strong>Does SqueakyClean delete anything automatically?</strong></summary> |
| 135 | + |
| 136 | +No. Scans are read-only. A candidate moves only after explicit approval, and permanent purge requires a separate destructive confirmation. |
| 137 | +</details> |
| 138 | + |
| 139 | +<details> |
| 140 | +<summary><strong>Is Full Disk Access required?</strong></summary> |
| 141 | + |
| 142 | +No. Start without it. The app explains when a deliberately selected scan location could not be read, and its access check is a practical probe rather than an operating-system entitlement verdict. |
| 143 | +</details> |
| 144 | + |
| 145 | +<details> |
| 146 | +<summary><strong>Does it remove caches or Application Support folders?</strong></summary> |
| 147 | + |
| 148 | +Not in the current release. Those locations may be inventoried, but only positively identified dead user LaunchAgents can become cleanup candidates. |
| 149 | +</details> |
83 | 150 |
|
84 | | -## Local App Data |
| 151 | +<details> |
| 152 | +<summary><strong>Where does SqueakyClean keep its own data?</strong></summary> |
85 | 153 |
|
86 | | -The app stores its own audit and quarantine data under: |
| 154 | +Audit history, transaction state, and quarantined payloads live under `~/Library/Application Support/SqueakyClean`. |
| 155 | +</details> |
87 | 156 |
|
88 | | -`~/Library/Application Support/SqueakyClean` |
| 157 | +## Project status |
89 | 158 |
|
90 | | -## Distribution Status |
| 159 | +SqueakyClean is currently version `0.1.0` and under active development. This repository ships source code and development artifacts, not a notarized public release. Public distribution still requires Developer ID signing, Apple notarization, and a finalized permissions story. |
91 | 160 |
|
92 | | -The local app bundle is ad-hoc signed and strictly verified. That is appropriate for development and personal testing. Public distribution still needs a Developer ID identity, Apple notarization, and a clearer permissions story. The build script supports Developer ID signing and the hardened runtime, but it deliberately does not submit credentials or notarize automatically. |
| 161 | +A project license has not yet been selected. See the [changelog](CHANGELOG.md) for the latest work and known scope. |
0 commit comments