Safety model

Giving an agent a Mac is a trust decision. macos-mcp is built so that the decision is bounded, inspectable and reversible — in code that ships, not in a policy document.

Hard lines

Some things the server will not do under any profile, for any caller, with any token:

Two things that widen the blast radius, and how they are fenced

The ios toolset and macos-mcp vm both extend an agent's reach past the Mac in front of it. Both are opt-in, and neither loosens anything above.

Permission profiles

ProfileWhat it allows
readonlyOnly tools annotated read-only: snapshots, screenshots, OCR, reads of the clipboard, calendar, contacts, tabs. Everything else is refused with a typed error naming the profile.
standard (default)Everything except raw applescript_eval. Tools annotated destructive need a confirmation token (below).
fullTrusts the caller: destructive tools run directly and raw AppleScript/JXA is allowed.

The profile is enforced in the one dispatcher every tool passes through, so mounted Apple tools (web_*, xcode_*) obey it through their wire annotations without any per-tool code.

Confirmation tokens

Under standard, a destructive call — trash, app_quit, applescript_eval, a mounted tool that declares destructiveHint — does not run. It returns confirmation_required with a one-shot token bound to that exact call: the same tool, the same arguments. Re-issue the call with the token and it runs once; the token is validated, consumed and stripped before the tool sees it, and it authorizes nothing else. An agent that wants to delete something has to say so twice, in the same words.

Clients that support MCP elicitation get the same handshake as a user-facing prompt.

Verify, never trust the return code

This is the property most macOS automation gets wrong. Accessibility writes (AXUIElementSetAttributeValue) and synthetic events (CGEvent) both report success while doing nothing — a field that ignores the value, an event dropped by an untrusted process, a click delivered to a window that was covered. macos-mcp reads every effect back:

Where there is genuinely no read-back — a drag's effect is app-specific — the result says verified: false and tells the agent to re-snapshot. Honesty about uncertainty beats a confident lie.

Typed, self-healing errors

Failures are a closed set. Every error carries a stable codepermission_missing, element_not_found, invalid_argument, refused, action_unverified, and the rest listed in the tool reference — plus what an agent needs to recover: the missing grant and its System Settings deep-link, the element ids visible right now, what the element actually offers. Prose-only errors are banned by test.

Audit log

Every call appends a line to ~/.macos-mcp/audit.jsonl: timestamp, caller, tool, outcome, error code — and a SHA-256 digest of the arguments, never the arguments themselves, because typed text can be a password or a private message. The file rotates; macos-mcp audit tail and macos-mcp audit summary read it back without a JSON tool.

A frozen tool API

Tool names and argument shapes are public API. The surface is snapshotted in tool-schemas.json and every build is diffed against the last release: a removed or renamed tool, a removed or newly required argument, a narrowed enum, or a tool that quietly becomes destructive fails the build unless the deprecation ledger announced it at least one minor version earlier. Nothing you write against this server should break because a name was tidied.

No telemetry

There is no analytics, no crash reporting, no update check and no network call inside the binary. The only network traffic is what a tool you invoke does, such as opening a URL. There is no surface for telemetry to be added quietly: the audit log is local and the code is MIT.

Supply chain

Releases are a Developer ID-signed, Apple-notarized binary published on GitHub Releases with SHA-256 sums. The installer verifies the checksum and the signature chain before installing and refuses anything that fails either check. A stable signing identity also means your TCC grants survive updates — an advantage native binaries have over Python and Node servers, whose grants attach to the interpreter.

Reporting a vulnerability

Use GitHub private vulnerability reporting on the repository; the security policy lists what is in scope — profile bypasses, confirmation-token or audit evasion, any hard-line crossing, installer or release integrity — and the response targets.