Tasks live in the vault. Tasks also live on the phone. Neither system knows about the other. Add a task on the phone while walking, it never reaches the vault. Complete a task in the vault, the phone still shows it overdue.
The Bridge
Built a sync script — about 290 lines — that runs in three phases:
- Vault → Reminders: Scan vault task files, push incomplete tasks to Apple Reminders
- Reminders → Vault: Pull phone-captured tasks back into the vault’s daily note
- Save state: Write the mapping file so the next run knows what’s already synced
The mapping uses a composite key: title::filepath. The same task text can appear in multiple vault files — “Review PR” in three different project folders is three different tasks, not one task mentioned three times.
The Unicode Problem
The title extraction broke on tasks containing emoji. The script used sed to strip checkbox syntax and extract the task title. macOS sed can’t handle multi-byte Unicode characters in character classes.
A task like 📱 Check phone captures would silently produce garbage — truncated titles, missing characters, or empty strings. The fix was replacing sed with perl and explicit Unicode codepoint handling.
| Tool | Unicode Support |
|---|---|
macOS sed | Breaks on multi-byte characters in character classes |
perl with explicit codepoints | Handles everything |
One line replacement. The kind of bug that’s invisible until you have tasks with emoji in them — which, on a phone, is most of them.
The Dedup Problem
The sync needed deduplication on both directions. Reset the mapping file — which happens during debugging, or when the state file gets corrupted — and the next run would create duplicates everywhere: the same task twice in Reminders, the same captured item imported twice into the daily note.
Fix: before pushing a vault task, check if Reminders already has one with that title. Before importing a phone capture, check if the daily note already contains it.
Cleaned 23 duplicate groups from Apple Reminders during the initial sync — artifacts from earlier sync attempts that didn’t have dedup.
Why Not a Background Process
The obvious question: why not run this as a launchd agent, automatically, every hour?
macOS doesn’t grant persistent Reminders access to background processes. A launchd agent would need to re-request permission on every run, or the permission would silently lapse. The sync engine runs inside the AI assistant session instead — interactive terminal sessions hold Reminders access; background processes don’t.
The accountability hooks
The sync script was half the build. The other half was accountability — hooks that fire during sessions to surface overdue tasks.
A session-start hook checks for overdue and due-today tasks, displaying them before any work begins. A mid-session hook fires every 15 prompts with escalating tone. The escalation exists because the first nudge is easy to dismiss. The third one isn’t.
The Damage Report
| Metric | Value |
|---|---|
| Session duration | ~60 minutes |
| Script size | ~290 lines |
| Sync directions | 2 (vault→reminders, reminders→vault) |
| Duplicate groups cleaned | 23 |
| Unicode fix | sed → perl (one line) |
| Permissions workaround | Session-scoped instead of background process |
| Accountability hooks | 2 (session start + mid-session nudge) |
Tasks captured on the phone now reach the vault. Tasks completed in the vault update on the phone. The bridge exists. The hardest part wasn’t the sync logic — it was macOS sed refusing to read emoji.