The macOS app for MacHarmony — an open-source storage scanner and cleaner.
A fast, modern macOS app for scanning, analyzing, and cleaning storage — built for developers and power users.
Build an optimized Release copy from source and install it to /Applications like any other app — no Xcode window, no Debug build.
Prerequisites: macOS 14 (Sonoma) or later, and Xcode 26+ (the full app — the Command Line Tools alone are not enough). Open Xcode once after installing it so it can finish setup.
git clone https://github.com/saleh2001k/MacHarmony.git && cd MacHarmony && ./scripts/install.shAlready cloned? From the repo root:
./scripts/install.shWhen it finishes, MacHarmony is in /Applications and shows up in Launchpad and Spotlight.
- Builds the
MacHarmonyscheme in the Release configuration (universal: Apple silicon + Intel) intobuild/install/ - Signs it ad-hoc ("Sign to Run Locally") — no Apple Developer account needed
- Quits MacHarmony if it is running, then replaces
/Applications/MacHarmony.app(asks for your password only if that folder isn't writable) - Verifies the code signature and launches the app
| Option | Effect |
|---|---|
--no-launch |
Install without opening the app |
INSTALL_DIR="$HOME/Applications" |
Install somewhere other than /Applications |
TEAM_ID=ABCDE12345 |
Sign with your Apple Developer team instead of ad-hoc (see below) |
Example: TEAM_ID=ABCDE12345 ./scripts/install.sh --no-launch
- Update:
git pull && ./scripts/install.sh - Uninstall:
./scripts/uninstall.sh— removes the app plus its caches, preferences and containers (usessudo)
macOS ties privacy permissions such as Full Disk Access to the app's code signature. An ad-hoc build gets a new signature every time it is rebuilt, so after an update you may need to re-enable MacHarmony in System Settings → Privacy & Security → Full Disk Access (remove it with –, then add /Applications/MacHarmony.app again with +).
To keep permissions across updates, sign with your own team: add your Apple ID in Xcode → Settings → Accounts, then pass your TEAM_ID (shown at developer.apple.com/account → Membership details).
- Open
MacHarmony.xcodeproj, select the MacHarmony target → Signing & Capabilities, and pick your Team (or set Signing Certificate to Sign to Run Locally) - Product → Archive
- In the Organizer: Distribute App → Custom → Copy App, then drag the exported
MacHarmony.appinto/Applications
- Parallel scanning — all categories scan concurrently using Swift structured concurrency; results stream in as each finishes
- Shared file index — one
fts_opentraversal builds an in-memory index upfront; alldeepFilesanddeepLargeFilescategories filter it in O(n) instead of re-traversing disk - Disk cache — scan results are persisted to JSON with a configurable TTL (default 1h); reopening the app loads instantly
- Incremental deletion — items disappear from the list one-by-one as each file is deleted; file I/O runs on a background thread so the UI never freezes
- Deletion progress HUD — a floating pill at the bottom shows current file name, batch count, and a linear progress bar
- Custom scanners — define your own categories by extension + directory
- Category ordering — drag-and-drop reorder in Settings; persisted across launches
- Ignored patterns — folder names to skip in deep scans (e.g.
.git,build,dist) - Excluded paths — explicit directories to skip entirely during all scans
- Menu bar access — quick stats and scan trigger from the menu bar
- Quick Look — press Space on a selected item in category detail to preview it
- Export — save scan results as CSV, JSON, or plain text
- Settings — 6-tab settings window (General, Scan, Categories, Notifications, Export, Custom Scanners)
- Tools (sidebar) — twelve standalone utilities (see Tools below); routed by
ToolRouterViewinContentView.swift - Loading indicators — indeterminate spinners use pure SwiftUI (
IndeterminateSpinnerinViewsToolSharedComponents.swift), not AppKitNSProgressIndicator, to avoid layout console noise
The sidebar lists Tools in ToolType / ToolType.allCases order. Each row opens a dedicated view via ToolRouterView.
| Tool | View file | What it does |
|---|---|---|
| Large File Browser | ViewsLargeFileBrowserView.swift |
Files larger than a user-chosen size threshold; grouped by type |
| Old & Unused Files | ViewsOldUnusedFilesView.swift |
Files not modified in 6, 12, or 24 months (segmented control); uses file index after a full scan or merged cached paths |
| Empty Folders | ViewsEmptyFolderView.swift |
Finds empty directories under the home folder |
| App Uninstaller | ViewsAppUninstallerView.swift |
Lists apps; drives uninstall / support-file cleanup |
| App Cache Breakdown | ViewsAppCacheView.swift |
Per-app cache size breakdown |
| Leftover Files | ViewsLeftoverFilesView.swift |
Orphaned Library folders after app removal (pairs with uninstaller scan) |
| Startup Items | ViewsStartupItemsView.swift |
Login items and launch agents |
| Memory Monitor | ViewsMemoryMonitorView.swift |
RAM usage and “free inactive memory” |
| Disk Health | ViewsDiskHealthView.swift |
Volume / SMART-style disk info |
| Scan History | ViewsScanHistoryView.swift |
History of space freed over time |
| Safe Mode Queue | ViewsSafeModeView.swift |
Stage items before committing deletion |
| iCloud Cleaner | ViewsiCloudCleanerView.swift |
iCloud-backed files reclaim hints |
Supporting types: ModelsToolType.swift (enum + LargeFileGroup), ModelsAppEntry.swift (app entries for uninstaller/cache tools). Tool-specific services include ServicesAppScanner, ServicesEmptyFolderScanner, ServicesLeftoverScanner, ServicesStartupItemScanner, ServicesDiskHealth, ServicesDiskUsage, ServicesMemoryMonitor, ServicesScanHistory.
Flat layout under MacHarmony/ (Xcode may group files visually; names match the repo).
MacHarmony/
│
├── MacHarmonyApp.swift # App entry, scenes, menu bar, Settings
├── ContentView.swift # NavigationSplitView, toolbar, ToolRouterView
│
├── Models/
│ ├── ModelsStorageCategory.swift # StorageCategory + ScanStyle
│ ├── ModelsStorageItem.swift # StorageItem (Codable)
│ ├── ModelsCustomCategory.swift # CustomScanCategory
│ ├── ModelsToolType.swift # ToolType enum + LargeFileGroup
│ └── ModelsAppEntry.swift # App bundle entries (uninstaller / cache tools)
│
├── ViewModels/
│ └── ViewModelsStorageManager.swift # StorageManager — state, scan, delete, tools, cache
│
├── Services/
│ ├── ServicesStorageScanner.swift # Actor — category scans, deep scans, custom scans
│ ├── ServicesFastScanEngine.swift # fts-based traversal (async streams)
│ ├── ServicesFileIndex.swift # Shared in-memory index for one pass per scan
│ ├── ServicesSizeCalculator.swift # du -sk sizing
│ ├── ServicesScanCache.swift # On-disk size cache (paths + TTL)
│ ├── ServicesResultsCache.swift # Full scan results JSON for relaunch
│ ├── ServicesThumbnailCache.swift # Thumbnail NSCache
│ ├── ServicesCustomCategoryStore.swift
│ ├── ServicesReportExporter.swift
│ ├── ServicesFileAccessService.swift # Home folder security-scoped access
│ ├── ServicesQuickLookCoordinator.swift
│ ├── ServicesDiskUsage.swift # Tool: volume usage
│ ├── ServicesDiskHealth.swift
│ ├── ServicesMemoryMonitor.swift
│ ├── ServicesScanHistory.swift
│ ├── ServicesAppScanner.swift
│ ├── ServicesEmptyFolderScanner.swift
│ ├── ServicesLeftoverScanner.swift
│ └── ServicesStartupItemScanner.swift
│
├── Views/ (main UI + tools)
│ ├── ViewsSidebarView.swift # Storage categories, Tools, custom scanners
│ ├── ViewsCategoryDetailView.swift
│ ├── ViewsCustomCategoryDetailView.swift
│ ├── ViewsStorageOverviewView.swift
│ ├── ViewsDashboardView.swift
│ ├── ViewsScanningView.swift # ScanProgressBanner, ScanningFullView, category chips
│ ├── ViewsSettingsView.swift
│ ├── ViewsMenuBarQuickAccessView.swift
│ ├── ViewsAddCustomCategoryView.swift
│ ├── ViewsHelpView.swift
│ ├── ViewsTheme.swift
│ ├── ViewsToolSharedComponents.swift # IndeterminateSpinner, LinearProgressFill, ToolLoadingView, …
│ │
│ ├── ViewsLargeFileBrowserView.swift
│ ├── ViewsOldUnusedFilesView.swift
│ ├── ViewsEmptyFolderView.swift
│ ├── ViewsAppUninstallerView.swift
│ ├── ViewsAppCacheView.swift
│ ├── ViewsLeftoverFilesView.swift
│ ├── ViewsStartupItemsView.swift
│ ├── ViewsMemoryMonitorView.swift
│ ├── ViewsDiskHealthView.swift
│ ├── ViewsScanHistoryView.swift
│ ├── ViewsSafeModeView.swift
│ └── ViewsiCloudCleanerView.swift
│
└── (Assets & entitlements in Xcode project)
View ──────────────────────► StorageManager (@MainActor ObservableObject)
│
┌──────────────────┤
│ │
StorageScanner ResultsCache
(actor) (actor)
│
┌──────────┼──────────┐
│ │ │
FastScanEngine FileIndex SizeCalculator
(async stream) (in-memory) (du subprocess)
│
ScanCache
(JSON on disk)
**StorageManager**is the single source of truth. All@Publishedstate lives here; Views bind to it directly.**StorageScanner**is an actor.StorageManager.scan()calls it from the main actor; the actor runs all work off-thread.**FileIndex**is built once per scan (one parallel FTS traversal). Categories that usedeepFilesordeepLargeFilesfilter the index in memory instead of re-traversing disk — dramatically faster for repeated scans.**ResultsCache**persists the full[String: [StorageItem]]dictionary to a JSON file. On relaunch, if the cache is within the TTL, results load in milliseconds.
StorageManager.scan()
│
├─ scanner.configure(ignoredNames:excludedPaths:globalMinBytes:)
│
├─ scanner.buildIndex(rootPath:) ← one fts_open pass, builds FileIndex
│
└─ withTaskGroup { for each category }
│
└─ scanner.scanCategory(_:rootPath:index:)
│
├─ .nodeModules → scanNodeModules ← FTS name-match + du -sk
├─ .topLevelDirs → scanTopLevelDirs ← shallow dirs + du -sk, size threshold
├─ .topLevelAll → scanTopLevelAll ← shallow files+dirs, size from fts stat
├─ .deepFiles → index.filter(ext:) ← O(n) in-memory (or scanFilesDeep)
├─ .deepLargeFiles → index.filter(min:) ← O(n) in-memory (or scanFilesDeep)
└─ .timeMachine → tmutil listlocalsnapshots
scanFilesDeep (fallback when index misses)
├─ expands root into parallel sub-dirs
├─ filters: ignoredNames, excludedPaths, extensions, minBytes
└─ fts stat for file size (no subprocess)
User selects → confirmation → deleteItems(_:from:mode:)
│
└─ performDeletion(_:mode:onItemDeleted:)
│
for each item:
├─ Task.detached { FileManager.trashItem / removeItem } ← background thread
├─ await result ← main actor suspended (UI responsive)
├─ onItemDeleted(item) → storageItems[category].remove ← item vanishes from list live
└─ publish: deletionCurrentName, deletionCompleted, deletionProgress
│
├─ scanner.evictCache(for: deleted)
└─ resultsCache.invalidate()
| Category | Paths | Strategy |
|---|---|---|
| Xcode Derived Data | ~/Library/Developer/Xcode/DerivedData |
Top-level dirs |
| Xcode Archives | ~/Library/Developer/Xcode/Archives |
Top-level dirs |
| Device Support | ~/Library/Developer/Xcode/*DeviceSupport |
Top-level dirs |
| iOS Simulators | ~/Library/Developer/CoreSimulator/Devices |
Top-level dirs |
| Node Modules | ~/Developer, ~/Documents, ~/Desktop, etc. |
Deep name search |
| CocoaPods Cache | ~/Library/Caches/CocoaPods |
Top-level dirs |
| Homebrew Cache | ~/Library/Caches/Homebrew |
Top-level dirs |
| Category | Paths | Strategy |
|---|---|---|
| Gradle Cache | ~/.gradle/caches, ~/.gradle/wrapper/dists |
Top-level dirs |
| Android Emulators | ~/.android/avd |
Top-level (files + dirs) |
| APK / AAB Files | ~/ (whole home) |
Deep file scan (.apk, .aab, .aar) |
| Category | Paths | Strategy |
|---|---|---|
| System Caches | ~/Library/Caches |
Top-level dirs (> 1 MB) |
| Application Support | ~/Library/Application Support |
Top-level dirs (> 1 MB) |
| Log Files | ~/Library/Logs |
Top-level dirs |
| Category | Paths | Strategy |
|---|---|---|
| Downloads | ~/Downloads |
Top-level (files + dirs) |
| Images | ~/ (whole home) |
Deep file scan (.jpg, .png, .heic, .webp, …) |
| Videos | ~/ (whole home) |
Deep file scan (.mp4, .mov, .mkv, .avi, …) |
| Large Files | ~/Documents, ~/Downloads, ~/Desktop |
Deep file scan (> 100 MB) |
| Category | Paths | Strategy |
|---|---|---|
| iOS Backups | ~/Library/Application Support/MobileSync/Backup |
Top-level dirs |
| Time Machine Snapshots | tmutil listlocalsnapshots |
Shell command |
Accessible via ⌘ , or the gear button in the toolbar.
| Tab | Contents |
|---|---|
| General | Color scheme (dark/light), default action after scan, deletion safety mode, reset all preferences |
| Scan | Performance mode, global minimum file size, ignored folder names, excluded paths, cache TTL, auto-scan schedule |
| Categories | Drag-and-drop reorder for sidebar and scan progress display |
| Notifications | Enable/disable storage alerts, threshold picker (75 – 95%) |
| Export | Format (CSV / JSON / Plain Text), include hidden categories, default export folder |
| Custom Scanners | Create and manage user-defined scanners by extension + directory |
- Old & Unused Files tool — shows files whose modification date is older than a segmented threshold (6, 12, or 24 months). Uses the shared file index when available after a full scan, otherwise merges paths from cached category results.
- No full re-scan after delete — items are removed from the in-memory list instantly; disk usage is recomputed from current state
- Background file I/O — deletions run on a detached task; the main actor is suspended (not blocked) so the UI stays at 60 fps
- Live deletion HUD — a floating progress card shows current file, count, and a progress bar; the list updates row-by-row as each file is removed
- Ignored patterns are scoped — ignored folder names (e.g.
node_modules) skip entries indeepFiles/deepLargeFilesscans; they do not affect the dedicated Node Modules category scanner which specifically targets those folders **.d.tsfiltering** — TypeScript declaration files are explicitly excluded from the Videos and other extension-matched scans- Thumbnail caching —
NSCachebacks per-path thumbnail generation; cache is cleared when the app is in the background - Auto-scan — uses
NSBackgroundActivityScheduler; fires a notification if disk usage exceeds the configured threshold
The app runs sandboxed with ENABLE_USER_SELECTED_FILES = readwrite.
On first launch an NSOpenPanel prompts for home directory access. The returned security-scoped URL is retained for the session — this grants read-write access to all subdirectories under ~/.
Full Disk Access (System Settings → Privacy & Security → Full Disk Access) is recommended for complete coverage including ~/Library. Use the lock icon in the toolbar to open the relevant settings pane.
Failed deletions (permission errors, system-protected files) surface a named-item alert rather than silently swallowing the error.
- macOS 14.0 (Sonoma)+
- Xcode 26.0+ (Swift 6.2 toolchain; the project compiles in Swift 5 language mode)
To build and install a Release copy, see Install.
- Add a case to
StorageCategoryinModelsStorageCategory.swift - Implement
scanStyle,title,icon,color,description, andpaths(from:)for the new case - The existing scan strategies (
topLevelDirs,deepFiles,deepLargeFiles, etc.) cover most cases — only add a newScanStylecase if none fit - If a new traversal is needed, add a private method to
ServicesStorageScanner.swiftand a corresponding FTS function inServicesFastScanEngine.swift
- Add a case to
ToolTypeinModelsToolType.swift(title,icon,color,description) - Create
ViewsYourToolView.swiftand switch it inToolRouterViewinsideContentView.swift - Add any dedicated scanner logic under
Services/and wire throughStorageManageras needed