Keep Google, iCloud, and Microsoft/Outlook personal contacts in sync using each provider's native API/protocol — no unofficial scraping.
Status: actively developed, built for personal use. Use at your own risk — back up your contacts on each provider before your first real sync.
A local SQLite database (contacts.db) holds a canonical merged copy of every contact plus links to its ID on each service. Each sync run pulls incremental changes from all three providers, merges them (multi-value fields like emails/phones are unioned; single-value fields use newest-edit-wins), and pushes the merged result back out.
Every real (non-dry-run) sync appends one line per contact created/updated/deleted to sync.log (gitignored) — useful for auditing what a run actually changed or diagnosing an unexpected sync result.
See docs/superpowers/specs/2026-07-01-contacts-sync-design.md for the full design and docs/superpowers/plans/2026-07-01-contacts-sync.md for the task-by-task implementation plan.
pip install -e ".[dev]"- Credentials for all three providers are stored in a local
.envfile (gitignored) in the project root, created automatically the first time you run anauthcommand. - Google: create OAuth credentials (Desktop app type) in Google Cloud Console for the People API, set the consent screen's publishing status to "In production" (you'll see an "unverified app" warning when you authorize — that's expected for a personal-use app), then run:
contacts-sync auth google --client-secrets path/to/client_secrets.json - Microsoft: register a public-client app in Entra ID (Azure Portal), enable "Allow public client flows," support "personal Microsoft accounts," then run:
contacts-sync auth microsoft --client-id <your-client-id> - iCloud: generate an app-specific password at appleid.apple.com, then run:
contacts-sync auth icloud
You need to complete auth for all three providers before sync will do anything — see Known limitations below.
contacts-sync doctor # check all three providers are reachable
contacts-sync sync --dry-run --microsoft-client-id X # preview changes without writing
contacts-sync sync --microsoft-client-id X # run a real sync
contacts-sync sync --full --microsoft-client-id X # full re-pull + re-merge (clears sync tokens/etags)
contacts-sync fix-names # preview split of "full name stuck in first-name field"
contacts-sync fix-names --apply # write those splits locally and push to all providers
contacts-sync fix-photos # preview photo repairs from user-set Google photos
contacts-sync fix-photos --apply # apply photo repairs and push to Microsoft/iCloud
contacts-sync push --all # force-push every local contact to every provider
contacts-sync review # resolve ambiguous first-time matches
contacts-sync status # see contact counts and sync token state
contacts-sync version # print the installed version
Set MICROSOFT_CLIENT_ID in your environment to avoid passing --microsoft-client-id every time (it's read as the option's envvar, so --microsoft-client-id can be omitted once it's set).
dist/contacts-sync/contacts-sync.exe is a fully standalone build — no Python/venv install needed to run it, just the folder. Build it once from a dev machine with .venv set up:
pip install -e ".[dev,build]"
./build.ps1
This runs pyinstaller contacts-sync.spec, producing dist/contacts-sync/. Copy that whole folder anywhere; .env/contacts.db/sync.log are created next to the exe (resolved relative to the exe's own location, not whatever directory you launch it from), so the folder is portable. Add dist/contacts-sync to your PATH to call it from anywhere as contacts-sync.
Syncs run with earlier versions had a bug that silently dropped the second provider's data (structured first/last names, photos, extra emails) when a contact was matched across providers for the first time. If your contacts show the full name in the first-name field or are missing photos, run:
contacts-sync sync --full --microsoft-client-id X # re-pull everything; backfills photos and any provider-side data
contacts-sync fix-names # preview name splits for whatever is still unsplit
contacts-sync fix-names --apply # apply and push the splits
contacts-sync fix-photos # preview photo repairs (versions before v0.2.2 could sync a
contacts-sync fix-photos --apply # wrong-person PROFILE photo; this restores user-set photos)
- Google and iCloud credentials are checked eagerly before a sync starts — if either hasn't been set up yet (
contacts-sync auth <provider>not run),syncfails immediately with an error likeNo Google credentials found. Run 'contacts-sync auth google' first., even if you only care about syncing the other two right now. This is documented, deliberate v1 behavior, not a bug. Microsoft credentials are checked lazily instead: a missing/invalid Microsoft credential is not caught up front, sosyncstill runs and syncs Google/iCloud successfully, reporting the Microsoft failure separately in the summary — the same per-provider isolation used for other mid-sync failures (e.g. an expired token, a network error). Runcontacts-sync doctorto check all three providers' credentials up front without running a real sync. - Postal addresses are modeled in the local database (
addressestable) but no provider adapter reads or writes them yet — this is unimplemented, not just unmerged. - The iCloud CardDAV addressbook path is hardcoded to Apple's common default (
/carddavhome/addressbooks/card/) rather than discovered per-account, so it may not work for every Apple ID. - No scheduler is built in — run
contacts-sync syncmanually or via your OS's task scheduler/cron.
pytest -v