sysmon is a lightweight, sandbox-compliant macOS application that displays real-time CPU and Memory utilization in two locations:
- Menu Bar — via SwiftUI's
MenuBarExtra(macOS 13+) - Notification Center Widget — via WidgetKit
The app uses only public, approved C/Swift system APIs (host_statistics,
host_statistics64, sysctl) — no private frameworks, kernel extensions,
or shell-command scraping. It is built from the ground up for Mac App Store
submission and hardened notarization.
Build the DMG from source — no Xcode required:
./scripts/build_from_source.shRequirements: macOS 13+ and Xcode Command Line Tools (xcode-select --install).
Output: build/sysmon-latest.dmg
macOS Gatekeeper warning: If macOS blocks the app with "Apple could not verify sysmon is free of malware", see Gatekeeper Workaround below.
For a distribution-signed build:
SYS_SIGN_IDENTITY='Developer ID Application: Your Name (TEAMID)' ./scripts/build_from_source.sh- Project Architecture
- Core System Monitoring Engine
- SwiftUI User Interface
- Deployment & Distribution
- Directory Layout Reference
sysmon is built with swiftc directly — no Xcode GUI or xcodebuild required.
./scripts/build_from_source.sh
The script compiles the main app and WidgetKit extension, assembles the
.app bundle (including Contents/PlugIns/ for the widget), generates
entitlements, code-signs, and packages a DMG — all from a single command.
Requirements: macOS 13+ and Xcode Command Line Tools (xcode-select --install).
Both targets share data via App Groups. The App Group ID used throughout the code is:
group.com.sysmon.shared
Entitlements are auto-generated into build/ at build time. No manual
configuration needed. The .entitlements files in this repository contain
the required keys. If you change the App Group ID, update
AppGroupStore.swift accordingly.
host_statistics, host_statistics64, and sysctl operate entirely
within the App Sandbox. No temporary exceptions or special entitlements
are required. The only entitlements needed are:
| Entitlement | Purpose |
|---|---|
com.apple.security.app-sandbox |
Required for Mac App Store |
com.apple.security.application-groups |
Share stats with widget |
No network, file-access, camera, microphone, or Bluetooth entitlements are used.
A thread-safe actor-lite class (@unchecked Sendable) that:
-
Uses
host_statistics()withHOST_CPU_LOAD_INFOto read accumulated CPU ticks (user, system, idle, nice) and computes per-state percentages. -
Uses
host_statistics64()withHOST_VM_INFO64to read VM page counts (free, active, inactive, wired, compressed) and multiplies by the kernel page size to get byte values. -
Uses
sysctlbyname("hw.memsize")to get total physical memory. -
Runs a low-overhead
DispatchSourceTimeron a.utilityQoS queue at a configurable interval (default 2 seconds). Each invocation is O(1). -
Exposes an
AsyncStream<SystemSnapshot>so that SwiftUI views can consume updates withfor awaitloops. -
Persists the latest snapshot into the App Group
UserDefaultssuite viaAppGroupStore, so the Widget Extension'sTimelineProvideralways has fresh data.
Immutable value types conforming to Sendable:
| Type | Fields |
|---|---|
CPUStats |
systemLoad, userLoad, systemCPULoad, niceLoad (0–100 %) |
MemoryStats |
totalBytes, usedBytes, wiredBytes, activeBytes, inactiveBytes, freeBytes, compressedBytes, usagePercentage, usedGB, totalGB |
SystemSnapshot |
cpu: CPUStats, memory: MemoryStats, timestamp: Date |
sysmonApp.swift — The @main entry point. Creates a MenuBarExtra
scene with a custom label view and a window-style dropdown.
MenuBarLabelView.swift — The compact text row displayed in the menu
bar:
[CPU icon] 42% | [RAM icon] 68%
StatsDetailView.swift — The dropdown panel (280–300 pt wide) showing:
- CPU section: progress bar, user/system/idle breakdown
- Memory section: progress bar, used/total GB
- Footer: last-updated timestamp, refresh button, Quit button (⌘Q)
MenuBarViewModel.swift — ObservableObject that owns the
SystemMonitorEngine, subscribes to its AsyncStream, publishes
@Published properties, and flushes each snapshot to AppGroupStore.
sysmonWidget.swift — The Widget definition. Uses a
StaticConfiguration with SystemStatsProvider as the timeline provider.
Supported families: .systemSmall, .systemMedium.
SystemStatsProvider — Implements TimelineProvider:
placeholder— Static dummy data for the widget gallery.getSnapshot— ReadsAppGroupStore.latestSnapshot.getTimeline— Reads latest snapshot, schedules next refresh in 60 s.
SystemStatsWidgetView.swift — SwiftUI rendering:
- Small: Two stacked
Gaugeviews (CPU + RAM) with circular capacity style. - Medium: Horizontal layout with two gauges, percentage text, and memory used/total GB text.
./scripts/build_from_source.shThe script will:
- Detect the macOS SDK automatically via
xcrun - Compile the main app and widget extension with
swiftc - Assemble the
.appbundle (Info.plists, PlugIns directory, etc.) - Generate entitlements on-the-fly
- Code-sign both targets — ad-hoc by default, or with a Developer ID
identity if
SYS_SIGN_IDENTITYis set - Verify the code signature
- Package everything into
build/sysmon-latest.dmgviahdiutil
For a distribution-signed build:
SYS_SIGN_IDENTITY='Developer ID Application: Your Name (TEAMID)' ./scripts/build_from_source.shsysmon is distributed as a Homebrew Cask through a custom Tap. Run these three commands to find and install it:
brew tap mickyballadelli/sysmon
brew trust mickyballadelli/sysmon
brew search sysmonWhat these commands do:
| Command | Purpose |
|---|---|
brew tap mickyballadelli/sysmon |
Adds the sysmon Tap repository (github.com/mickyballadelli/homebrew-sysmon) to your local Homebrew installation so Homebrew can discover the Cask. |
brew trust mickyballadelli/sysmon |
Marks the Tap as trusted. When a tap is trusted, Homebrew will install its formulae/casks without prompting for confirmation each time. This is safe for taps you control or trust. |
brew search sysmon |
Searches all tapped repositories (including mickyballadelli/sysmon) for packages matching "sysmon". You should see mickyballadelli/sysmon/mickyballadelli-sysmon in the results. |
Install:
brew install --cask mickyballadelli/sysmon/mickyballadelli-sysmonUninstall (the Cask's zap trash: section removes all app data):
brew uninstall --cask --zap mickyballadelli-sysmonThe zap trash: directives clean:
~/Library/Application Scripts/group.com.sysmon.shared~/Library/Application Support/sysmon~/Library/Caches/com.sysmon.app~/Library/Containers/com.sysmon.app~/Library/Containers/com.sysmon.app.widget~/Library/Group Containers/group.com.sysmon.shared~/Library/HTTPStorages/com.sysmon.app~/Library/Preferences/com.sysmon.app.plist~/Library/Saved Application State/com.sysmon.app.savedState~/Library/WebKit/com.sysmon.app
The Cask lives at Casks/mickyballadelli-sysmon.rb. Homebrew only discovers Casks
from this top-level Casks/ directory.
1. Build a distribution DMG:
export SYS_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDEF1234)"
./scripts/build_from_source.shNotarize and staple build/sysmon-latest.dmg before publishing it.
The build script can handle notarization automatically if you set
SYS_NOTARY_KEYCHAIN_PROFILE or SYS_NOTARY_APPLE_ID + SYS_NOTARY_TEAM_ID.
2. Create the release and update the Cask:
./scripts/release_tap.sh 1.0 build/sysmon-latest.dmg
./scripts/generate_cask.sh build/sysmon-latest.dmg 1.0Commit and push the updated Cask:
git add Casks/mickyballadelli-sysmon.rb
git commit -m "Update mickyballadelli-sysmon to v1.0"
git push origin main3. Verify the published Tap:
./scripts/deploy_brew.shxcrun notarytool submit build/sysmon-latest.dmg \
--apple-id "your@email.com" \
--team-id ABCD123456 \
--wait
xcrun stapler staple build/sysmon-latest.dmgOnce the DMG is built (and optionally notarized), publish it as a GitHub Release with a single command:
./scripts/release.sh 1.0.0This script will:
- Create and push an annotated git tag (
v1.0.0) - Generate release notes from
CHANGELOG.md(or use sensible defaults) - Create the GitHub Release via
gh - Attach
build/sysmon-latest.dmgas a downloadable asset
Requirements: GitHub CLI (brew install gh)
and authentication (gh auth login).
The full release pipeline from source to GitHub is two commands:
./scripts/build_from_source.sh # compile → DMG
./scripts/release.sh 1.0.0 # tag → release → uploadsysmon/
├── .gitignore
├── CHANGELOG.md
├── README.md
├── Shared/
│ ├── SystemMonitorData.swift # Data models
│ ├── SystemMonitorEngine.swift # Core sampling engine
│ └── AppGroupStore.swift # App Group persistence
├── sysmon/
│ ├── sysmonApp.swift # @main entry point
│ ├── MenuBarViewModel.swift # ObservableObject binding
│ ├── MenuBarLabelView.swift # Menu bar text label
│ ├── StatsDetailView.swift # Drop-down detail panel
│ ├── Info.plist
│ └── sysmon.entitlements
├── sysmonWidget/
│ ├── sysmonWidget.swift # Widget definition + provider
│ ├── SystemStatsWidgetView.swift # Widget SwiftUI views
│ ├── Info.plist
│ └── sysmonWidget.entitlements
└── scripts/
├── build_from_source.sh # Standalone CLI build (no Xcode required)
├── deploy_brew.sh # Published Homebrew Cask verifier
├── generate_cask.sh # DMG → Cask generator (for Homebrew Tap)
├── release.sh # GitHub Release creator (main repo)
└── release_tap.sh # DMG uploader (Tap repo releases)
---
## Gatekeeper Workaround
If you built sysmon locally without a paid Apple Developer account (ad-hoc
signature), macOS Gatekeeper will block the app with:
> *"sysmon" cannot be opened because Apple cannot verify it is free of malware.*
This is expected — only Developer ID-signed + notarized apps pass Gatekeeper
automatically. Use one of these methods to bypass it:
### Method 1: System Settings (recommended)
1. Open **System Settings → Privacy & Security**
2. Scroll to the **Security** section at the bottom
3. You will see: `"sysmon" was blocked to protect your Mac`
4. Click **Open Anyway**
5. Confirm by clicking **Open Anyway** in the popup
### Method 2: Right-click open
1. In Finder, navigate to `build/sysmon.app`
2. **Right-click** (or Control-click) the app icon
3. Select **Open** from the context menu
4. Click **Open** in the dialog that appears
This adds a one-time exception and you won't be prompted again.
### Method 3: Remove quarantine attribute (terminal)
```bash
xattr -cr build/sysmon.app
This strips the com.apple.quarantine extended attribute that macOS attaches
to downloaded files.
When you first launch sysmon (or after clearing its sandbox container), macOS may show:
"Keeping app data separate makes it easier to manage your privacy and security."
This is normal and expected — it's macOS creating the sandbox container for the app. It only appears once per user account. Click OK to dismiss it. This happens because sysmon uses the App Sandbox entitlement, which is required for Mac App Store submission and provides defense-in-depth security.
If the permission dialog keeps reappearing even after clicking Allow, the
app's sandbox container may be in a bad state. This typically happens when
running the app from outside /Applications (e.g., from the build/
directory). To fix it:
Step 1 — Copy (not move) the app to /Applications:
cp -R build/sysmon.app /Applications/sysmon.app
xattr -cr /Applications/sysmon.appStep 2 — Reset sandbox containers via Finder (not Terminal):
The ~/Library/Containers folder is protected by System Integrity Protection
and requires Full Disk Access — using Finder is more reliable:
-
Open Finder and press ⌘⇧G (Go to Folder)
-
Paste each of these paths one at a time and delete the folder:
~/Library/Containers/com.sysmon.app~/Library/Containers/com.sysmon.app.widget~/Library/Group Containers/group.com.sysmon.shared
If a folder doesn't exist, skip it and move to the next.
Step 3 — Reset TCC permissions from Terminal:
This command works without extra permissions:
tccutil reset All com.sysmon.appThen launch /Applications/sysmon.app — the prompt should appear only once
and stay accepted after clicking Allow.
If "Operation not permitted" occurs on
rm -rf: Terminal needs Full Disk Access to delete sandbox containers. Grant it in System Settings → Privacy & Security → Full Disk Access → add Terminal. Alternatively, use Finder as described in Step 2.
-
Ad-hoc signed builds (the default when no
SYS_SIGN_IDENTITYis set) are not trusted by Gatekeeper. -
Developer ID signed + notarized builds pass Gatekeeper without any workaround. To produce one, you need a paid Apple Developer Program membership ($99/year) and run:
SYS_SIGN_IDENTITY='Developer ID Application: Your Name (TEAMID)' \ SYS_NOTARY_KEYCHAIN_PROFILE=sysmon-notary \ ./scripts/build_from_source.shSee Notarize & Staple for details on setting up notarization credentials.