Skip to content

Commit 46288f2

Browse files
committed
Initial SqueakyClean release
0 parents  commit 46288f2

40 files changed

Lines changed: 7958 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: macOS CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: macos-ci-${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
build-test-package:
17+
runs-on: macos-26
18+
timeout-minutes: 30
19+
20+
steps:
21+
- name: Check out repository
22+
uses: actions/checkout@v6
23+
24+
- name: Show Swift toolchain
25+
run: swift --version
26+
27+
- name: Build with warnings as errors
28+
run: swift build --configuration release -Xswiftc -warnings-as-errors
29+
30+
- name: Test with warnings as errors
31+
run: swift test -Xswiftc -warnings-as-errors
32+
33+
- name: Package universal app
34+
env:
35+
ARCHS: arm64 x86_64
36+
WARNINGS_AS_ERRORS: 1
37+
run: Scripts/build-app.sh
38+
39+
- name: Verify packaged app
40+
shell: bash
41+
run: |
42+
app="build/SqueakyClean.app"
43+
executable="$app/Contents/MacOS/SqueakyClean"
44+
plutil -lint "$app/Contents/Info.plist"
45+
test -x "$executable"
46+
lipo "$executable" -verify_arch arm64 x86_64
47+
codesign --verify --deep --strict --verbose=2 "$app"
48+
49+
- name: Archive app
50+
run: ditto -c -k --sequesterRsrc --keepParent build/SqueakyClean.app build/SqueakyClean.zip
51+
52+
- name: Upload app artifact
53+
uses: actions/upload-artifact@v7
54+
with:
55+
name: SqueakyClean-universal
56+
path: build/SqueakyClean.zip
57+
if-no-files-found: error

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
/.build
3+
/build
4+
/Packages
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/configuration/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc

CHANGELOG.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
### Correctness
6+
7+
- Moved scans off the main actor so the UI remains responsive and the spinner renders during slow scans.
8+
- Added scan cancellation without clearing the previous report or showing an error alert.
9+
- Made inventory scans resilient to malformed plists, unreadable roots, and per-file metadata failures.
10+
- Added launch target expansion for `~`, `$HOME`, and `${HOME}` before checking whether launch agents point at missing targets.
11+
- Changed ownership resolution to fail closed: an unmatched owner hint is unknown and ignored instead of being treated as proof of an orphan.
12+
- Limited production cleanup candidates to positive evidence, currently user launch items with resolved targets that are confirmed missing.
13+
- Removed the dormant age-only unknown-script candidate rule so future scan-profile expansion remains fail closed.
14+
- Made relative launch commands fail closed instead of checking them against SqueakyClean's working directory and falsely calling them missing.
15+
- Distinguished a definitively missing launch target from an inaccessible one and revalidated missing-target evidence immediately before quarantine.
16+
- Refreshed installed-app ownership at approval time so a reinstalled owner blocks a stale cleanup candidate.
17+
- Made every `/Library` finding analysis-only, including shared launch items and installer receipts found by Deep scans.
18+
- Hardened Apple-managed path blocking for CloudKit, Mobile Documents, iCloud, Containers, and Group Containers.
19+
- Added an action-time path and identity gate that rejects paths outside allowed user roots, protected paths, symbolic links, missing or stale filesystem identities, changed modification state, and non-quarantine actions.
20+
21+
### Cleanup Semantics
22+
23+
- Added candidate-only allocated-size estimates, including hidden files and package contents, without spending I/O on blocked findings.
24+
- Added version 2 type-separated, full-content fingerprints for files and directories and fingerprint verification before restore.
25+
- Journaled quarantine, restore, and purge operations in a serialized, atomically written manifest with startup reconciliation for interrupted operations.
26+
- Preserved ambiguous move outcomes as explicit interrupted records, fixed restore-destination collision rollback, and stopped move errors from deleting potentially completed payloads.
27+
- Added a filesystem transaction lock across app instances and repeated managed-path validation immediately around moves and deletion.
28+
- Made restore reject legacy or unknown fingerprint versions and reverify content at the destination before finalizing.
29+
- Clarified that quarantine itself normally reclaims no space and that displayed byte totals are estimates. Only purge removes the quarantined payload.
30+
- Added per-record purge and Empty Quarantine flows for reclaiming disk space through a separately confirmed destructive action.
31+
- Preserved restored records as audit history when purging the remaining quarantine.
32+
33+
### Usability
34+
35+
- Added search, kind filtering, multi-select, and batch quarantine for candidate lists.
36+
- Added destructive confirmation dialogs for per-record purge and Empty Quarantine.
37+
- Added distinct empty states for no candidates and no filter matches.
38+
- Added completed-scan scope and timestamp, stale-report protection, and an explicit Deep Read Only label.
39+
- Reframed space totals as purge-time estimates and clarified that Full Disk Access is optional.
40+
- Made the protected-location access check test every available sample before reporting success and avoided presenting the probe as an operating-system entitlement verdict.
41+
- Decoupled read-only scanning from audit and quarantine initialization so one damaged local store cannot disable the scanner.
42+
- Added restored destination locations and accurate history-removal wording for restored records.
43+
44+
### Infrastructure
45+
46+
- Replaced generated `PlistBuddy` bundle metadata with a static `Resources/Info.plist` template.
47+
- Hardened the local app build script with plist validation, native or universal architecture builds, completed-bundle signing, hardened runtime and timestamp support for real identities, and strict signature verification.
48+
- Added macOS CI that builds and tests with warnings treated as errors, packages a universal app, verifies it, and uploads the app archive.
49+
- Kept CI on the current official macOS 26 runner, checkout v6, and upload-artifact v7 action majors.
50+
- Changed large-file hashing to stream SHA256 in 1 MiB chunks.
51+
- Made directory sizing and fingerprinting propagate descendant-enumeration failures instead of silently accepting partial results.
52+
- Made directory fingerprints deterministic when parent directory names reappear deeper in a tree.
53+
54+
### Tests
55+
56+
- Expanded the suite across fail-closed ownership, scan profiles, launch target resolution, Apple-managed path blocking, action-time path policy, allocated sizing, transaction recovery, purge behavior, cancellation, streaming hashing, and full-content directory fingerprints.

Package.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// swift-tools-version: 6.3
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "SqueakyClean",
7+
platforms: [
8+
.macOS(.v14)
9+
],
10+
products: [
11+
.library(
12+
name: "SqueakyCleanCore",
13+
targets: ["SqueakyCleanCore"]
14+
),
15+
.executable(
16+
name: "SqueakyClean",
17+
targets: ["SqueakyCleanApp"]
18+
)
19+
],
20+
targets: [
21+
.target(
22+
name: "SqueakyCleanCore"
23+
),
24+
.executableTarget(
25+
name: "SqueakyCleanApp",
26+
dependencies: ["SqueakyCleanCore"]
27+
),
28+
.testTarget(
29+
name: "SqueakyCleanCoreTests",
30+
dependencies: ["SqueakyCleanCore"]
31+
)
32+
],
33+
swiftLanguageModes: [.v6]
34+
)

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# SqueakyClean
2+
3+
`SqueakyClean` is a native macOS cleanup app built with `SwiftUI` and a safety-first scanning engine.
4+
5+
## What It Does
6+
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.
17+
18+
## Current Scope
19+
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.
21+
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.
23+
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.
25+
26+
## Safety Model
27+
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.
40+
41+
## Project Layout
42+
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
48+
49+
## Run It
50+
51+
```bash
52+
Scripts/build-app.sh
53+
open build/SqueakyClean.app
54+
```
55+
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:
57+
58+
```bash
59+
ARCHS="arm64 x86_64" Scripts/build-app.sh
60+
```
61+
62+
For a distribution build, select an installed signing identity. Non-ad-hoc identities automatically enable the hardened runtime and request a secure timestamp:
63+
64+
```bash
65+
ARCHS="arm64 x86_64" \
66+
CODE_SIGN_IDENTITY="Developer ID Application: Example, Inc. (TEAMID)" \
67+
Scripts/build-app.sh
68+
```
69+
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.
71+
72+
## Verify It
73+
74+
```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
80+
```
81+
82+
GitHub Actions runs release builds and tests with warnings treated as errors, then packages and verifies a universal app bundle.
83+
84+
## Local App Data
85+
86+
The app stores its own audit and quarantine data under:
87+
88+
`~/Library/Application Support/SqueakyClean`
89+
90+
## Distribution Status
91+
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.

Resources/Info.plist

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>en</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>SqueakyClean</string>
9+
<key>CFBundleExecutable</key>
10+
<string>SqueakyClean</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>local.squeakyclean.app</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>SqueakyClean</string>
17+
<key>CFBundlePackageType</key>
18+
<string>APPL</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>0.1.0</string>
21+
<key>CFBundleVersion</key>
22+
<string>1</string>
23+
<key>LSMinimumSystemVersion</key>
24+
<string>14.0</string>
25+
<key>LSApplicationCategoryType</key>
26+
<string>public.app-category.utilities</string>
27+
<key>NSHighResolutionCapable</key>
28+
<true/>
29+
</dict>
30+
</plist>

0 commit comments

Comments
 (0)