A native macOS command-line tool for managing Calendar events and Reminders using the EventKit framework. All output is JSON, making it perfect for scripting and automation.
- List, create, and delete calendar events
- List, create, complete, and delete reminders
- Calendar aliases - Use friendly names instead of long IDs
- JSON output for easy parsing and scripting
- Full EventKit integration with proper permission handling
- Support for all calendar and reminder list types (iCloud, Exchange, local, etc.)
- macOS 13.0 (Ventura) or later
- Xcode Command Line Tools or Xcode
- Swift 5.9+
brew tap schappim/ekctl brew install ekctl
# Clone the repository git clone https://github.com/schappim/ekctl.git cd ekctl # Build release version swift build -c release # Optional: Sign with entitlements for better permission handling codesign --force --sign - --entitlements ekctl.entitlements .build/release/ekctl # Install to /usr/local/bin sudo cp .build/release/ekctl /usr/local/bin/
On first run, macOS will prompt you to grant access to Calendars and Reminders. You can manage these permissions later in:
System Settings → Privacy & Security → Calendars / Reminders
List all calendars (event calendars and reminder lists):
ekctl list calendars
Output:
{
"calendars": [
{
"id": "CA513B39-1659-4359-8FE9-0C2A3DCEF153",
"title": "Work",
"type": "event",
"source": "iCloud",
"color": "#0088FF",
"allowsModifications": true
},
{
"id": "4E367C6F-354B-4811-935E-7F25A1BB7D39",
"title": "Reminders",
"type": "reminder",
"source": "iCloud",
"color": "#1BADF8",
"allowsModifications": true
}
],
"status": "success"
}Instead of using long calendar IDs, you can create friendly aliases:
# Set an alias for a calendar ekctl alias set work "CA513B39-1659-4359-8FE9-0C2A3DCEF153" ekctl alias set personal "4E367C6F-354B-4811-935E-7F25A1BB7D39" ekctl alias set groceries "E30AE972-8F29-40AF-BFB9-E984B98B08AB" # List all aliases ekctl alias list # Remove an alias ekctl alias remove work
Output for ekctl alias list:
{
"aliases": [
{ "name": "groceries", "id": "E30AE972-8F29-40AF-BFB9-E984B98B08AB" },
{ "name": "personal", "id": "4E367C6F-354B-4811-935E-7F25A1BB7D39" },
{ "name": "work", "id": "CA513B39-1659-4359-8FE9-0C2A3DCEF153" }
],
"count": 3,
"configPath": "/Users/you/.ekctl/config.json",
"status": "success"
}Once set, use aliases anywhere you would use a calendar ID:
# These are equivalent: ekctl list events --calendar "CA513B39-1659-4359-8FE9-0C2A3DCEF153" --from ... ekctl list events --calendar work --from ... # Works with all commands ekctl add event --calendar work --title "Meeting" --start ... ekctl list reminders --list groceries ekctl add reminder --list personal --title "Call mom"
Aliases are stored in ~/.ekctl/config.json.
List events in a calendar within a date range:
# Using calendar ID ekctl list events \ --calendar "CA513B39-1659-4359-8FE9-0C2A3DCEF153" \ --from "2026年01月01日T00:00:00Z" \ --to "2026年01月31日T23:59:59Z" # Or using an alias (after setting one) ekctl list events \ --calendar work \ --from "2026年01月01日T00:00:00Z" \ --to "2026年01月31日T23:59:59Z"
Output:
{
"count": 2,
"events": [
{
"id": "ABC123:DEF456",
"title": "Team Meeting",
"calendar": {
"id": "CA513B39-1659-4359-8FE9-0C2A3DCEF153",
"title": "Work"
},
"startDate": "2026年01月15日T09:00:00Z",
"endDate": "2026年01月15日T10:00:00Z",
"location": "Conference Room A",
"notes": null,
"allDay": false,
"hasAlarms": true,
"hasRecurrenceRules": false
}
],
"status": "success"
}ekctl show event "ABC123:DEF456"Create a new calendar event:
# Basic event (using alias) ekctl add event \ --calendar work \ --title "Lunch with Client" \ --start "2026年02月10日T12:30:00Z" \ --end "2026年02月10日T13:30:00Z" # Event with location and notes ekctl add event \ --calendar work \ --title "Project Review" \ --start "2026年02月15日T14:00:00Z" \ --end "2026年02月15日T15:30:00Z" \ --location "Building 2, Room 301" \ --notes "Bring Q1 reports" # All-day event (using full ID also works) ekctl add event \ --calendar "CA513B39-1659-4359-8FE9-0C2A3DCEF153" \ --title "Company Holiday" \ --start "2026年03月01日T00:00:00Z" \ --end "2026年03月02日T00:00:00Z" \ --all-day
Output:
{
"status": "success",
"message": "Event created successfully",
"event": {
"id": "NEW123:EVENT456",
"title": "Lunch with Client",
"calendar": {
"id": "CA513B39-1659-4359-8FE9-0C2A3DCEF153",
"title": "Work"
},
"startDate": "2026年02月10日T12:30:00Z",
"endDate": "2026年02月10日T13:30:00Z",
"location": null,
"notes": null,
"allDay": false
}
}ekctl delete event "ABC123:DEF456"Output:
{
"status": "success",
"message": "Event 'Team Meeting' deleted successfully",
"deletedEventID": "ABC123:DEF456"
}List reminders in a reminder list:
# List all reminders (using alias) ekctl list reminders --list personal # List only incomplete reminders ekctl list reminders --list personal --completed false # List only completed reminders (using full ID also works) ekctl list reminders --list "4E367C6F-354B-4811-935E-7F25A1BB7D39" --completed true
Output:
{
"count": 2,
"reminders": [
{
"id": "REM123-456-789",
"title": "Buy groceries",
"list": {
"id": "4E367C6F-354B-4811-935E-7F25A1BB7D39",
"title": "Reminders"
},
"dueDate": "2026年01月20日T17:00:00Z",
"completed": false,
"priority": 0,
"notes": null
}
],
"status": "success"
}ekctl show reminder "REM123-456-789"Create a new reminder:
# Simple reminder (using alias) ekctl add reminder \ --list personal \ --title "Call the dentist" # Reminder with due date ekctl add reminder \ --list personal \ --title "Submit expense report" \ --due "2026年01月25日T09:00:00Z" # Reminder with priority and notes # Priority: 0=none, 1=high, 5=medium, 9=low ekctl add reminder \ --list groceries \ --title "Buy milk" \ --due "2026年02月01日T12:00:00Z" \ --priority 1 \ --notes "Check expiration date first"
Output:
{
"status": "success",
"message": "Reminder created successfully",
"reminder": {
"id": "NEWREM-123-456",
"title": "Submit expense report",
"list": {
"id": "4E367C6F-354B-4811-935E-7F25A1BB7D39",
"title": "Reminders"
},
"dueDate": "2026年01月25日T09:00:00Z",
"completed": false,
"priority": 0,
"notes": null
}
}Mark a reminder as completed:
ekctl complete reminder "REM123-456-789"
Output:
{
"status": "success",
"message": "Reminder 'Buy groceries' marked as completed",
"reminder": {
"id": "REM123-456-789",
"title": "Buy groceries",
"completed": true,
"completionDate": "2026年01月21日T10:30:00Z"
}
}ekctl delete reminder "REM123-456-789"All dates use ISO 8601 format with timezone. Examples:
| Format | Example | Description |
|---|---|---|
| UTC | 2026年01月15日T09:00:00Z |
9:00 AM UTC |
| With offset | 2026年01月15日T09:00:00+10:00 |
9:00 AM AEST |
| Midnight | 2026年01月15日T00:00:00Z |
Start of day |
| End of day | 2026年01月15日T23:59:59Z |
End of day |
# Using jq to find a calendar by name CALENDAR_ID=$(ekctl list calendars | jq -r '.calendars[] | select(.title == "Work") | .id') echo $CALENDAR_ID
TODAY=$(date -u +"%Y-%m-%dT00:00:00Z") TOMORROW=$(date -u -v+1d +"%Y-%m-%dT00:00:00Z") ekctl list events \ --calendar "$CALENDAR_ID" \ --from "$TODAY" \ --to "$TOMORROW"
TITLE="Sprint Planning" START="2026年01月20日T10:00:00Z" END="2026年01月20日T11:00:00Z" ekctl add event \ --calendar "$CALENDAR_ID" \ --title "$TITLE" \ --start "$START" \ --end "$END"
ekctl list reminders --list "$LIST_ID" --completed false | jq '.count'
ekctl list events \ --calendar "$CALENDAR_ID" \ --from "2026年01月01日T00:00:00Z" \ --to "2026年12月31日T23:59:59Z" \ | jq -r '.events[] | [.title, .startDate, .endDate, .location // ""] | @csv'
When an error occurs, the output includes an error message:
{
"status": "error",
"error": "Calendar not found with ID: invalid-id"
}Common errors:
Permission denied- Grant access in System SettingsCalendar not found- Check the calendar ID withlist calendarsInvalid date format- Use ISO 8601 format (see examples above)
Get help for any command:
ekctl --help ekctl list --help ekctl add event --help ekctl list reminders --help
MIT License
Contributions are welcome! Please feel free to submit a Pull Request.