Puremac macOS Cleaner
Free open-source macOS cleaner built with SwiftUI — CleanMyMac alternative with zero telemetry, scheduled auto-cleaning, and Xcode/Homebrew/system cache cleanup.
- Skill ID
- reason-machines/trending-skills/puremac-macos-cleaner
- Publisher
- reason-machines
- Repository
- trending-skills
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
reason-machines/trending-skills/puremac-macos-cleanerInstalls these files- SKILL.md
What this skill tells the agent
PureMac macOS Cleaner
Skill by ara.so — Daily 2026 Skills collection.
PureMac is a free, native SwiftUI macOS application that cleans system junk, user caches, Xcode derived data, Homebrew caches, mail attachments, and purgeable APFS space. It is a privacy-respecting, open-source alternative to CleanMyMac X with no telemetry, no subscriptions, and no network calls.
Install
Homebrew (recommended)
brew tap momenbasel/tap
brew install --cask puremacDirect Download
Download the latest .app from Releases, unzip, and drag to /Applications.
Build from Source
brew install xcodegen
git clone https://github.com/momenbasel/PureMac.git
cd PureMac
xcodegen generate
xcodebuild \
-project PureMac.xcodeproj \
-scheme PureMac \
-configuration Release \
-derivedDataPath build \
build
open build/Build/Products/Release/PureMac.appRequirements: macOS 13.0+, Swift 5.9, Xcode 15+.
Project Structure
PureMac/
├── PureMac/
│ ├── App/
│ │ └── PureMacApp.swift # App entry point
│ ├── Views/
│ │ ├── ContentView.swift # Main window
│ │ ├── ScanView.swift # Smart scan UI
│ │ ├── CategoryDetailView.swift # Per-category drill-down
│ │ └── SettingsView.swift # Schedule & preferences
│ ├── Models/
│ │ ├── CleanCategory.swift # Category definitions
│ │ └── ScanResult.swift # Scan result model
│ ├── Services/
│ │ ├── ScannerService.swift # File scanning logic
│ │ ├── CleanerService.swift # Deletion logic
│ │ ├── SchedulerService.swift # Auto-clean scheduling
│ │ └── PurgeableService.swift # APFS purgeable space
│ └── Utilities/
│ └── FileSizeFormatter.swift
├── project.yml # XcodeGen spec
└── CONTRIBUTING.mdCore Concepts
Clean Categories
PureMac operates on named categories, each mapping to specific filesystem paths:
| Category | Key Paths |
|---|---|
| System Junk | /Library/Caches, /Library/Logs, /tmp, ~/Library/Logs |
| User Cache | ~/Library/Caches, npm/pip/yarn/pnpm caches |
| Mail Attachments | ~/Library/Mail Downloads |
| Trash | ~/.Trash |
| Large & Old Files | ~/Downloads, ~/Documents, ~/Desktop (>100 MB or >1 year old) |
| Purgeable Space | APFS Time Machine snapshots via tmutil |
| Xcode Junk | DerivedData, Archives, CoreSimulator/Caches |
| Homebrew Cache | ~/Library/Caches/Homebrew |
Large & Old Files are never auto-selected — the user must explicitly choose items before cleaning.
Working with the Codebase
Adding a New Clean Category
- Define the category in
CleanCategory.swift:
// CleanCategory.swift
enum CleanCategory: String, CaseIterable, Identifiable {
case systemJunk = "System Junk"
case userCache = "User Cache"
case mailAttachments = "Mail Attachments"
case trash = "Trash"
case largeOldFiles = "Large & Old Files"
case purgeableSpace = "Purgeable Space"
case xcodeJunk = "Xcode Junk"
case homebrewCache = "Homebrew Cache"
// Add your new category here:
case gradleCache = "Gradle Cache"
var id: String { rawValue }
var iconName: String {
switch self {
case .systemJunk: return "trash.circle"
case .userCache: return "internaldrive"
case .xcodeJunk: return "hammer"
case .homebrewCache: return "shippingbox"
case .gradleCache: return "archivebox" // new
default: return "folder"
}
}
}- Add scanning logic in
ScannerService.swift:
// ScannerService.swift
func scanCategory(_ category: CleanCategory) async throws -> ScanResult {
switch category {
case .gradleCache:
return try await scanPaths([
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gradle/caches")
])
// ...existing cases
default:
throw ScannerError.unsupportedCategory
}
}
private func scanPaths(_ urls: [URL]) async throws -> ScanResult {
var files: [ScannedFile] = []
let fm = FileManager.default
for url in urls {
guard fm.fileExists(atPath: url.path) else { continue }
let enumerator = fm.enumerator(
at: url,
includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],
options: [.skipsHiddenFiles]
)
while let fileURL = enumerator?.nextObject() as? URL {
let values = try fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey])
let size = Int64(values.fileSize ?? 0)
let modified = values.contentModificationDate ?? Date.distantPast
files.append(ScannedFile(url: fileURL, size: size, modifiedDate: modified))
}
}
let totalBytes = files.reduce(0) { $0 + $1.size }
return ScanResult(category: .gradleCache, files: files, totalBytes: totalBytes)
}- Add cleaning logic in
CleanerService.swift:
// CleanerService.swift
func clean(_ result: ScanResult, selectedFiles: Set<URL>? = nil) async throws -> Int64 {
let filesToDelete = selectedFiles.map { Array($0) } ?? result.files.map(\.url)
var bytesFreed: Int64 = 0
let fm = FileManager.default
for url in filesToDelete {
do {
let attrs = try fm.attributesOfItem(atPath: url.path)
let size = attrs[.size] as? Int64 ?? 0
try fm.removeItem(at: url)
bytesFreed += size
} catch {
// Log but continue — don't abort on single-file failure
print("Failed to delete \(url.lastPathComponent): \(error.localizedDescription)")
}
}
return bytesFreed
}Scheduled Auto-Cleaning
Configure via Settings → Schedule tab. Intervals: hourly, 3h, 6h, 12h, daily, weekly, biweekly, monthly.
// SchedulerService.swift — how scheduling is implemented
import UserNotifications
class SchedulerService: ObservableObject {
@AppStorage("schedulingEnabled") var schedulingEnabled: Bool = false
@AppStorage("cleaningInterval") var cleaningInterval: String = "daily"
@AppStorage("autoCleanAfterScan") var autoCleanAfterScan: Bool = false
@AppStorage("autoPurgePurgeable") var autoPurgePurgeable: Bool = false
private var timer: Timer?
func scheduleIfNeeded() {
timer?.invalidate()
guard schedulingEnabled else { return }
let interval = intervalSeconds(for: cleaningInterval)
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
Task { await self?.runScheduledClean() }
}
}
private func intervalSeconds(for key: String) -> TimeInterval {
switch key {
case "hourly": return 3_600
case "3h": return 10_800
case "6h": return 21_600
case "12h": return 43_200
case "daily": return 86_400
case "weekly": return 604_800
case "biweekly": return 1_209_600
case "monthly": return 2_592_000
default: return 86_400
}
}
@MainActor
private func runScheduledClean() async {
let scanner = ScannerService()
let cleaner = CleanerService()
for category in CleanCategory.allCases where category != .largeOldFiles {
if let result = try? await scanner.scanCategory(category), autoCleanAfterScan {
_ = try? await cleaner.clean(result)
}
}
if autoPurgePurgeable {
try? await PurgeableService.shared.purge()
}
}
}Enable scheduling programmatically:
let scheduler = SchedulerService()
scheduler.cleaningInterval = "weekly"More from reason-machines
Use this skill in a project
Open any RiverX project, search the Skills panel for reason-machines/trending-skills/puremac-macos-cleaner, and install it in one click.
