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 calendarsOutput:
{
"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 workOutput 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-01T00:00:00Z" \
--to "2026-01-31T23:59:59Z"
# Or using an alias (after setting one)
ekctl list events \
--calendar work \
--from "2026-01-01T00:00:00Z" \
--to "2026-01-31T23:59:59Z"Output:
{
"count": 2,
"events": [
{
"id": "ABC123:DEF456",
"title": "Team Meeting",
"calendar": {
"id": "CA513B39-1659-4359-8FE9-0C2A3DCEF153",
"title": "Work"
},
"startDate": "2026-01-15T09:00:00Z",
"endDate": "2026-01-15T10: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-10T12:30:00Z" \
--end "2026-02-10T13:30:00Z"
# Event with location and notes
ekctl add event \
--calendar work \
--title "Project Review" \
--start "2026-02-15T14:00:00Z" \
--end "2026-02-15T15: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-01T00:00:00Z" \
--end "2026-03-02T00:00:00Z" \
--all-dayOutput:
{
"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-10T12:30:00Z",
"endDate": "2026-02-10T13: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 trueOutput:
{
"count": 2,
"reminders": [
{
"id": "REM123-456-789",
"title": "Buy groceries",
"list": {
"id": "4E367C6F-354B-4811-935E-7F25A1BB7D39",
"title": "Reminders"
},
"dueDate": "2026-01-20T17: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-25T09: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-01T12: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-25T09: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-21T10:30:00Z"
}
}ekctl delete reminder "REM123-456-789"All dates use ISO 8601 format with timezone. Examples:
| Format | Example | Description |
|---|---|---|
| UTC | 2026-01-15T09:00:00Z |
9:00 AM UTC |
| With offset | 2026-01-15T09:00:00+10:00 |
9:00 AM AEST |
| Midnight | 2026-01-15T00:00:00Z |
Start of day |
| End of day | 2026-01-15T23: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_IDTODAY=$(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-20T10:00:00Z"
END="2026-01-20T11: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-01T00:00:00Z" \
--to "2026-12-31T23: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 --helpMIT License
Contributions are welcome! Please feel free to submit a Pull Request.