apple-reminders — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited apple-reminders (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill wraps a small, self-contained Swift binary (reminders-eventkit) that speaks Apple's EventKit framework directly. It is 10-200× faster than the AppleScript alternative and returns structured JSON for every operation. The skill never improvises AppleScript at runtime — every call is a single shell invocation against the binary.
Language neutrality. This skill is intentionally language-agnostic. Match the user's intent, not specific keywords — a user writing in German ("Leg mir eine Erinnerung an"), French ("Ajoute un rappel"), Spanish, or any other language should be served identically. Reply to the user in whatever language they wrote in. Reminder content (titles, notes, list names) is passed through verbatim as UTF-8; umlauts, accents, CJK characters, and emoji work without special handling.
Load this skill whenever the user's intent involves Apple Reminders: listing or inspecting reminder lists, creating/updating/completing/deleting individual reminders, or running smart queries across lists (what's due today, what's overdue, what's currently scheduled, full-text search). This applies regardless of the language the user writes in.
Do not use this skill for Calendar events, Notes, Mail, or Contacts — those belong to separate skills.
Commands fall into three risk tiers. Respect the tier when deciding whether to confirm with the user first:
| Tier | Commands | Confirmation |
|---|---|---|
| read-only | list-lists, get-list-info, list-reminders, search-reminders, get-today, get-overdue, get-scheduled, get-reminder | no confirmation needed |
| additive | create-reminder | proceed unless user's intent is ambiguous |
| destructive | update-reminder, complete-reminder, uncomplete-reminder, delete-reminder | confirm before calling when the target is inferred rather than explicitly named |
Destructive operations are idempotent — running them twice yields the same end state — but they mutate user data and cannot be undone from within this skill (the user has to restore via Time Machine or iCloud).
bin/reminders-eventkit (relative to this skill directory). If it's missing, see the "Rebuilding the binary" section below.Bash tool must be available. This skill does not use the mcp__Control_your_Mac__osascript tool — the EventKit binary is called directly via Bash, which has no 30-second timeout.Every function is a single shell invocation. Do not deviate.
Bash tool: ~/.claude/skills/apple-reminders/bin/reminders-eventkit <command> [args...]{"status":"ok","data": <payload>} — success.{"status":"error","code":"<TOKEN>","message":"<text>"} — failure.0 for ok, 1 for error. You can rely on either signal.Commands that take structured data use JSON payloads. Commands that take simple scalars use plain positional arguments.
JSON payload via stdin — required for `create-reminder` and `update-reminder`. Pass the literal sentinel - as the single positional argument and stream the JSON on stdin. This is the only correct way to pass user-supplied content: shell-quoting arbitrary strings from a user message is unsafe and will break on quotes, backticks, $, newlines, or any bracket the shell tries to glob. The Bash tool supports stdin via a here-doc:
~/.claude/skills/apple-reminders/bin/reminders-eventkit create-reminder - <<'JSON'
{"list":"Groceries","title":"Buy milk","body":"organic, 1.5l","dueDate":"2026-04-11T18:00:00","priority":5}
JSONBuild the JSON with a proper JSON encoder (e.g. jq -n, a Python one-liner, or by assembling an object literal) — never by string concatenation of user content. The <<'JSON' form (note the single quotes around the tag) disables all shell interpolation inside the heredoc, so the payload is passed through bit-for-bit.
Scalar arguments (list names, IDs, filters): quote each with double quotes to handle spaces. Example:
~/.claude/skills/apple-reminders/bin/reminders-eventkit list-reminders "Reise und Freizeit" "open"Disambiguating duplicate list names. Multiple reminder lists can share the same title (common when you have both an iCloud "Personal" list and a local one). Every command that accepts a list name also accepts id:<calendar_identifier> — take the stable calendar_identifier from a prior list-lists call. If a plain title is ambiguous, the binary returns LIST_AMBIGUOUS with a candidates array listing each matching list's name, account, and calendar_identifier; surface the ambiguity to the user, pick the intended one, and retry with id:....
Date format: ISO-8601 local time, seconds precision, no timezone suffix: 2026-04-11T18:00:00. The binary also accepts full ISO-8601 with a timezone offset (e.g. 2026-04-11T18:00:00+02:00). Never pass natural-language date strings — parse those yourself first. If dueDate is present but unparseable, the binary returns INVALID_PAYLOAD rather than silently dropping the field.
| Command | Arguments | Returns (on success) |
|---|---|---|
list-lists | — | {"lists":[{"name","account","open_count","completed_count"}]} |
get-list-info | <listName> | {"name","account","open_count","completed_count"} |
list-reminders | <listName> <filter> | {"reminders":[<reminder>...]} |
search-reminders | <query> <filter> <limit> | {"reminders":[<reminder>...]} |
get-today | — | {"reminders":[<reminder>...]} |
get-overdue | — | {"reminders":[<reminder>...]} |
get-scheduled | — | {"reminders":[<reminder>...]} |
get-flagged | — | {"reminders":[], "warning":"..."} — see note below |
get-reminder | <id> | {"reminder": <reminder>} |
create-reminder | - (JSON on stdin) | {"reminder": <reminder>} |
update-reminder | - (JSON on stdin) | {"reminder": <reminder>} |
complete-reminder | <id> | {"reminder": <reminder>} |
uncomplete-reminder | <id> | {"reminder": <reminder>} |
delete-reminder | <id> | {"deleted_id": "..."} |
<filter> is always exactly one of: open, completed, all. Any other value returns INVALID_FILTER.
{
"list": "Groceries", // required
"title": "Buy milk", // required
"body": "organic, 1.5l", // optional
"dueDate": "2026-04-11T18:00:00", // optional, ISO-8601
"priority": 5, // optional, one of 0|1|5|9, default 0
"flagged": false // accepted but ignored — see flagged note
}{
"id": "B769EAF1-7271-4EE7-B228-8A0FD6B65B47", // required
"title": "...", // optional, replaces
"body": "...", // optional, replaces
"dueDate": "2026-04-11T18:00:00", // optional, replaces
"clearDueDate": true, // optional, wipes due date explicitly
"priority": 5, // optional
"flagged": true // accepted but ignored — see flagged note
}Use clearDueDate: true to distinguish "remove the due date" from "leave unchanged" (which is what happens when you omit dueDate).
{
"id": "B769EAF1-7271-4EE7-B228-8A0FD6B65B47",
"name": "Buy milk",
"body": "organic, 1.5l",
"due_date": "2026-04-11T18:00:00",
"remind_me_date": "2026-04-11T18:00:00",
"completed": false,
"completion_date": null,
"priority": 5,
"flagged": false,
"list": "Groceries"
}Optional fields are null when not set. Field names are stable — the skill and any downstream consumers can depend on them.
flagged is not readable or writable via EventKitApple's EventKit framework does not expose the "flagged" attribute that the Reminders UI shows. get-flagged always returns an empty array with a warning field. create-reminder and update-reminder accept flagged in the payload for API stability but silently ignore it. If the user explicitly needs flagged queries, tell them this limitation and offer to fall back to the AppleScript path described below.
EventKit returns the true completed count from the database. The Reminders app UI may hide completed items based on a user preference (Reminders → Settings → Show → Completed: All / Last 30 Days / ...). If the user is surprised by a high completed_count, point them at that setting.
v1 is read/write on reminders within existing lists only. List management (create/rename/delete) would require additional EventKit calls and is deliberately deferred to keep the binary small. If the user asks to create a list, tell them to create it manually in the Reminders app once, then the skill can write into it freely.
Same reasoning — deferred to keep the surface area small. Sub-tasks in particular require the macOS 13+ subreminder API and careful parent-child handling.
flaggedIf the user absolutely needs flagged-reminder queries, there is a pre-built AppleScript template at scripts/get_flagged.applescript plus a prelude at lib/_prelude.applescript that together implement this one missing function via the Control your Mac MCP. Read both files, concatenate (body + prelude), and send via mcp__Control_your_Mac__osascript. This fallback is only for get-flagged and is slow on large databases — use it sparingly.
If the binary at bin/reminders-eventkit is missing, corrupted, or outdated, rebuild it:
swiftc -O \
~/.claude/skills/apple-reminders/src/reminders-eventkit.swift \
-o ~/.claude/skills/apple-reminders/bin/reminders-eventkitRequirements:
/usr/bin/swiftc, ships with macOS command-line tools)requestFullAccessToReminders on macOS 14+ with a fallback for older versions)The source is a single ~500-line Swift file under src/reminders-eventkit.swift. Read it before rebuilding if you need to audit what the binary does.
LIST_NOT_FOUND — the named list doesn't exist. Run list-lists to show the user what's available.LIST_AMBIGUOUS — the list name matches more than one calendar (e.g. iCloud "Personal" and a local "Personal"). The response carries a candidates array with each match's calendar_identifier; surface the options to the user and retry with id:<calendar_identifier>.REMINDER_NOT_FOUND — the ID is unknown. Usually means a stale ID from a previous session; re-query.INVALID_PRIORITY — priority was not one of 0|1|5|9.INVALID_FILTER — filter was not one of open|completed|all.INVALID_PAYLOAD — the JSON payload couldn't be parsed, was missing a required field, or contained an unparseable dueDate. The message says which.UNKNOWN_COMMAND — the first positional argument wasn't one of the commands listed in the catalog above. Usually a typo.PERMISSION_DENIED — macOS refused Reminders access. Tell the user to approve it in System Settings → Privacy & Security → Reminders.SAVE_FAILED / DELETE_FAILED — EventKit refused the write. Rare; usually transient. Retry once, then show the user the raw message.User: "Add 'Buy milk' to my Groceries list for tomorrow at 6 PM, note: 'organic, 1.5l'."
~/.claude/skills/apple-reminders/bin/reminders-eventkit create-reminder - <<'JSON'
{"list":"Groceries","title":"Buy milk","body":"organic, 1.5l","dueDate":"2026-04-12T18:00:00","priority":0}
JSONResponse:
{"status":"ok","data":{"reminder":{"id":"...","name":"Buy milk","body":"organic, 1.5l","due_date":"2026-04-12T18:00:00",...}}}Confirm to the user in their language, e.g. "Done — 'Buy milk' ist auf deiner Groceries-Liste, fällig morgen 18 Uhr."
If create-reminder returns:
{"status":"error","code":"LIST_AMBIGUOUS","message":"Multiple reminder lists match 'Personal'. ...","candidates":[{"name":"Personal","account":"iCloud","calendar_identifier":"A1B2..."},{"name":"Personal","account":"On My Mac","calendar_identifier":"C3D4..."}]}show the user the two candidates, ask which one they meant, and retry with "list":"id:A1B2..." in the payload.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.