Claude-code-plugins-plus-skills apple-notes-performance-tuning
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/apple-notes-pack/skills/apple-notes-performance-tuning" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-apple-notes-performance-tuning && rm -rf "$T"
manifest:
plugins/saas-packs/apple-notes-pack/skills/apple-notes-performance-tuning/SKILL.mdsource content
Apple Notes Performance Tuning
Overview
Apple Notes automation performance degrades linearly with note count because JXA loads all note objects into memory when you access a collection. A vault with 10,000+ notes can take 30+ seconds for a simple list operation. The primary bottleneck is the Apple Events bridge between your script and Notes.app — every property access (name, body, date) is a separate IPC call. This guide covers caching strategies, incremental sync, batch optimization, and architectural patterns to keep automation responsive at scale.
Performance Benchmarks
| Operation | 100 notes | 1,000 notes | 10,000 notes |
|---|---|---|---|
| List all (names only) | ~0.5s | ~3s | ~30s |
Search by name () | ~0.3s | ~2s | ~20s |
| Full-text search (body scan) | ~1s | ~8s | ~80s |
| Create single note | ~0.2s | ~0.2s | ~0.2s |
| Export all to JSON | ~1s | ~10s | ~100s |
Count notes only () | ~0.1s | ~0.3s | ~1s |
Strategy 1: Minimize Property Access
// BAD: Each property access is a separate Apple Event IPC call const Notes = Application("Notes"); const allNotes = Notes.defaultAccount.notes(); allNotes.forEach(n => { console.log(n.name()); // IPC call 1 console.log(n.body()); // IPC call 2 console.log(n.modificationDate()); // IPC call 3 }); // With 1000 notes = 3000 IPC calls // GOOD: Batch extract in a single JXA evaluation const data = Notes.defaultAccount.notes().map(n => ({ title: n.name(), modified: n.modificationDate().toISOString(), })); // Single JXA evaluation, much faster for bulk reads
Strategy 2: Local SQLite Cache
#!/bin/bash # Export notes to SQLite for fast local queries DB="$HOME/.notes-cache.db" sqlite3 "$DB" "CREATE TABLE IF NOT EXISTS notes ( id TEXT PRIMARY KEY, title TEXT, body TEXT, folder TEXT, created TEXT, modified TEXT, indexed_at TEXT );" osascript -l JavaScript -e ' const Notes = Application("Notes"); Notes.defaultAccount.notes().map(n => JSON.stringify({ id: n.id(), title: n.name(), body: n.plaintext(), folder: n.container().name(), created: n.creationDate().toISOString(), modified: n.modificationDate().toISOString() })).join("\n"); ' | while IFS= read -r line; do echo "$line" | jq -r '[.id, .title, .body, .folder, .created, .modified, now | todate] | @csv' \ | sqlite3 "$DB" ".import /dev/stdin notes" done 2>/dev/null # Now query locally (instant) sqlite3 "$DB" "SELECT title FROM notes WHERE body LIKE '%project%' ORDER BY modified DESC LIMIT 10;"
Strategy 3: Incremental Sync
// src/sync/incremental.ts import { execSync } from "child_process"; import { readFileSync, writeFileSync } from "fs"; const LAST_SYNC_FILE = ".notes-last-sync"; function getLastSync(): Date { try { return new Date(readFileSync(LAST_SYNC_FILE, "utf8").trim()); } catch { return new Date(0); } // First run: sync everything } function incrementalSync(): void { const lastSync = getLastSync(); const allNotes = JSON.parse(execSync( `osascript -l JavaScript -e 'JSON.stringify(Application("Notes").defaultAccount.notes().map(n => ({id: n.id(), title: n.name(), modified: n.modificationDate().toISOString()})))'`, { encoding: "utf8" } )); const changed = allNotes.filter((n: any) => new Date(n.modified) > lastSync); console.log(`${changed.length} notes modified since ${lastSync.toISOString()}`); // Process only changed notes (fetch full body only for these) for (const note of changed) { console.log(`Syncing: ${note.title}`); // ... process individual note } writeFileSync(LAST_SYNC_FILE, new Date().toISOString()); }
Strategy 4: Use .whose()
for Filtered Queries
.whose()// .whose() pushes filtering to Notes.app (faster than client-side filter) const Notes = Application("Notes"); // Faster than loading all notes and filtering in JS const recentNotes = Notes.defaultAccount.notes.whose({ _match: [ObjectSpecifier().modificationDate, ">", new Date(Date.now() - 86400000)] }); // Search by name (case-insensitive) const matches = Notes.defaultAccount.notes.whose({ name: { _contains: "project" } });
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Script hangs for >60s | Too many notes with body() access | Use first to assess scale; use cache for large vaults |
| Memory spike during export | All note bodies loaded into JXA runtime | Process in batches; stream to file instead of building array |
| SQLite cache stale | Forgot to re-sync after edits | Run incremental sync on schedule via launchd |
returns wrong results | Complex predicates not supported in JXA | Fall back to full load + JS filter for complex queries |
| iCloud sync slows writes | Each write triggers sync | Batch writes with 1s delay; use "On My Mac" for bulk import |
Resources
Next Steps
For handling rate limits during bulk operations, see
apple-notes-rate-limits. For monitoring performance trends, see apple-notes-observability.