| name | puremac-macos-cleaner |
| description | Free open-source macOS cleaner built with SwiftUI — CleanMyMac alternative with zero telemetry, scheduled auto-cleaning, and Xcode/Homebrew/system cache cleanup. |
| triggers | ["clean my mac with PureMac","set up PureMac cleaner","build PureMac from source","add scheduled cleaning to my mac","integrate PureMac into my workflow","configure PureMac auto-cleaning","contribute to PureMac","how does PureMac clean Xcode cache"] |
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 puremac
Direct 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.app
Requirements: 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.md
Core 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:
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"
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:
.gradleCache:
:
}
}
}
- Add scanning logic in
ScannerService.swift:
func scanCategory(_ category: CleanCategory) async throws -> ScanResult {
switch category {
case .gradleCache:
return try await scanPaths([
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gradle/caches")
])
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 {
values fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey])
size (values.fileSize )
modified values.contentModificationDate .distantPast
files.append((url: fileURL, size: size, modifiedDate: modified))
}
}
totalBytes files.reduce() { .size }
(category: .gradleCache, files: files, totalBytes: totalBytes)
}
- Add cleaning logic in
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 {
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.
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() }
}
}
( : ) -> {
key {
:
:
:
:
:
:
:
:
:
}
}
() {
scanner ()
cleaner ()
category .allCases category .largeOldFiles {
result scanner.scanCategory(category), autoCleanAfterScan {
cleaner.clean(result)
}
}
autoPurgePurgeable {
.shared.purge()
}
}
}
Enable scheduling programmatically:
let scheduler = SchedulerService()
scheduler.cleaningInterval = "weekly"
scheduler.autoCleanAfterScan = true
scheduler.autoPurgePurgeable = false
scheduler.schedulingEnabled = true
scheduler.scheduleIfNeeded()
Purgeable Space (APFS Snapshots)
PureMac uses tmutil to delete local Time Machine snapshots — this is the only operation requiring elevated privileges:
import Foundation
class PurgeableService {
static let shared = PurgeableService()
func listSnapshots() async throws -> [String] {
let output = try await shell("tmutil listlocalsnapshots /")
return output
.split(separator: "\n")
.map(String.init)
.filter { $0.hasPrefix("com.apple.TimeMachine") }
}
func purge() async throws {
let snapshots = try await listSnapshots()
for snapshot in snapshots {
try await shell("tmutil deletelocalsnapshots \(snapshot)")
}
}
@discardableResult
private func shell(_ command: String) async throws -> String {
try await withCheckedThrowingContinuation { continuation
task ()
task.launchPath
task.arguments [, command]
pipe ()
task.standardOutput pipe
task.terminationHandler {
data pipe.fileHandleForReading.readDataToEndOfFile()
continuation.resume(returning: (data: data, encoding: .utf8) )
}
{ task.run() } { continuation.resume(throwing: error) }
}
}
}
Xcode Cache Paths
let home = FileManager.default.homeDirectoryForCurrentUser
let xcodePaths: [URL] = [
home.appendingPathComponent("Library/Developer/Xcode/DerivedData"),
home.appendingPathComponent("Library/Developer/Xcode/Archives"),
home.appendingPathComponent("Library/Developer/CoreSimulator/Caches"),
]
Scan these and safely delete their contents without removing the directories themselves.
SwiftUI View Patterns
Scan Progress View
struct ScanView: View {
@StateObject private var scanner = ScannerService()
@State private var results: [ScanResult] = []
@State private var isScanning = false
var body: some View {
VStack {
if isScanning {
ProgressView("Scanning…")
} else {
Button("Smart Scan") {
Task { await runScan() }
}
}
List(results, id: \.category) { result in
CategoryRow(result: result)
}
}
}
private func runScan() async {
isScanning = true
results = []
for category in CleanCategory.allCases {
if let result = try? await scanner.scanCategory(category) {
results.append(result)
}
}
isScanning
}
}
File Inspector (Click-to-Inspect)
struct CategoryDetailView: View {
let result: ScanResult
@State private var selected: Set<URL> = []
@State private var cleaned = false
var body: some View {
List(result.files, id: \.url, selection: $selected) { file in
HStack {
Image(systemName: "doc")
Text(file.url.lastPathComponent)
Spacer()
Text(ByteCountFormatter.string(fromByteCount: file.size, countStyle: .file))
.foregroundStyle(.secondary)
}
}
.toolbar {
Button("Clean Selected") {
Task {
let cleaner = CleanerService()
_ = try? await cleaner.clean(result, selectedFiles: selected)
cleaned = true
}
}
.disabled(selected.isEmpty)
}
}
}
Configuration (AppStorage Keys)
All preferences are stored in UserDefaults via @AppStorage:
| Key | Type | Default | Description |
|---|
schedulingEnabled | Bool | false | Enable scheduled cleaning |
cleaningInterval | String | "daily" | Interval key (see above) |
autoCleanAfterScan | Bool | false | Auto-clean after scheduled scan |
autoPurgePurgeable | Bool | false | Auto-purge APFS snapshots |
Read/write from anywhere:
UserDefaults.standard.set(true, forKey: "schedulingEnabled")
UserDefaults.standard.set("weekly", forKey: "cleaningInterval")
Building & Testing
xcodegen generate
xcodebuild \
-project PureMac.xcodeproj \
-scheme PureMac \
-configuration Release \
-derivedDataPath build \
build
xcodebuild test \
-project PureMac.xcodeproj \
-scheme PureMac \
-destination 'platform=macOS'
open build/Build/Products/Release/PureMac.app
Contributing
- Fork and clone the repo.
- Run
xcodegen generate to create the .xcodeproj.
- Create a feature branch:
git checkout -b feature/gradle-cache-cleaning
- Follow existing patterns in
ScannerService / CleanerService.
- Never add network calls, analytics SDKs, or telemetry of any kind.
- Large & Old Files must never be auto-selected for deletion.
- Open a PR against
main.
See CONTRIBUTING.md for full guidelines.
Troubleshooting
| Problem | Solution |
|---|
xcodegen: command not found | brew install xcodegen |
| App blocked by Gatekeeper | The release build is notarized; if building from source, run xattr -cr PureMac.app |
| Purgeable scan returns 0 bytes | No local Time Machine snapshots exist — this is normal if TM is off |
| Xcode paths not found | Xcode has not been used yet or DerivedData was already cleared |
tmutil requires password | Purgeable purge may prompt for admin credentials — this is expected macOS behavior |
| Scheduled cleaning not triggering | Ensure the app is running (it is not a background daemon); check Settings → Schedule |
Safety Guarantees
- Never deletes system-critical files or application bundles.
- Only removes caches, logs, temp files, and user-selected items.
- Large & Old Files require explicit user selection before deletion.
- Purgeable operations target only APFS Time Machine snapshots — not free space.
- All filesystem operations use
FileManager.removeItem(at:) — no rm -rf shell calls for regular cleaning.