Contacts consolidate
A runbook for cleaning up the Mac's local Contacts store, which is iCloud-synced, so every change propagates to all devices. macOS only.
Golden rules (read first)
- Back up before ANY destructive step (dedupe merge, source removal). Two backups, because they fail differently: a Contacts Archive (
Contacts.app → File → Export → Contacts Archive, giving a .abbu) AND a raw copy of the source folders (cp -R ~/Library/Application\ Support/AddressBook/Sources/<uuid> ~/contacts-backup-<date>/).
- Writes go through JXA, never raw SQLite.
osascript -l JavaScript with Application('Contacts'). New cards land in the default account, which should be iCloud. Editing the .abcddb directly bypasses CloudKit sync and can corrupt or desync the store. SQLite is READ-only here.
- Confirm destructive actions before running them: the dedupe merge, and removing a source. Enrichment additions (new cards, filling empty fields) can be auto-applied for high-confidence matches; everything uncertain goes to a review list.
- Fill empty fields only. Never overwrite existing data. Guard every write: exactly one match, and the field currently empty.
- The user's notes are authoritative for life facts, and you must not fabricate. If the notes and mail lack a fact, leave the card alone and say so. Never infer a value.
- This skill is about contacts. Reverse-direction edits to the notes only ADD or correct; they never delete.
Tools
- Dump:
python3 scripts/dump_contacts.py [out.json], all sources to JSON, a headless SQLite read.
- Analyse:
python3 scripts/analyze_contacts.py [dump.json], reporting sources, duplicate clusters (exact versus divergent), and data unique to secondary sources.
- Invoke bundled scripts as
python3 <path> rather than ./<script>, because a synced folder can strip execute bits.
Workflow
1. Snapshot and assess
python3 scripts/dump_contacts.py /tmp/contacts_dump.json
python3 scripts/analyze_contacts.py /tmp/contacts_dump.json
Note the total count, duplicate clusters (exact is safe, divergent needs review), the number of sources, and any data unique to a secondary source. Identify the primary iCloud source: it is the largest, and you can confirm it with
sqlite3 ~/Library/Accounts/Accounts4.sqlite "SELECT ZACCOUNTDESCRIPTION,ZIDENTIFIER FROM ZACCOUNT"
The iCloud contacts account's identifier equals the primary source UUID.
2. Dedupe (confirm first, back up first)
Contacts.app → Card → Look for Duplicates. Leave "Also merge cards that have the same name but contain different information" UNCHECKED on the first pass, which merges only byte-identical cards. Only re-run with the box checked after the analyser confirms the same-name clusters really are one person each (zero divergent clusters), because otherwise it fuses two different people who share a name. Re-dump and re-analyse to confirm no clusters remain.
3. Mine the notes for enrichment
Spawn a sub-agent to extract contact-worthy information about real, living, contactable people, giving it the dump path. Scope it explicitly:
- INCLUDE: family, in-laws, friends, personal service providers (doctors, trades), colleagues.
- EXCLUDE: genealogy subjects and the dead; research subjects, who are topics rather than contacts and will otherwise arrive as cards; businesses that are not personal providers.
- For each person output: name, aliases, relationship, phones, emails, address, birthday, employer and role, a note, the source paths, whether a card already exists (matched on email, phone or name), and which fields that card lacks. Write it to JSON.
4. Optional: mail enrichment, known people only
Mine signatures and headers through a mail connector to fill missing details for people already in contacts or the notes. Keep it bounded: do not trawl for new strangers unless asked.
5. Match and apply (JXA, guarded)
- Matching: by email (reliable) or exact unique full name. Raw-digit phone
contains matching FAILS, because cards store spaced formats. Normalise to the last nine digits in code if you must match on phone.
- Apply only where exactly one card matches and the target field is empty. A person with no card at all gets one created.
- Auto-apply the unambiguous matches; collect everything else into a review list.
- Log every action. See
references/enrich-template.js for the guarded JXA pattern.
6. Reverse: contacts to notes
Flag any card carrying information the notes lack (a surname, an employer, a number) and ADD it to the relevant page. Never delete. Verify a claimed gap actually exists before "filling" it, because sub-agents do sometimes mis-report one.
7. Single source of truth (confirm first, back up first)
If the analyser shows more than one source:
8. Verify and confirm sync
- Read affected cards back via JXA and confirm the fields persisted.
- Confirm cross-device: created cards appear on the phone.
- iPhone gotcha:
Settings → Contacts → Default Account = iCloud only routes NEW contacts. Existing "On My iPhone" contacts need Settings → [name] → iCloud → Contacts switched OFF, choosing "Keep on My iPhone", then back ON, choosing "Merge". Only the device owner can do that.
Report
Summarise: dedupe result (before and after), enrichment applied as a table, the review list, single-source status, where the backups are, and any cross-device caveat. Keep uncertain items, such as bare-name cards, flagged for the user to reconcile.
1---2name: contacts-consolidate3description: Dedupe and consolidate macOS/iCloud contacts to a single iCloud source of truth, enriching cards from the user's notes (and optionally Gmail) and flagging facts worth writing back. Use when the user wants to "sync/clean up/consolidate my contacts", dedupe contacts, or keep the contact list current. NOT for one-off single-contact edits.4---56# Contacts consolidate78A runbook for cleaning up the Mac's local Contacts store, which is iCloud-synced, so every change propagates to all devices. macOS only.910## Golden rules (read first)1112- **Back up before ANY destructive step** (dedupe merge, source removal). Two backups, because they fail differently: a Contacts Archive (`Contacts.app → File → Export → Contacts Archive`, giving a `.abbu`) AND a raw copy of the source folders (`cp -R ~/Library/Application\ Support/AddressBook/Sources/<uuid> ~/contacts-backup-<date>/`).13- **Writes go through JXA, never raw SQLite.** `osascript -l JavaScript` with `Application('Contacts')`. New cards land in the default account, which should be iCloud. Editing the `.abcddb` directly bypasses CloudKit sync and can corrupt or desync the store. **SQLite is READ-only here.**14- **Confirm destructive actions** before running them: the dedupe merge, and removing a source. Enrichment additions (new cards, filling empty fields) can be auto-applied for high-confidence matches; everything uncertain goes to a review list.15- **Fill empty fields only. Never overwrite existing data.** Guard every write: exactly one match, and the field currently empty.16- **The user's notes are authoritative for life facts, and you must not fabricate.** If the notes and mail lack a fact, leave the card alone and say so. Never infer a value.17- **This skill is about contacts.** Reverse-direction edits to the notes only ADD or correct; they never delete.1819## Tools2021- **Dump:** `python3 scripts/dump_contacts.py [out.json]`, all sources to JSON, a headless SQLite read.22- **Analyse:** `python3 scripts/analyze_contacts.py [dump.json]`, reporting sources, duplicate clusters (exact versus divergent), and data unique to secondary sources.23- Invoke bundled scripts as `python3 <path>` rather than `./<script>`, because a synced folder can strip execute bits.2425## Workflow2627### 1. Snapshot and assess28```29python3 scripts/dump_contacts.py /tmp/contacts_dump.json30python3 scripts/analyze_contacts.py /tmp/contacts_dump.json31```32Note the total count, duplicate clusters (exact is safe, divergent needs review), the number of sources, and any data unique to a secondary source. Identify the primary iCloud source: it is the largest, and you can confirm it with33```34sqlite3 ~/Library/Accounts/Accounts4.sqlite "SELECT ZACCOUNTDESCRIPTION,ZIDENTIFIER FROM ZACCOUNT"35```36The iCloud contacts account's identifier equals the primary source UUID.3738### 2. Dedupe (confirm first, back up first)3940`Contacts.app → Card → Look for Duplicates`. **Leave "Also merge cards that have the same name but contain different information" UNCHECKED on the first pass**, which merges only byte-identical cards. Only re-run with the box checked after the analyser confirms the same-name clusters really are one person each (zero divergent clusters), because otherwise it fuses two different people who share a name. Re-dump and re-analyse to confirm no clusters remain.4142### 3. Mine the notes for enrichment4344Spawn a sub-agent to extract contact-worthy information about **real, living, contactable** people, giving it the dump path. Scope it explicitly:45- INCLUDE: family, in-laws, friends, personal service providers (doctors, trades), colleagues.46- EXCLUDE: genealogy subjects and the dead; research subjects, who are topics rather than contacts and will otherwise arrive as cards; businesses that are not personal providers.47- For each person output: name, aliases, relationship, phones, emails, address, birthday, employer and role, a note, the source paths, whether a card already exists (matched on email, phone or name), and which fields that card lacks. Write it to JSON.4849### 4. Optional: mail enrichment, known people only5051Mine signatures and headers through a mail connector to fill missing details for people already in contacts or the notes. Keep it bounded: do not trawl for new strangers unless asked.5253### 5. Match and apply (JXA, guarded)5455- **Matching:** by **email** (reliable) or **exact unique full name**. Raw-digit phone `contains` matching FAILS, because cards store spaced formats. Normalise to the last nine digits in code if you must match on phone.56- Apply only where exactly one card matches and the target field is empty. A person with no card at all gets one created.57- Auto-apply the unambiguous matches; collect everything else into a review list.58- Log every action. See `references/enrich-template.js` for the guarded JXA pattern.5960### 6. Reverse: contacts to notes6162Flag any card carrying information the notes lack (a surname, an employer, a number) and ADD it to the relevant page. Never delete. Verify a claimed gap actually exists before "filling" it, because sub-agents do sometimes mis-report one.6364### 7. Single source of truth (confirm first, back up first)6566If the analyser shows more than one source:67- Preserve any data unique to a secondary source into the iCloud primary FIRST, via JXA.68- Confirm the secondary is safe to remove: it is not listed in `Accounts4.sqlite` (an orphan), or it is a stale duplicate fully contained in the primary.69- Remove it:70 ```71 osascript -e 'tell application "Contacts" to quit'72 killall contactsd73 mv ~/Library/Application\ Support/AddressBook/Sources/<uuid> ~/.Trash/74 ```75 Relaunch Contacts, verify the primary count is intact and that it is now the only source.7677### 8. Verify and confirm sync7879- Read affected cards back via JXA and confirm the fields persisted.80- Confirm cross-device: created cards appear on the phone.81- **iPhone gotcha:** `Settings → Contacts → Default Account = iCloud` only routes NEW contacts. Existing "On My iPhone" contacts need `Settings → [name] → iCloud → Contacts` switched OFF, choosing "Keep on My iPhone", then back ON, choosing "Merge". Only the device owner can do that.8283## Report8485Summarise: dedupe result (before and after), enrichment applied as a table, the review list, single-source status, where the backups are, and any cross-device caveat. Keep uncertain items, such as bare-name cards, flagged for the user to reconcile.