ifolder-sync
View on GitHub →The idea for this project was born from a very specific, very personal itch: I wanted a single folder — my Obsidian vault — from an iCloud account different from the one signed into my Mac to stay synced locally, in real time, without signing that whole account into the system, creating another macOS user, or syncing its entire Drive. macOS simply doesn’t do this natively, so I went and solved it by hand.
Key Highlights
So, this one is a much smaller-scoped little project than the others in the LAB, but it has more engineering hidden in it than it lets on. Let’s take it piece by piece.
-
The problem it solves. macOS only natively syncs the iCloud Drive of the account signed into the system — there’s no way to locally mount the Drive of a second account. ifolder-sync reaches the other account through the (unofficial) iCloud web API, via
pyicloud: you authenticate with the Apple ID + password + 2FA of the account to sync, the session is saved (2FA is asked for once, with ~90 days of trust in practice), and from then on the chosen folder mirrors local ↔ remote on its own. -
True bidirectional sync, with a three-way baseline. The heart of the engine is a SQLite baseline that stores the signature (size + mtime) of each file as it was at the last sync, per side. Each pass compares the current state of each side against that baseline — and that’s exactly what tells “new file here” apart from “file deleted there” (without the baseline, the two would look identical). Changed on one side only → copy it to the other; changed on both → conflict (resolved by policy); gone from one side without having changed → propagate the deletion. Each pass decides every action first and only then applies them — which is what enables
--dry-runand the delete threshold further down. -
Change detection on two channels. On the local side, an FSEvents watcher (
watchdog) with debounce — near-instant. On the remote side, adaptive polling: while changes are flowing it speeds up (every 20s), and when things cool down it falls back to the idle interval (60s). There’s no iCloud webhook, so a remote change only shows up on the next poll. The cheap stays cheap because iCloud folder etags fingerprint whole subtrees — a no-change pass costs about one network call. -
The safety model — where the real engineering lives. Bidirectional sync on top of a reverse-engineered API can fail in unforeseen ways, so the engine is defensive by default. The guard-rails worth calling out:
-
Walk guard on both sides: if the remote listing fails (network/auth) or the local scan hits a permission error / missing vault root, the pass aborts with zero deletions, instead of mistaking partial visibility for “everything got deleted.”
-
Vault identity marker: a local
.ifolder-sync-vaultfile ties the baseline to the folder that produced it. Moved or recreated the vault? The daemon stops with “vault identity mismatch” instead of deriving phantom deletions — you recover withifolder-sync rebaseline, and the next pass becomes purely additive (downloads and uploads, never deletes). -
Delete threshold: if a single pass would erase more than 50% of tracked files (or more than 100), it skips the deletions and warns. Conscious override with
sync --force-delete. If it trips right after start, it flags DRIFT SUSPECTED; after 3 consecutive trips, it slows polling until a clean pass. -
Bootstrap pass: the first pass after start transfers content normally, but defers all deletions to the next pass. A destructive decision never rides on a possibly-stale startup view.
-
Recoverable soft-delete: a local deletion goes to a trash outside the vault; a remote deletion goes to iCloud’s “Recently Deleted” (recoverable for ~30 days). Nothing actually vanishes on the first move.
-
-
Smart
.obsidian/handling (opt-in). Obsidian’s config folder mixes two things that can’t be synced the same way: the shared assets (plugin code, themes, snippets, icons — these sync, so you get the same plugins on every device) and the volatile per-device config (workspace.json,appearance.json,community-plugins.jsonand friends — these never sync, because that’s exactly what causes the theme “reverting” on its own and the plugin list going incoherent between desktop and mobile). Turn it on withifolder-sync init --obsidian; in a plain folder, it syncs.obsidian/like any other content. -
Set-and-forget via launchd.
start --backgroundhands the daemon to launchd, macOS’s own service manager: it starts at every login (RunAtLoad), gets restarted if it crashes (KeepAlive), and survives logout and reboot with no extra “add to startup” step. One agent per profile. The only thing that turns it off is you (ifolder-sync stop).
Other Features
-
Profiles (multiple folders): each synced folder is an isolated profile, with its own config, state, lock, and launchd agent. A failed scan or a crash in one profile never touches another’s baseline. Sessions are shared per Apple ID — profiles on the same account reuse a single 2FA; different accounts get separate sessions automatically.
-
Password never in a file: resolution follows an order — environment variable, then the macOS Keychain, and finally the interactive prompt (which, on success, saves it to the Keychain automatically so the non-interactive daemon can connect later). It has to be the Apple ID’s primary password; app-specific passwords don’t work in the web/SRP flow iCloud Drive uses.
-
2FA with a real fallback: ifolder-sync fires the push itself and offers resend, SMS, and diagnostics — killing the classic “asks for a code but none ever arrives” of API-based logins.
-
Configurable conflict policy:
newer(default — the newermtimewins and the loser is saved as.conflict-…, locally, losing nothing),local,remote, orboth. -
A credential-free test suite: an in-memory fake iCloud covers the entire bidirectional matrix (create/edit/delete on both sides, idempotence, conflict), the Obsidian taxonomy, and the safety guard-rails (walk guards, threshold, path traversal, locking, soft-delete, dry-run).
-
The limitations I own up to openly: this is reverse-engineered Apple API — it can break when Apple changes something, has no SLA or official support, and is subject to rate limiting. Advanced Data Protection (ADP) is incompatible: enabling ADP on the Apple ID ends web access to iCloud Drive entirely (this affects every tool in this space, not just this one). And it’s honestly alpha, built for my personal use — don’t point it at a vault you don’t have a full backup of. Use it at your own risk, as the MIT license says.
-
Documentation: the full component map, the sync-pass sequence diagram, and the safety and performance models live in docs/ARCHITECTURE.md.
Related posts
Not referenced in any post yet.