# Contacts Consolidate

> 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.

- Skill: `campbellsmurphy/contacts-consolidate` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add campbellsmurphy/contacts-consolidate`
- Raw SKILL.md: https://api.skillmd.com/api/skills/campbellsmurphy/contacts-consolidate/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: campbellsmurphy (https://skillmd.com/u/campbellsmurphy)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/campbellsmurphy/contacts-consolidate

---


# 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:
- Preserve any data unique to a secondary source into the iCloud primary FIRST, via JXA.
- 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.
- Remove it:
  ```
  osascript -e 'tell application "Contacts" to quit'
  killall contactsd
  mv ~/Library/Application\ Support/AddressBook/Sources/<uuid> ~/.Trash/
  ```
  Relaunch Contacts, verify the primary count is intact and that it is now the only 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.

