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).
${CLAUDE_SKILL_DIR}/bin/reminders-eventkit. If it's missing, see "If the binary is missing" below.Bash tool must be available. Every command below is a Bash invocation of the binary; only the optional flagged fallback at the end runs AppleScript, also through Bash.Every function is a single shell invocation. Do not deviate.
Bash tool: ${CLAUDE_SKILL_DIR}/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_SKILL_DIR}/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_SKILL_DIR}/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). For a reminder due on a day without a time, pass the date only: 2026-04-11. It gets no alert, like one created in the Reminders app with "Time" off; a dueDate with a time also sets an alert at that time. Read results report such a reminder's due_date the same way, as 2026-04-11. 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","calendar_identifier","open_count","completed_count"}]} |
get-list-info | <listName> | {"name","account","calendar_identifier","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-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, or "2026-04-11" without a time
"priority": 5 // optional, one of 0|1|5|9, default 0
}{
"id": "B769EAF1-7271-4EE7-B228-8A0FD6B65B47", // required
"title": "...", // optional, replaces
"body": "...", // optional, replaces
"dueDate": "2026-04-11T18:00:00", // optional, replaces the due date and its alerts
"clearDueDate": true, // optional, wipes due date explicitly
"priority": 5 // optional
}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. A due_date without a time is a date only, such as "2026-04-11". 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. The binary has no flagged query, every reminder it returns carries "flagged": false whatever the Reminders app shows, and the create/update payloads have no flagged field. 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 query. Concatenate them (body first, then prelude) and pipe the result into osascript via Bash:
cat "${CLAUDE_SKILL_DIR}/scripts/get_flagged.applescript" \
"${CLAUDE_SKILL_DIR}/lib/_prelude.applescript" | osascriptIt prints the same {"status":"ok","data":{"reminders":[...]}} envelope as the binary. macOS may first ask the user to let the terminal control Reminders. This fallback is only for flagged queries and is slow on large databases — use it sparingly.
Do not compile anything on the user's machine. The binary is a signed, notarized release artifact:
bin/. It runs when a session starts, so ask the user to start a new session; if the hook failed, its error output says why (most often no network access to GitHub).reminders-eventkit from the latest release into ${CLAUDE_SKILL_DIR}/bin/ and make it executable (chmod +x), or to copy the skill again from dist/skill/ after running ./build.sh skill in a checkout of the repository.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.PERMISSION_UNAVAILABLE — macOS refused the request without showing a dialog, so there is no entry in System Settings to switch on. Granting or resetting permissions cannot fix it; show the user the message and point them at the project's issue tracker.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_SKILL_DIR}/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.