Apple pim
Native macOS personal information management for calendars, reminders, contacts, and local Mail.app. Use when the user wants to schedule meetings, create events, check their calendar, create or complete reminders, look up contacts, find someone's phone number or email, manage tasks and to-do lists, triage local Mail.app messages, or troubleshoot EventKit, Contacts, or Mail.app permissions on macOS.From its SKILL.md
npx -y skills add omarshahine/apple-pim --skill apple-pimAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
14.9 KB, as published. Nobody here has run it
Apple PIM (EventKit, Contacts & Mail)
Overview
Apple provides frameworks and scripting interfaces for personal information management:
- EventKit: Calendars and Reminders
- Contacts: Address book management
- Mail.app: Local email — reads via direct SQLite (Envelope Index, milliseconds), mutations via JXA/AppleScript
EventKit and Contacts require explicit user permission via privacy prompts. Mail.app requires Automation permission and must be running.
For detailed API property tables and code examples, see:
references/eventkit-api.md— EKEvent, EKReminder, EKCalendar, recurrence rules, alarmsreferences/contacts-api.md— CNContact, labeled values, groupsreferences/mail-jxa.md— JXA message properties, batch fetching, Mail.app vs Fastmail scope
Authorization & Permissions
Permission Model
Each PIM domain requires separate macOS authorization:
| Domain | Framework | Permission Section |
|---|---|---|
| Calendars | EventKit | Privacy & Security > Calendars |
| Reminders | EventKit | Privacy & Security > Reminders |
| Contacts | Contacts | Privacy & Security > Contacts |
| Mail (mutations) | Automation (JXA) | Privacy & Security > Automation |
| Mail (fast reads) | Full Disk Access | Privacy & Security > Full Disk Access |
Authorization States
| State | Meaning | Action |
|---|---|---|
notDetermined | Never requested | Use apple-pim with action authorize to trigger prompt |
authorized | Full access granted | Ready to use |
denied | User refused access | Must enable in System Settings manually |
restricted | System policy (MDM, parental) | Cannot override |
writeOnly | Limited write access (macOS 17+) | Upgrade to Full Access in Settings |
SSH Sessions
Permissions must be granted on the Mac where the CLI runs. SSH does not inherit GUI-level permission dialogs. Grant permissions locally first.
Configuration (PIMConfig)
The PIM CLIs share a configuration system for filtering calendars/reminder lists and setting defaults.
Config File Locations
| Path | Purpose |
|---|---|
~/.config/apple-pim/config.json | Base configuration |
~/.config/apple-pim/profiles/{name}.json | Named profile overrides |
Example Config
{
"calendars": {
"enabled": true,
"mode": "blocklist",
"items": ["US Holidays", "Birthdays"],
"default": "Personal"
},
"reminders": {
"enabled": true,
"mode": "allowlist",
"items": ["Tasks", "Shopping", "Work"],
"default": "Tasks"
},
"contacts": {
"enabled": true
},
"mail": {
"enabled": true
}
}
Domain Filter Config
| Field | Type | Description |
|---|---|---|
enabled | boolean | Whether the domain is active (default: true) |
mode | string | Filter mode: all, allowlist, or blocklist (default: all) |
items | string[] | Calendar/list names for allowlist or blocklist |
default | string | Default calendar or list for creating new items |
Filter Modes
| Mode | Behavior |
|---|---|
all | No filtering — all calendars/lists are visible (default) |
allowlist | Only calendars/lists named in items are visible |
blocklist | All calendars/lists are visible EXCEPT those named in items |
Profiles
Profiles allow different configurations for different contexts (e.g., work vs personal).
Selection priority: --profile CLI flag > APPLE_PIM_PROFILE env var > base config only.
Merge semantics: A profile replaces entire domain sections. If a profile defines calendars, it completely replaces the base calendars config (not a field-by-field merge).
Discovery Tools
apple-pimwith actionconfig_show: Returns the current resolved config after profile merging. Shows domains, filters, defaults, and paths.apple-pimwith actionconfig_init: Lists all available calendars and reminder lists from macOS with their sources and system defaults. Does NOT write any files.
Both accept an optional profile parameter.
Defaults Resolution
When creating events or reminders, the default calendar/list is resolved in this order:
- Explicit
--calendaror--listparameter - Config
defaultvalue for the domain - System default calendar/list from EventKit
Note
There is no MCP tool for writing config files. Users must manually create or edit ~/.config/apple-pim/config.json. Use apple-pim with action config_init to discover available calendars/lists, then guide the user on creating the config.
Trusted Senders (auth_check)
The auth_check action verifies sender identity by parsing Authentication-Results headers (DKIM + SPF) against a trusted senders config.
Config file: ~/.config/apple-pim/trusted-senders.json
{
"version": 1,
"trustedSenders": [
{
"name": "Alice",
"emails": ["[email protected]"],
"expectedDkimDomains": ["example.com"],
"requireSpf": true
}
]
}
Override path with trustedSenders parameter: mail({ action: "auth_check", id: "<msg-id>", trustedSenders: "~/custom/senders.json" })
Best Practices
Calendar Management
- Use default calendar for new events when user doesn't specify
- Preserve recurrence rules when updating recurring events
- Handle
.thisEventvs.futureEventsspan for recurring event edits (see EKSpan below) - Check
allowsContentModificationsbefore attempting writes - Use
calendarwith actionbatch_createwhen creating multiple events for efficiency
EKSpan for Recurring Events
EventKit uses EKSpan to control which occurrences are affected by save/delete operations:
| Span | Effect | When to Use |
|---|---|---|
.thisEvent | Affects only the single occurrence | Default for delete and update. Use when cancelling one meeting. |
.futureEvents | Affects this and all future occurrences | Use when ending a series or changing the pattern going forward. |
- Delete: Default is
.thisEvent. Pass--future-eventsto use.futureEvents. - Update: Default is
.thisEvent. Pass--future-eventsto apply changes to all future occurrences. - Remove recurrence: Pass
recurrence: { frequency: "none" }with--future-eventsto convert a recurring event into a single event.
Recurrence Output
When reading events/reminders, the recurrence array includes:
frequency: daily, weekly, monthly, yearlyinterval: repeat every N periodsdaysOfTheWeek: which days (e.g.,["monday", "wednesday", "friday"])daysOfTheMonth: which days of month (e.g.,[1, 15])endDateoroccurrenceCount: when the series ends
Reminder Management
-
Default to incomplete reminders when listing
-
Use filters for focused views:
overduefor urgent items,todayfor daily planning,weekfor weekly review -
Set completionDate when marking complete
-
Respect priority levels (1=high is flagged in UI)
-
Use dueDateComponents not absolute dates for better handling
-
Use batch operations (
reminderwith actionbatch_complete,batch_delete) when acting on multiple items -
urlis an EventKit field Apple Reminders never renders — a link written only toEKReminder.urlis invisible to the user. The CLI therefore mirrors it into the notes as a🔗 <url>line, which Reminders does display and data-detect. Clearing the URL removes that line; updating other fields preserves both. PassurlInNotes: false(or--no-url-in-noteson the CLI) when you want the value stored for machine use only -
alarmon a reminder moves the reminder; it is not an early heads-up — Apple Reminders has one notion of a reminder's time and draws the earliest alarm, falling back to the due date only when a reminder has no alarms.alarm: [15]on a reminder due at 3:00 makes it read and fire at 2:45, and the 3:00 due time appears nowhere in the app. Adding a companion alarm at the due date does not restore it (earliest still wins), and an absolute alarm resolves to the same instant. Usealarm: [0]to alert at the due date — that is what Reminders itself writes — or set the due date to the time the user actually wants. The CLI returns awarningsarray whenever a write moves the visible time; surface it -
startDatemirrors the due date and is returned on reads — Reminders offers no separate start-date control, so the CLI keeps the two in sync on every write. A reminder that ends up with a start date but no due date renders as dateless in Reminders while still occupying a date slot in EventKit; the returnedstartDateis how you diagnose that
What EventKit cannot reach
These are Reminders/Calendar features with no EventKit API. Say so and stop — do not
improvise an adjacent thing. Writing #tag into a title, creating five flat reminders to
stand in for a checklist, or putting "SUBTASK:" in the notes produces data the user did not ask
for and has to clean up by hand.
| Feature | Status |
|---|---|
| Subtasks / nesting | Verified dead. parentID (type EKObjectID) accepts a setValue in memory but does not survive eventStore.save(); every object ID comes back temporary, and AppleScript's container is read-only. Only the Reminders UI can create the relationship |
| Tags | No API — a #tag in a title or note is inert text, not a tag |
| Flag | No API. Distinct from priority, which is supported |
| Attached images / files | No API. Part of why a link has to live in notes to be reachable |
| Sections within a list | No API |
| Smart Lists | No API |
| Remind me when messaging | No API |
| Assignee on a shared list | No API |
Calendar's url field is not on this list: Calendar renders EKEvent.url as a live link in
the event inspector, so url on an event reaches the user as-is and needs no mirroring. Only
Reminders hides it.
Contact Management
- Use unified contacts for consistent view across accounts
- Preserve existing data when updating (only modify changed fields)
- Handle labeled values carefully - don't lose non-primary entries
- Request minimum necessary keys for performance
Mail Management
- Mail.app must be running for mutations, sends, and
contentsearch (reads use the direct SQLite path and work with Mail.app closed when Full Disk Access is granted) - Use batch operations (
mailwith actionbatch_update,batch_delete) for inbox triage - Use filters (unread, flagged) for efficient message listing
- Message IDs are RFC 2822 — stable across mailbox moves
- Use mailbox/account hints when available for faster lookups
- Send (
mailwith actionsend) uses AppleScript — supportsto,cc,bcc,from(account selection),subject,body - Reply (
mailwith actionreply) preserves threading — looks up message by RFC 2822 ID, then uses Mail.app'sreplyverb - Auth check (
mailwith actionauth_check) verifies DKIM/SPF against~/.config/apple-pim/trusted-senders.json— returnsverified,suspicious,untrusted, orunknown, plusevaluated. A missing config file no longer stops the check: authentication is still evaluated, nobody is enrolled, and the result says so. A config file that exists but will not parse is a hard error - Read
evaluatedbefore trusting a verdict —unknownwithevaluated: falsemeans the DKIM/SPF checks never ran (no headers, no trustedauthserv-id), which is a different situation from running them and being unsure. Treating the first as the second means acting on a check that did not happen - Use
senderAddressfor decisions,senderfor display —senderjoins the display name and the address into one string, and the display name is chosen by the sender.messages,search, andgetall returnsenderAddress/senderNameseparately (getaddsreplyToAddress/replyToName); route, filter, and match on the address
Error Handling
- Check authorization first with
apple-pimactionstatuswhen encountering errors - Use
apple-pimactionauthorizeto request access fornotDetermineddomains - Guide users to System Settings for
denieddomains - Validate dates before creating events/reminders
- Check for conflicts when scheduling
- Provide clear feedback on operation success/failure
Common Patterns
Date Parsing
Support flexible input:
- ISO 8601:
2024-01-15T14:30:00 - Natural language: "tomorrow at 3pm"
- Relative: "in 2 hours", "next Tuesday"
Time Zone Handling
- EventKit stores dates in UTC
- Display in local time zone
- Be explicit about time zones in user output
Searching
- Name search:
CNContact.predicateForContacts(matchingName:) - ID lookup:
CNContact.predicateForContacts(withIdentifiers:) - Date range:
eventStore.predicateForEvents(withStart:end:calendars:)
Troubleshooting
Permission Issues
- Use
apple-pimwith actionstatusto check all domains at once - Use
apple-pimwith actionauthorizeto trigger permission prompts - Check System Settings > Privacy & Security
- Terminal/app must be granted access
- Restart app after granting permission
Configuration Issues
- Unexpected filtering: Use
apple-pimwith actionconfig_showto verify the active config. Check if an unexpected profile is being applied viaAPPLE_PIM_PROFILEenv var. - Missing calendars/lists: Use
apple-pimwith actionconfig_initto see all available calendars/lists from macOS, then compare with actionconfig_showto see what's being filtered. - Profile not applying: Check profile selection priority:
--profileflag >APPLE_PIM_PROFILEenv var > base config. Profile files must be at~/.config/apple-pim/profiles/{name}.json. - Malformed config: If
config.jsonhas invalid JSON, CLIs fall back to default behavior (all domains enabled, no filtering). Useapple-pimwith actionconfig_showto verify — it reports the config path and whether it was loaded successfully.
Missing Data
- Ensure keys are requested when fetching contacts
- Check calendar source/account sync status
- Verify iCloud sync is working
Performance
- Limit date ranges for event queries
- Use predicates to filter server-side
- Fetch only needed contact keys
- Use batch operations for multi-item actions