~/projects/icloud-linux/
iCloud Linux
A local-first Linux filesystem/sync project for mounting iCloud Drive with persistent caching, background hydration, bidirectional sync, and service-oriented UX.

The filesystem was the interface
iCloud Linux began with a simple frustration: iCloud Drive had no first-class Linux client, and a download script would not make it feel native.
I wanted ordinary Linux tools to see a mounted directory. That meant the project had to answer filesystem operations, not just call a cloud API. Listing a directory, opening a file, reading metadata, renaming, deleting, and writing all carry expectations that shells, editors, file managers, and applications assume without asking.
FUSE provided the boundary. The driver translated kernel-facing operations into a local metadata store, cache, and iCloud API calls. That sounds like an adapter, but the difficult behavior lives in timing: a listing should not wait on every remote file, an open may need to hydrate content, and a write should feel local before a background sync confirms the remote state.
The project became a study in preserving familiar semantics while one side of the mount was eventually consistent, authenticated over HTTP, and sometimes unavailable.
Metadata and content needed different lives
A useful mount could not redownload the entire tree or every file on each start.
I separated persistent metadata from hydrated content. SQLite records entries, paths, types, sizes, timestamps, remote identifiers, hydration state, and tombstones. A mirror cache stores local file data. The FUSE layer can answer many metadata operations from SQLite while downloading bytes only when an application actually opens an unhydrated file.
That separation made cold starts and directory traversal practical. It also created invariants: a database entry can describe a file whose content is absent, a cached file must correspond to the right remote identity, and a tombstone must prevent a deleted item from reappearing during the next refresh.
Stable path handling mattered throughout. iCloud filenames can arrive with normalization or encoding behavior that differs from local expectations. I added filename repair tooling and kept path allow-lists and exclusion rules centralized so the mount, hydration commands, and sync engine did not disagree about which subtree belonged to the user.
Hydration made local-first behavior visible
An unhydrated file had to look present without pretending its bytes were already local.
The driver records remote files in the metadata tree and marks whether content is hydrated. Opening an unhydrated file triggers the lazy-download path, writes the result into the mirror cache, and updates persistent state before returning data to the caller. Subsequent reads use the local copy.
I added explicit hydration commands for users who wanted more control. The command scans SQLite for eligible unhydrated entries, respects the same sync and exclusion paths as the driver, then opens each file through the FUSE mount so the normal lazy-download machinery remains the single implementation. Dry-run, progress, interruption, and rerun behavior make the operation understandable instead of hiding a large transfer behind ordinary file reads.
This was an important design choice: the convenience path and the bulk path share the same state transition. A separate downloader would have been easier to write, but it could have bypassed cache rules and produced two meanings of “hydrated.”
Writes became a queue, not a pause button
Blocking every local write on a remote round trip would make the filesystem feel broken whenever the network slowed down.
Local operations update the cache and metadata first, then the sync engine pushes remote changes in the background. Creates, modifications, renames, and deletions need enough recorded context to retry without guessing what happened. A failure should remain visible and recoverable rather than silently losing the local action or freezing the process that wrote the file.
The background loop reconciles local and remote state, applies path rules, and records progress in persistent state. Retries are safe because operations are derived from current metadata and remote identifiers rather than from an in-memory callback that disappears on restart. Tombstones keep deletes explicit until the remote side catches up.
The interesting systems tradeoff is that “write succeeded” has layers. The local filesystem operation can succeed because data is durable in the cache, while remote convergence is still pending. The CLI and logs need to expose that distinction so local-first behavior does not become false reassurance.
Remote refresh had to preserve local intent
Synchronization is easy when one side is authoritative and nothing changes concurrently. A mounted filesystem does not get that luxury.
A remote refresh can discover new files, changed metadata, moves, or deletes while local work is waiting to upload. Applying the remote tree blindly could discard a local write; treating every local difference as authoritative could resurrect an intentionally deleted remote item.
I used persistent identifiers, timestamps, hydration state, tombstones, and ordered reconciliation to decide which transitions were safe. The implementation favors explicit, inspectable behavior over a magical conflict resolver. When the available API cannot prove that one side should win, the system should preserve data and surface the disagreement.
That work changed the way I think about sync engines. The core is not copying bytes. It is carrying intent across failures, restarts, partial observations, and two timelines that do not advance together.
Authentication and service UX were part of the mount
A filesystem project is not useful if it only works from the author’s terminal after several undocumented commands.
iCloud authentication can require interactive steps and persisted session data. I separated authentication from the driver, added clear configuration for mount and cache paths, and wrapped common operations in `icloudctl`. Starting, stopping, checking status, reading logs, hydrating, and refreshing should feel like one tool rather than knowledge of several Python entry points.
The service path supports long-running use with predictable mount behavior and logs. Configuration files define sync and exclusion paths once, and environment-specific state lives outside the repository. Error messages point to the next useful check, such as an inactive service, missing state database, or inaccessible mount.
These pieces are less interesting than FUSE callbacks on paper, but they are what let another person use the system. Community issues and setup failures repeatedly showed that operational UX is part of systems programming, not documentation added after it.
Tests followed the boundary conditions
The project needed more than a happy-path mount test because most failures happen between layers.
Driver tests exercise path handling, directory and file operations, cache behavior, and remote-client seams without requiring every run to contact iCloud. Hydration tooling is structured around testable path filters and database queries. Separate scripts help inspect and repair filename state when external data violates local assumptions.
I also used real community reports as architecture feedback. Setup paths, authentication behavior, unexpected filenames, service state, and hydration edge cases exposed assumptions that a private test fixture would not. Fixes often improved the CLI or persisted model as much as the callback where the symptom appeared.
The result is still a personal open-source project rather than a complete replacement for Apple’s native clients. The value is in the boundary it makes concrete: POSIX behavior, local durability, remote APIs, and user trust meeting in one mount point.
Why this changed my direction
iCloud Linux is the project that pulled my production experience closer to the operating-system boundary.
The platform work I had done before was still relevant. Idempotency, background jobs, state machines, authentication, logging, and operator controls all reappeared, but now a shell command or file open exposed the result. The tighter interface made incorrect state transitions much harder to hide.
It also gave my low-level direction a more honest foundation. I am not claiming to have written a kernel filesystem or a new distributed protocol. I built a user-space filesystem and sync engine that had to respect the semantics and failure modes around them, then made it usable enough for people outside my machine to test.
- Separate metadata presence from local content hydration.
- Make local writes durable before remote convergence, then expose pending state honestly.
- Use one hydration transition for lazy reads and bulk commands.
- Treat setup, service control, logs, and community failures as part of the systems interface.