Repository navigation
Description
Summary
Follow-up to #51. The spike there showed a reminder's flagged state can be read, and the CLI now ships read-only support: isFlagged in JSON, a (flagged) suffix in plain output, and a --flagged filter on show/show-all. Setting the flag isn't possible through EventKit. This issue tracks write support and records why it was left out.
Proposed CLI surface
add --flaggededit --flagged/edit --clear-flagged, mutually exclusive and checked invalidate(). Either one counts as a change for the "at least one change is required" rule.- If the chosen mechanism isn't available on the running macOS version, throw
invalid_argumentwith a message saying flagging isn't supported there.
Why EventKit can't write the flag
The following was checked by runtime introspection on macOS 26.6.2 (25G83):
-
EventKit has no public flag property.
EKReminder,EKCalendarItemandEKObjecthave noflagged/isFlaggedproperty, method or KVC key, andinstancesRespond(to:)is false for both names. The public SDK (EventKit.frameworkheaders,.swiftinterface,.tbd) doesn't mention "flagged" anywhere.setValue(_:forKey: "flagged")on anEKReminderwould raiseNSUndefinedKeyException, which Swift can't catch, so the process would crash. -
The flag lives in a private framework. Since macOS 10.15, Reminders data is stored by
ReminderKit.framework(/System/Library/PrivateFrameworks), not by the Calendar database. EventKit's reminder objects are thin wrappers around it. The flag appears only on ReminderKit classes:REMReminderStoragehasflagged/setFlagged:REMReminderhasflaggedContext, which returns aREMReminderFlaggedContextwith aflaggedgetterREMReminderFlaggedContextChangeItemhasflagged/setFlagged:REMAccountCapabilitieshassupportsFlagged, so some account types don't support flags at all
-
Reading works only by walking into those private objects. The read path in the CLI is
EKReminder.backingObject(anEKFrozenReminderReminder) →_reminder(aREMReminder) →flaggedContext→flagged. Every step is an undocumented selector. The CLI guards each step withresponds(to:)and treats any missing link as "not flagged". That fallback is acceptable for a read, but there is no safe equivalent for a write. -
EventKit's save path never carries the flag. The
REMReminderbehind anEKReminderis an immutable snapshot ("frozen" object). OnEKEventStore.save(_:commit:), EventKit translates its own change set (EKChangeSet→EKObject.prepareReminderKitObjectForSaveWithUpdatedBackingObjectProvider:→EKReminderStore) into ReminderKit change items, and it only translates the properties EventKit models. Setting a value somewhere on the snapshot therefore wouldn't be persisted. It would be silently discarded, or overwritten on the next save. -
Writing through ReminderKit directly means calling private API. It would require building a
REMSaveRequest, getting aREMReminderChangeItemfor the reminder, settingflaggedContext/REMReminderFlaggedContextChangeItem.flagged, and committing through aREMStore. The problems with that:- No stability contract. Class names, selectors and the save flow can change in any macOS update, including minor ones, with no deprecation. A write that half-works is worse than a read that falls back to
false, because it can lose or corrupt user data. - It isn't guarded the way the read is. Reading can fall back to
false. A write has to either succeed or fail loudly, and verifying that it succeeded would itself rely on more private API. - Entitlements and TCC.
reminddmay check that clients talking to ReminderKit directly have private entitlements, which a notarized third-party binary can't have. Access granted through EventKit'srequestFullAccessToRemindersisn't guaranteed to cover the private XPC path. - Distribution. Linking or
dlopen-ing a private framework conflicts with Spike: expose and filter by flagged status #51's "no private frameworks" constraint and is a liability for the Homebrew-distributed binary.
- No stability contract. Class names, selectors and the save flow can change in any macOS update, including minor ones, with no deprecation. A write that half-works is worse than a read that falls back to
Most promising alternative: AppleScript / Apple Events
Reminders.app's scripting dictionary (sdef /System/Applications/Reminders.app) publicly documents the flag:
<property name="flagged" code="flgd" type="boolean" description="Whether the reminder is flagged">
<cocoa key="isFlagged" />
</property>It has no access="r", so it should be writable. This was verified read-only on macOS 26.6.2:
tell application "Reminders" to get {id, name, flagged} of reminders of list "Test"returned the correct flags.- The AppleScript
idisx-apple-reminder://<UUID>, and the UUID equals EventKit'scalendarItemIdentifier. For iCloud reminders it was also equal tocalendarItemExternalIdentifier, but that may not hold for other account types.
The write was not tested. Open questions before choosing this route:
- Does
set flagged of reminder id "x-apple-reminder://…" to truepersist and sync through iCloud? - Mapping: the CLI resolves reminders by
calendarItemExternalIdentifier. Is the AppleScript id always derived fromcalendarItemIdentifier, including for non-iCloud accounts? - Permissions: this needs a separate Automation (Apple Events) TCC grant for the terminal or parent app, and possibly an
NSAppleEventsUsageDescription. How should the CLI report a denied Automation prompt (a newCLIErrorcode?) - It launches Reminders.app if it isn't running. Is that acceptable for a CLI?
- How far back does the
flaggedscripting property go (which macOS versions)? - Should reads also move to AppleScript so read and write use the same public mechanism, or stay on the fast in-process path?
Other options
- Wait for Apple to add
isFlaggedtoEKReminder, and file a Feedback Assistant request for it. - Shortcuts: check whether the "Edit Reminder" action can set the flag and whether it can be driven from the CLI (
shortcuts run). This would need a user-installed shortcut, so it's probably a poor fit.
Activity
feat: show and filter by flagged status (#64)