Contributing to Mac Clean
May 30, 2026 · View on GitHub
Thank you for your interest in contributing! Mac Clean is a community-driven project and we welcome contributions of all kinds — bug fixes, new features, documentation improvements, and more.
Code of Conduct
Be respectful, constructive, and inclusive. We're building software together.
Getting Started
- Fork the repository
- Clone your fork:
git clone https://github.com/YOUR_USERNAME/MacClean.git - Build:
swift build - Run tests:
swift test(must pass with 0 failures) - Create a branch:
git checkout -b feature/your-feature
Pull Request Guidelines
Before Submitting
- Your code compiles with
swift build(zero errors) - All tests pass (
swift test) - You've added tests for new functionality (see "Testing pattern" below)
- No new compiler warnings introduced
- You've tested the feature in the running app (not just compilation)
- Safety-critical changes (
SafetyGuard.swift,CleaningEngine.swift,PlistJunkFilter.swift) come with adversarial test cases
PR Format
## Summary
Brief description of what this PR does and why.
## Changes
- Bullet list of specific changes
## Test Plan
- How you tested this
- Edge cases considered
## Screenshots
If UI changes, include before/after screenshots.
PR Size
- Keep PRs focused — one feature or fix per PR
- Large features should be broken into smaller, reviewable chunks
- Refactors should be separate from feature work
Commit Messages
- Use present tense: "Add feature" not "Added feature"
- First line under 72 characters
- Reference issues when applicable: "Fix #42: handle empty scan results"
Testing pattern
This is the architectural rule that every PR must follow. It's how the codebase stays testable without dragging in real filesystem state.
Rule: Business logic lives in MacCleanKit as pure functions; system
dependencies (FileManager, NSWorkspace, Process, Mach APIs) are injected
as closures at the boundary. The thin wrappers in the MacClean target are
where real implementations get wired up.
Example: the PlistJunkFilter pattern
// ❌ Don't do this — untestable, mixes logic with FS calls
struct BrokenPreferencesCategory: JunkCategory {
func filterBroken(_ items: [FileItem]) -> [FileItem] {
items.filter { item in
guard let data = try? Data(contentsOf: item.url) else { return true }
// ... decision logic mixed with FS state ...
}
}
}
// ✅ Do this — pure function in Kit, injected loader, fully testable
public enum PlistJunkFilter {
public static func isLikelyBroken(
at url: URL,
loadData: (URL) -> Data?, // injected
appExistsForBundleID: (String) -> Bool // injected
) -> Bool {
// pure decision logic — no I/O
}
}
// And the thin wrapper in MacClean target
struct BrokenPreferencesCategory: JunkCategory {
func filterBroken(_ items: [FileItem]) -> [FileItem] {
items.filter { item in
PlistJunkFilter.isLikelyBroken(
at: item.url,
loadData: { try? Data(contentsOf: \$0) },
appExistsForBundleID: { NSWorkspace.shared.urlForApplication(...) != nil }
)
}
}
}
Available test fixtures
Use these helpers from Tests/MacCleanTestSupport/ so your tests don't touch
the real home directory:
TestFixtures.withTempDir { dir in ... }— temp dir, auto-cleanedTestFixtures.withTempHome { fakeHome in ... }— synthetic~/Library/...treeTestFixtures.writeFakeApp(at:bundleIdentifier:name:)— synthetic.appbundleTestFixtures.writePlist(_:to:)andwriteCorruptPlist(at:)— synthetic plistsTestFixtures.writeFile(at:size:modificationDate:contents:)— synthetic filesMockClock— controllable time for date-based logic
Where tests live
Tests/MacCleanKitTests/— pure unit tests againstMacCleanKitonlyTests/MacCleanTests/— integration tests that exercise theMacCleanshellTests/MacCleanTestSupport/— shared fixture helpers (don't add test cases here)
Code Style
Swift Conventions
- Swift 6 with strict concurrency — use actors,
@Sendable,async/await - Use
@Observablefor view models (notObservableObject) - Prefer
async/awaitover completion handlers - Use
TaskGroupfor parallel work - No force unwrapping (
!) except in tests
Architecture
- Modules implement the
ScanModuleprotocol - Views use
ModuleContainerViewfor consistent scan/results/done flow - Safety first — all file operations go through
SafetyGuardandCleaningEngine - Keep scanning logic in modules, not in views
File Organization
New module? Follow this structure:
Sources/MacClean/Modules/YourModule/
├── YourModuleModule.swift # Implements ScanModule
└── (optional helpers)
Sources/MacClean/Views/YourSection/
└── YourModuleView.swift # SwiftUI view
What NOT to Do
- Don't bypass
SafetyGuardfor file operations - Don't add network calls without discussion (Mac Clean is offline-first)
- Don't add telemetry or analytics
- Don't add third-party dependencies without an issue discussion first
- Don't modify protected paths lists without security review
Types of Contributions
Bug Reports
Open an issue with:
- macOS version
- Steps to reproduce
- Expected vs actual behavior
- Console output (if relevant)
Feature Requests
Open an issue describing:
- What the feature does
- Why it's useful
- How CleanMyMac (or similar tools) handle it (if applicable)
New Scan Categories
To add a new System Junk category:
- Create a new file in
Sources/MacClean/Modules/SystemJunk/Categories/ - Implement the
JunkCategoryprotocol - Add it to the
SystemJunkModulecategories array - Add a corresponding
ScanCategoryenum case - Add tests in
MacCleanTestRunner
New Modules
To add a new scan module:
- Create
Sources/MacClean/Modules/YourModule/YourModuleModule.swift - Implement
ScanModuleprotocol - Create the view in
Sources/MacClean/Views/ - Add sidebar entry in
SidebarView.swift - Wire it up in
ContentView.swift - Register in
AppState.swift - Add tests
Security
If you discover a security vulnerability, please do not open a public issue. Instead, email the maintainers directly. We take security seriously and will respond promptly.
Security Review Required For
- Changes to
SafetyGuard.swift - Changes to protected paths in
Constants.swift - Changes to
CleaningEngine.swift - Changes to XPC helper operations
- Any new file deletion logic
Development Tips
Running the App
# Quick launch (builds + creates .app bundle + opens)
swift build && cp "$(swift build --show-bin-path)/MacClean" \
/tmp/MacClean.app/Contents/MacOS/MacClean && \
open /tmp/MacClean.app
Dry-Run Mode
The cleaning engine defaults to dryRun mode during development. To test actual cleaning:
- Change
.dryRunto.trashin the relevant view'sclean()method - Never use
.permanentduring development - Revert before committing
Full Disk Access
Some modules need FDA to find results. If your scan returns empty:
- Build the app bundle (see README)
- Grant FDA in System Settings
- Restart the app
Questions?
Open a discussion or issue — we're happy to help you get started.