在 Claude Code / 一般 shell 裡查詢、管理行事曆的操作手冊。 下次不用從頭摸。
| 帳號類型 | 讀取 | 寫入(建議) | 寫入(不建議) |
|---|---|---|---|
| Google Calendar | Google Calendar MCP | Google Calendar MCP | — |
| iCloud | Calendar.sqlitedb (read-only) | AppleScript / EventKit | 直接寫 sqlite |
| Exchange / CalDAV(公司、院內) | Calendar.sqlitedb (read-only) | AppleScript | 直接寫 sqlite |
| URL 訂閱 (.ics) | Calendar.sqlitedb | 改不了(唯讀來源) | — |
規則:讀 sqlite OK,寫絕對走正規 API。
~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb
舊的 ~/Library/Calendars/ 在新版 macOS 幾乎空著,真正資料在上面那個。
| Table | 作用 |
|---|---|
Calendar |
所有 calendar(含訂閱、group、iCloud、Exchange 等) |
Store |
帳號 / 資料來源(iCloud、各 Gmail、Exchange、Subscribed Calendars…) |
CalendarItem |
事件本體 |
Location |
地點 |
OccurrenceCache |
重複事件展開後的每個 instance |
Alarm |
提醒 |
Apple 用 NSDate:2001-01-01 00:00:00 UTC 開始的秒數。
apple_ts = unix_ts - 978307200
SQL 裡要顯示可讀時間:
datetime(col + 978307200, 'unixepoch', 'localtime')產生查詢窗的 Python one-liner:
python3 -c "from datetime import datetime, timezone, timedelta; \
tz=timezone(timedelta(hours=8)); \
print(int(datetime(2026,4,25,0,0,tzinfo=tz).timestamp() - 978307200))"SELECT c.title, c.type, s.name AS account, c.subcal_url
FROM Calendar c
LEFT JOIN Store s ON c.store_id = s.ROWID
ORDER BY s.name, c.title;subcal_url有值 = URL 訂閱的 calendar(例如webcal://…/.ics)s.name = 'Subscribed Calendars'= 純 URL 訂閱帳號
SELECT title, subcal_url, refresh_interval
FROM Calendar
WHERE subcal_url IS NOT NULL AND subcal_url != '';SELECT c.title AS cal,
datetime(ci.start_date + 978307200, 'unixepoch', 'localtime') AS start,
datetime(ci.end_date + 978307200, 'unixepoch', 'localtime') AS end,
ci.all_day,
ci.summary,
l.title AS location
FROM CalendarItem ci
JOIN Calendar c ON ci.calendar_id = c.ROWID
LEFT JOIN Location l ON ci.location_id = l.ROWID
WHERE c.title IN ('<calendar-A>', '<calendar-B>')
AND ci.hidden = 0
AND ci.status != 2 -- 排除 cancelled
AND ci.end_date >= :window_start
AND ci.start_date <= :window_end
ORDER BY ci.start_date;重複事件的單次 instance 存在 OccurrenceCache:
SELECT c.title AS cal,
datetime(oc.occurrence_start_date + 978307200, 'unixepoch', 'localtime') AS start,
datetime(oc.occurrence_end_date + 978307200, 'unixepoch', 'localtime') AS end,
ci.summary,
l.title AS loc
FROM OccurrenceCache oc
JOIN CalendarItem ci ON oc.event_id = ci.ROWID
JOIN Calendar c ON oc.calendar_id = c.ROWID
LEFT JOIN Location l ON ci.location_id = l.ROWID
WHERE oc.occurrence_end_date > :window_start
AND oc.occurrence_start_date < :window_end
AND ci.hidden = 0
ORDER BY oc.occurrence_start_date;SELECT DISTINCT 或用 event_id 分組,不然 occurrence 會重複出現多次(每個 day row 一份)。
SELECT datetime(ci.start_date + 978307200, 'unixepoch', 'localtime') AS start,
ci.summary,
ci.description
FROM CalendarItem ci
JOIN Calendar c ON ci.calendar_id = c.ROWID
WHERE c.title = '<calendar-name>'
AND ci.hidden = 0
AND ci.status != 2
AND ci.start_date BETWEEN :year_start AND :year_end
AND ci.summary LIKE '%<keyword>%'
ORDER BY ci.start_date;- Calendar.app / CalendarAgent 常駐打開檔案 — 寫入可能被覆蓋或造成 WAL 損毀
- 繞過同步層 — CalendarAgent 把變更推到 iCloud / Google / Exchange,直接改 sqlite 等於只改本機
- 內部快取要同步維護 —
OccurrenceCache,OccurrenceCacheDays,Notification,Alarm,CalendarChanges互相綁 trigger - TCC 權限 — 新版 macOS 對
Group Containers寫入會擋
Claude 的 google_calendar MCP:create_event / update_event / delete_event / list_events / suggest_time。
tell application "Calendar"
tell calendar "<calendar-name>"
make new event with properties {
summary:"<title>",
start date:(current date),
end date:(current date) + 1 * hours,
location:"<loc>"
}
end tell
end tellCLI:osascript -e '...' 或 osascript /path/to/script.applescript
需要 TCC 授權,適合寫獨立 app / 背景 daemon。一次性腳本不划算,用 AppleScript 就好。
python3 -c "
from datetime import datetime, timezone, timedelta
tz = timezone(timedelta(hours=8))
def apple(d): return int(d.timestamp() - 978307200)
today = datetime.now(tz).replace(hour=0,minute=0,second=0,microsecond=0)
print('today :', apple(today))
print('today +7d :', apple(today + timedelta(days=7)))
"SELECT ci.summary, ci.description
FROM CalendarItem ci
WHERE ci.summary LIKE '%<keyword>%'
LIMIT 5;Exchange 推來的 description 常常夾 HTML / Zoom 連結,直接 grep 就能挖到會議連結。
- 這份 DB 受 macOS TCC (Transparency, Consent, and Control) 保護,跑在某些沙盒 shell 裡可能被拒(需要 Full Disk Access 或在 iTerm2/Terminal 主 shell 跑)
- 不要把查詢結果原樣上傳到公開空間(事件 description 可能含病人資訊、會議連結、帳號密碼、附件 URL)
- Calendar.app 關掉再查可以避免
-wal/-shm半寫狀態造成結果不一致(通常不會影響讀)
- 寫一個
cal-weekbash 函式,帶日期參數,回傳指定週的表格 - 寫一個
cal-subs列出所有 URL 訂閱與 refresh 狀態 - 做一個 AppleScript 範本:從 markdown 清單批次加事件到指定 calendar
- 研究 EventKit CLI 工具(例如
icalBuddy,dateutils)是否能取代手寫 SQL