Puremac Macos Cleaner

by reason-machines2384a003145aNo license83 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 3 months ago

Free open-source macOS cleaner built with SwiftUI — CleanMyMac alternative with zero telemetry, scheduled auto-cleaning, and Xcode/Homebrew/system cache cleanup.

Instructions onlySoftware Development
AI-generated overview

Documents PureMac, an open-source SwiftUI macOS cleaner, covering its install, code structure, clean categories, and scheduling.

What it does
This skill provides reference documentation for PureMac, a native SwiftUI macOS cleaning application that removes system junk, user caches, Xcode derived data, Homebrew caches, mail attachments, and APFS purgeable space. It explains installation via Homebrew, direct download, or building from source with XcodeGen and xcodebuild, and describes the project layout, clean category definitions, scanner and cleaner services, scheduled auto-cleaning, and purgeable snapshot handling. It also includes Swift code samples for adding categories, configuring scheduling through AppStorage keys, and building and testing the app.
When to use it
Use it when you need to install, build, configure, or contribute to PureMac, or when you want to understand how its scanning, cleaning, scheduling, and purgeable-space features are implemented. It is also useful for adding a new clean category or wiring up scheduled cleaning.
Requirements
Requires macOS 13.0+, Swift 5.9, and Xcode 15+ to build from source; XcodeGen is needed to generate the project. Homebrew is used for installation or for installing XcodeGen. Purgeable-space operations use tmutil and may require admin credentials. The skill itself is instructions only and ships no scripts.

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)

bash
brew tap momenbasel/tapbrew install --cask puremac

Direct Download

Download the latest .app from Releases, unzip, and drag to /Applications.

Build from Source

bash
brew install xcodegengit clone https://github.com/momenbasel/PureMac.gitcd PureMacxcodegen generatexcodebuild \  -project PureMac.xcodeproj \  -scheme PureMac \  -configuration Release \  -derivedDataPath build \  buildopen 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:

CategoryKey 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 SpaceAPFS Time Machine snapshots via tmutil
Xcode JunkDerivedData, 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

  1. Define the category in CleanCategory.swift:
swift
// CleanCategory.swiftenum 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"        }    }}
  1. Add scanning logic in ScannerService.swift:
swift
// ScannerService.swiftfunc 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)}
  1. Add cleaning logic in CleanerService.swift:
swift
// CleanerService.swiftfunc 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.

swift
// SchedulerService.swift — how scheduling is implementedimport 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:

swift
let scheduler = SchedulerService()scheduler.cleaningInterval = "weekly"scheduler.autoCleanAfterScan = truescheduler.autoPurgePurgeable = falsescheduler.schedulingEnabled = truescheduler.scheduleIfNeeded()

Purgeable Space (APFS Snapshots)

PureMac uses tmutil to delete local Time Machine snapshots — this is the only operation requiring elevated privileges:

swift
// PurgeableService.swiftimport 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 in            let task = Process()            task.launchPath = "/bin/bash"            task.arguments = ["-c", command]            let pipe = Pipe()            task.standardOutput = pipe            task.terminationHandler = { _ in                let data = pipe.fileHandleForReading.readDataToEndOfFile()                continuation.resume(returning: String(data: data, encoding: .utf8) ?? "")            }            do { try task.run() } catch { continuation.resume(throwing: error) }        }    }}

Xcode Cache Paths

swift
// Paths cleaned by the Xcode Junk categorylet 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

swift
// Example: triggering a scan from a SwiftUI viewstruct 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 = false    }}

File Inspector (Click-to-Inspect)

swift
// Show files before deletion — users can deselectstruct 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:

KeyTypeDefaultDescription
schedulingEnabledBoolfalseEnable scheduled cleaning
cleaningIntervalString"daily"Interval key (see above)
autoCleanAfterScanBoolfalseAuto-clean after scheduled scan
autoPurgePurgeableBoolfalseAuto-purge APFS snapshots

Read/write from anywhere:

swift
UserDefaults.standard.set(true,    forKey: "schedulingEnabled")UserDefaults.standard.set("weekly", forKey: "cleaningInterval")

Building & Testing

bash
# Generate Xcode project from project.ymlxcodegen generate
# Build Releasexcodebuild \  -project PureMac.xcodeproj \  -scheme PureMac \  -configuration Release \  -derivedDataPath build \  build
# Run testsxcodebuild test \  -project PureMac.xcodeproj \  -scheme PureMac \  -destination 'platform=macOS'
# Open built appopen build/Build/Products/Release/PureMac.app

Contributing

  1. Fork and clone the repo.
  2. Run xcodegen generate to create the .xcodeproj.
  3. Create a feature branch: git checkout -b feature/gradle-cache-cleaning
  4. Follow existing patterns in ScannerService / CleanerService.
  5. Never add network calls, analytics SDKs, or telemetry of any kind.
  6. Large & Old Files must never be auto-selected for deletion.
  7. Open a PR against main.

See CONTRIBUTING.md for full guidelines.


Troubleshooting

ProblemSolution
xcodegen: command not foundbrew install xcodegen
App blocked by GatekeeperThe release build is notarized; if building from source, run xattr -cr PureMac.app
Purgeable scan returns 0 bytesNo local Time Machine snapshots exist — this is normal if TM is off
Xcode paths not foundXcode has not been used yet or DerivedData was already cleared
tmutil requires passwordPurgeable purge may prompt for admin credentials — this is expected macOS behavior
Scheduled cleaning not triggeringEnsure 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.

Source and attribution

Source:reason-machines/trending-skillsinskills/puremac-macos-cleanerat commit2384a00

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal