Minimal context with maximum flexibility — macOS system tools for AI agents. Native Apple frameworks. AI-friendly JSON/TSV output.
macli turns macOS system internals into a clean CLI. SMC sensors, streaming monitor, calendar/reminders — all callable from shell pipes or LLM agents, all JSON/TSV. One ~400 KB Swift binary. No Python runtime, no osascript overhead, no GUI.
Use it when you (or your AI agent) need to ask macOS something that system_profiler / ioreg / osascript either can't answer or answer badly: CPU die temperature right now, stream 1 Hz sensor readings into awk, today's calendar as JSON.
macli is maintained by the core team listed in CODEOWNERS.
All changes to main require a pull request and approval from a code owner.
See CONTRIBUTING.md for details.
Mirrors: github.com/ljh-sh/macli · codeberg.org/ljh-sh/macli
Docs: ljh-sh.github.io/macli
Minimal context with maximum flexibility — paste this one-line prompt into Claude Code, Cursor, or any agent's system prompt:
Use `macli` for macOS system state (sensors / battery / calendar / reminders). Install if missing: `brew install ljh-sh/cli/macli`. JSON output, check `ok`. Run `macli --help` for subcommands.brew install ljh-sh/cli/macliOr tap once, then use the short name:
brew tap ljh-sh/cli
brew install maclicurl -L https://github.com/ljh-sh/macli/releases/latest/download/macli-darwin-universal.tar.xz | tar xJ -
sudo mv bin/macli /usr/local/bin/The universal tarball is a fat Mach-O (arm64 + x86_64) — works on Apple Silicon and Intel Macs.
Via x-cmd eget:
x eget use ljh-sh/macli # install latest to ~/.local/bin
x eget use --tag v0.4.2 ljh-sh/macli # install a specific releaseAfter install, verify the package with npm audit signatures.
npm install -g @ljh-sh/macliThe npm package downloads the universal macOS binary from the GitHub release during install.
Requires Swift 5.10+ / macOS 12+.
git clone https://github.com/ljh-sh/macli
cd macli
swift build -c releasemacli smc temp # CPU/GPU temps as JSON
macli gpu info # GPU name, cores, unified memory
macli display brightness # built-in display brightness
macli monitor --count 10 --interval 1 # stream 10 samples to awk
macli cal ls # list calendars as JSONOutput schema: {"ok": true, ...} on success, {"ok": false, "error": "...", "hint": "..."} on failure. Never silent.
See ROADMAP.md for the current roadmap and completed milestones.
The headline use case. macli smc reads hardware sensors that macOS exposes only through private frameworks.
macli smc temp # → JSON, all temperature sensors
macli smc temp --tsv # → TSV for awk
macli smc volt # → PMU voltage rails
macli smc curr # → PMU current rails
macli smc all # → everythingSample output:
{
"ok": true,
"source": "HID",
"sensors": [{"name": "PMU tdie1", "value": 57.5, "unit": "°C"}],
"count": 45
}macli battery reads IOKit/AppleSmartBattery and returns the richest,
script-friendly battery snapshot on macOS. It is a strict superset of
ioreg -n AppleSmartBattery -r for useful data: 150+ fields, per-cell voltages,
USB-C PD telemetry, and decoded binary blobs like the per-cell resistance table
RaTableRaw.
- JSON / TSV / raw plist — pipe into
jq,awk, spreadsheets, or time-series DBs. - Derived power metrics —
healthPercent,designWh,currentWh,estimatedFullChargeWh,instantPowerWatts,cellVoltageDelta,charger.powerWatts. - Decoded binary fields —
raTableRaw(per-celluint16resistance tables),batteryStateBytes,charger.statusBytes,mfgDataAscii. - Per-port USB-C PD diagnostics —
fedDetails,portControllerInfo,adapter.usbHvcMenuPDOs. - Battery identity cross-check — compare
serialNumbervsbatterySerial, inspectmanufactureDateand manufacturer data.
macli battery # full JSON snapshot
macli battery --tsv # spreadsheet / awk friendly
macli battery --plist # raw AppleSmartBattery plist
# Core health and status
macli battery | jq '{status, stateOfCharge, healthPercent, cycleCount, temperature}'
# Power flow right now
macli battery | jq '{instantPowerWatts, batteryPower, systemPower, inputPower, charger: .charger.powerWatts}'
# Per-cell balance (delta > 0.05 V warrants attention)
macli battery | jq '{cellVoltages, cellVoltageDelta}'
# Adapter PDOs and per-port PD partners
macli battery | jq '{adapter: .adapter.usbHvcMenu, fedDetails}'# 1. Alert when health drops below 80%
macli battery | jq -e '.healthPercent < 80' && echo "consider replacement"
# 2. One-line battery report for remote support
macli battery | jq -r '"\(.deviceName) \(.cycleCount) cycles health=\(.healthPercent)% temp=\(.temperature)°C"'
# 3. Detect possible third-party / mismatched battery
macli battery | jq '{serialNumber, batterySerial, same: (.serialNumber == .batterySerial)}'
# 4. Log a time-series line every minute (CSV-ish)
macli battery | jq -r '[now, .stateOfCharge, .healthPercent, .temperature, .instantPowerWatts] | @tsv' >> battery.tsv
# 5. Compare current charge power to adapter rating
macli battery | jq '{inputPower: .inputPower, chargerPower: .charger.powerWatts, systemPower: .systemPower}'
# 6. Inspect decoded manufacturer ASCII inside the binary MfgData blob
macli battery | jq -r '.mfgDataAscii // "N/A"'
# 7. Stream battery power to awk / InfluxDB / Prometheus
macli monitor --metric battery_power --interval 1 --count 60
# 8. Find the hottest cell and the coldest cell
macli battery | jq -r '.cellVoltages | "max=\(max), min=\(min), delta=\(max - min)"'
# 9. Pretty-print the full resistance table for each cell
macli battery | jq '.raTableRaw'
# 10. Check if AC is connected but the battery is not charging (and why)
macli battery | jq '{externalConnected, isCharging, fullyCharged, notChargingReason: .charger.notChargingReason}'macli battery is read-only — it observes and reports, but does not control charging. To set charge limits or pause charging, use System Settings → Battery → Battery Health (official), or third-party tools like batt (Apple Silicon) / bclm (Intel). See docs/battery.md for the full guide and trade-offs.
macli ssd parses system_profiler SPNVMeDataType. It returns model, serial,
capacity, SMART status, TRIM support and volumes. It does not parse detailed
SMART log pages because Apple does not expose them through a public API. For
wear-level data (TBW, percentage used, media errors, etc.) use smartctl:
brew install smartmontools
smartctl -a disk0SSD compatibility:
| Platform | Basic info (macli ssd) |
Detailed SMART |
|---|---|---|
| Apple Silicon internal SSD | ✅ | Use smartctl |
| External Thunderbolt NVMe | ✅ | Use smartctl |
| USB/SATA adapters | May appear as storage, not NVMe | Use smartctl |
macli display reads and sets display brightness through the private
DisplayServices framework. macli gpu reports the active GPU via Metal and
reads core count / performance counters from AGXAccelerator in IOKit.
macli display list # all online displays + brightness
macli display brightness # current brightness (0.0–1.0)
macli display brightness --set 0.5 # set built-in display brightness
macli gpu info # name, unified memory, core count
macli gpu info --tsv # tab-separated outputmonitor can also stream GPU utilization (experimental, Apple Silicon only):
macli monitor --metric gpu_metrics --interval 1macli follows the x-cmd agent-tool design principle: minimal context with maximum flexibility. It stays dumb — it does not compute thermal indexes, aggregate, render charts, or decide what's "hot". It returns raw sensor values, full stop. Decisions belong to the caller:
macli smc temp --tsv | awk -F'\t' '$2 > 80 {print $1, "OVERHEAT"}'
macli smc temp --tsv | sort -t$'\t' -k2 -n | tail -5 # 5 hottest sensorsThis keeps macli --help short (saves tokens when an LLM loads it as context). The CLI is the API; the shell is the glue.
smc86 is the Intel-Mac counterpart, same interface. Returns empty on Apple Silicon (Intel SMC key space was cleared). Will be removed when Intel Macs go EOL.
monitor samples sensor sources on an interval and streams TSV — one row per sample. Single process, no subprocess fork per poll, no Python interpreter per tick. Designed as a long-running pipe stage for awk.
macli monitor --interval 1 --metric smc_temp,smc_curr
macli monitor --count 10 --interval 0.5 --metric smc_temp \
| awk -F'\t' 'NR>1 {sum+=$2; n++} END {print "avg:", sum/n}'
macli monitor --metric gpu_metrics --interval 1 # GPU utilization (experimental)Flags:
--interval N— seconds between samples (supports decimals, default 1.0)--metric list— comma-separated source or column-prefix filters (default: all). Sources:smc_temp,smc_volt,smc_curr,battery_power,gpu_metrics. Prefixes such assmc_temp_cpuselect only matching columns.--count N— exit after N samples (default: infinite, Ctrl-C to stop)
The header row locks column order; subsequent rows match positionally. awk -F'\t' is the intended downstream.
Why this matters: a shell loop (while; do macli smc temp; sleep 1; done) costs ~50ms of binary startup per iteration. monitor pays that once and streams samples at sub-millisecond marginal cost.
EventKit.framework is Apple's native API for Calendar and Reminders. macli wraps it for the shell — JSON output, no AppleScript involved.
macli cal ls # list calendars
macli event ls --calendar Work --today # today's events
macli reminder add --list Shopping "Buy milk"
macli aka set work <calendar-id> # alias calendar IDs for stable refsUse cases: dashboards, CI notifiers ("next event in 5 min"), reminder batching, automation hooks that need stable calendar references (aka).
- Snapshot commands: JSON with
{"ok": bool, ...}(default).--tsvfor awk-friendly. - Streaming commands (
monitor): TSV only, header on first line. - Errors:
{"ok": false, "error": "...", "hint": "..."}— never silent.
The full FAQ lives on the docs site: ljh-sh.github.io/macli/faq.
Source: docs/faq.md.
macli ships with ad-hoc signature (no Apple Developer ID). For direct-download installs, strip the quarantine attribute:
xattr -dr com.apple.quarantine /usr/local/bin/macliThe Homebrew formula does this automatically via post_install.
Homebrew 6 added a trust step for third-party taps. Run brew trust ljh-sh/cli once, then brew install ljh-sh/cli/macli. This is a security feature, not a bug.
macOS TCC is prompting for Calendar/Reminders access. Click the system dialog to grant. Subsequent calls are instant. If you missed the prompt, go to System Settings → Privacy & Security → Calendars (or Reminders) and enable the terminal you're running macli from.
The System Management Controller (SMC) is an Apple controller embedded in every Mac. It monitors and reports CPU / GPU / SoC die temperatures, PMU voltage rails, PMU current rails, fan speeds (Intel Macs), and battery state.
On Intel Macs, SMC is queried through IOKit.framework's private AppleSMC API using 4-character keys (TCXC, TG0P, …). On Apple Silicon (M1–M4), the same data moved to a HID sensor hub — the keys are entirely different (PMU tdie1, PMU tdie2, …) and undocumented.
References — the projects that mapped this out:
- dkorunic/iSMC — Go CLI, comprehensive SMC key catalog (Intel + Apple Silicon)
- beltex/SMCKit — Swift SMC library, the classic Intel-era reference
- freedomtan/sensors — early Apple Silicon IOKit exploration
Reading one sensor takes ~30 lines of C: open the AppleSMC / AppleHID IOService, serialize the key, call IOConnectCallScalarMethod, unpack the returned struct. The keys are private, the structs are private, the call convention changed between Intel and Apple Silicon.
PyObjC can call public frameworks, but the SMC key space is private. Reaching it from Python means ctypes-level struct packing that breaks with every macOS release. There is no pip install path that keeps up with Apple Silicon's new key namespace.
Expected. smc86 queries the Intel-Mac SMC key space, which Apple cleared on Apple Silicon. Use macli smc (not smc86) on M-series Macs.
- iStats — Ruby gem, Intel-only, last released 2018. GUI-leaning.
- smcFanControl — macOS app for setting minimum fan speed. GUI.
- stats — macOS menu-bar dashboard. GUI.
- iSMC — Go CLI. Closest peer, but Go runtime adds ~5 MB to the binary.
- SMCKit — Swift library, Intel-only. No CLI streaming, no EventKit.
- macli — Swift CLI built for shell pipes and LLM agents. JSON/TSV only, no GUI, no Ruby/Go runtime, Apple Silicon first-class. Smallest binary in the list (~400 KB stripped).
osascript routes through AppleScript → Calendar.app RPC channel → permission prompts. Every cold start loads the AppleScript component. macli links EventKit.framework directly and requests permission once via the standard TCC prompt; subsequent calls are in-process.
AppleScript returns human-formatted strings like {calendar "Work", calendar "Home"}. Parsing that requires regex on localized strings. JSON is parseable by jq, python, awk, every LLM tool-use interface, with stable field names regardless of system language.
Read commands (cal ls, event ls) never touch state. Write commands (cal add, event add, reminder add) only run when you invoke them explicitly, with the exact arguments you pass. macli never syncs, never deletes, never auto-modifies.
No. macli wraps Apple-private frameworks (IOKit, HID, EventKit) that exist only on macOS.
No. All subcommands run as the invoking user. Sensor reads go through user-space IOKit / HID APIs.
Both supported via a single universal binary. On Apple Silicon use macli smc; on Intel use macli smc86. The binaries are identical; the subcommand selects the sensor path.
~400 KB per arch (arm64 / x86_64), ~830 KB universal (fat Mach-O), ~110 KB arm64 tar.xz. Single static-ish binary — no Python runtime, no PyObjC bridge, no ctypes layer.
Ad-hoc. Not Apple Developer ID (would require $99/year and notarization for marginal benefit). The Homebrew formula strips com.apple.quarantine automatically. Manual installs need one xattr -dr.
Mostly yes.
- Reproducibility depends on pinning Xcode / LLVM versions.
- Build hardening —
SOURCE_DATE_EPOCH,ZERO_AR_DATE, deterministic mtimes, RPATH removal — lives in.x-cmd/release.common.sh.
Short version: macOS won't let a bare CLI use speech recognition.
SFSpeechRecognizerneeds anInfo.plistwithNSSpeechRecognitionUsageDescription.- SwiftPM CLI binaries don't have one, so TCC denies access and the process crashes.
- Fixing it means bundling macli as a
.app, which isn't worth the pipeline complexity. - Use hear instead — it's signed, notarized, and does exactly this job.
Aggregation (avg, max, rolling window) and alerting (threshold → notify) belong in awk/jq/python where you control the semantics. Embedding them in macli would mean every new stat needs a new flag, and the --help would balloon past what an LLM can cheaply load as context. See "Design: agent-oriented" above.
See changelog/ for versioned release notes, starting with v0.0.0.md.
- Built with Swift and Apple native frameworks.
- Release signing uses Sigstore and cosign.
- SLSA provenance generated by the OpenSSF slsa-github-generator.
macli releases are built deterministically in GitHub Actions. The build pins
SOURCE_DATE_EPOCH, disables variable timestamps, and produces bit-for-bit
reproducible tarballs on Apple Silicon. See .x-cmd/release.common.sh for the
exact environment and BUILD_INFO.txt in each release for build parameters.
- Read the docs.
- Open a discussion for questions.
- Open an issue for bugs or feature requests.
This project follows the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.
Apache 2.0 — see LICENSE.txt.
Release tarballs are signed with Sigstore/cosign. Each
release includes .sigstore.json bundles for the tarballs, SHA256SUMS, and
BUILD_INFO.txt. Verify a tarball with:
cosign verify-blob \
--bundle macli-darwin-universal.tar.xz.sigstore.json \
--certificate-identity-regexp '^https://github.com/ljh-sh/macli/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
macli-darwin-universal.tar.xzThe npm package @ljh-sh/macli is published from GitHub Actions. You can verify
its provenance with:
npm install -g @ljh-sh/macli
npm audit signatures