My AI App Kept Dying on a $4,000 MacBook. The Code Was Fine.
How iCloud silently sabotages Node.js projects β and why every fix I tried made it worse.
My app crashes. Random crash, the worst kind. It dies like a goldfish you find floating in the morning with zero explanation. Fifteen-minute builds on a $4,000 MacBook. The code compiles. Tests pass. And yet: next: command not found.
For two days Claude blamed Next.js, Turbopack, Node, npm, the weather. I read logs like a medieval monk copying scripture. Nothing. The culprit was hiding inside macOS itself, in something no developer thinks to check: iCloud Drive. Every fix attempt made the problem exponentially worse.
TLDR: If your projects live in ~/Documents/, iCloud treats node_modules like vacation photos and evicts files at random. Every "smart" fix makes it worse. The actual fix is one command. This article saves you two days.

And the thing that stings: my dev process is solid. I build with prompt contracts that turn AI coding from gambling into shipping. The code was tested, structured, documented. Which meant the problem had to live somewhere else entirely.
The App That Refused to Run
It started on a Tuesday. The Next.js dev server would boot, compile for a while, then die. Sometimes after 30 seconds. Sometimes after 10 minutes. Sometimes it would run fine for an hour then crash mid hot reload.
No pattern. No reproducible trigger. Just death.
Turbopack would complain about missing modules. Files that clearly existed according to ls would vanish mid-compilation. I'd restart, everything would work, then crash again while testing a component.
I did what any developer does: blamed the framework. Cleared .next. Deleted node_modules, ran npm install. Checked Turbopack config. Downgraded. Upgraded. Nothing helped consistently.
Then, out of pure desperation, I closed VS Code and opened Finder.
The bug wasn't in the code. It wasn't even in the terminal.
The Invisible Eviction
Small cloud icons. On files inside node_modules.
<figure data-source="image">

iCloud Drive was evicting JavaScript files from my dependencies. Removing actual source code from disk and replacing it with cloud placeholders to "optimize storage." My node_modules folder treated like a vacation photo gallery: uploaded, then cleaned up locally because surely I didn't need all those files on my machine.
Why? My projects lived in ~/Documents/dev/. On macOS, ~/Documents/ is synced to iCloud Drive by default. Every file in there is fair game.
The known fix is the .nosync suffix pattern:
mv node_modules node_modules.nosync
ln -s node_modules.nosync node_modules
Rename the real folder so iCloud ignores it, create a symlink so Node.js still finds it at the expected path.
Applied the fix. Server restarted. Dashboard loaded.
Problem solved. The next morning, everything was broken again.
The Descent: Five Fixes That Made Everything Worse
Morning coffee. Open terminal. next: command not found. The symlink was gone. Not the folder, the folder was right there, intact. Just the symlink had vanished.
Recreated it. Server booted. Fifteen seconds later: crash. Symlink gone.
That's when I noticed:
$ ls | grep node_modules
node_modules 3
node_modules 4
node_modules 5
node_modules 6
node_modules 7
node_modules 8
node_modules.nosync
iCloud wasn't deleting my symlink. It was renaming it. Conflict detected with some server-side ghost, resolved by slapping a number on the local version. By end of day: node_modules 20.
What follows is five attempts at fixing this. Each one more clever than the last. Each one making things worse. A proper descent into madness, like one of those horror stories where every door you open leads to a smaller room.
Fix 1: Clean up the numbered copies. Obviously the duplicates feed the loop. Remove them, problem goes away. Worked for 20 seconds. Then node_modules 9 appeared. The server-side entry was still there.
Fix 2: Extended attributes to exclude from sync. macOS has extended attributes (xattrs) that control how the file provider handles files. I marked the symlink as excluded:
xattr -w com.apple.bird.metadata '{"excludeFromSync":true}' node_modules
xattr -w com.apple.fileprovider.ignore 1 node_modules
This is the fix that broke everything for real. Instead of random disappearances, the symlink now vanished in 15 seconds flat. Every. Single. Time. Like I'd painted a target on it.
Fix 3: Immutable flag. chflags uchg makes a file undeletable. Surely nothing can override the filesystem itself?
iCloud's file provider daemon runs with elevated privileges. Bypassed the immutable flag, created node_modules 19, removed the "protected" original. (So much for that.)
Fix 4: Move node_modules outside iCloud entirely. Put the real files in ~/Library/ (not synced), symlink points there. Logical.
Still crashed. iCloud doesn't care where a symlink points to. It manages the symlink itself because the symlink lives in a synced directory. Safe target, unsafe link.
Fix 5: Watchdog script. A bash loop recreating the symlink every 2 seconds. Band-aid. Turbopack throws FATAL when node_modules vanishes mid-compilation. Two seconds is an eternity for a bundler.
Every "smart" fix gave iCloud one more raison d'Γͺtre to intervene.
<figure data-source="infographic">

The Project Next Door Worked Fine
After exhausting every clever idea, I did what I should have done first: compared my broken project with one that worked.
Another project sat in the exact same ~/Documents/dev/ folder. Same .nosync + symlink pattern. Never had a single conflict.
I ran xattr -l on both:
com.apple.fileprovider.dir#N: 1
com.apple.provenance:
com.apple.bird.metadata: {"excludeFromSync":true}
com.apple.fileprovider.dir#N: 1
com.apple.fileprovider.ignore: 1
com.apple.provenance:
Two extra attributes. com.apple.bird.metadata and com.apple.fileprovider.ignore. The exact ones I had added in Fix 2.
The healthy project had nothing special. It just hadn't been "protected."
One Command: xattr -c node_modules
xattr -c node_modules
Clear ALL custom extended attributes. Let iCloud treat the symlink as a boring, unremarkable file.
But clearing xattrs alone wasn't the end. iCloud's server still had the original node_modules directory from before the .nosync setup. Thirty minutes later, it downloaded that old copy and stomped over my symlink. The ghost was still on the server.
The full purge:
mv node_modules node_modules_purge.nosync
ls -la node_modules # should say "No such file"
ln -s node_modules.nosync node_modules
rm -rf node_modules_purge.nosync
The wait matters. Skip it and iCloud hasn't synced the deletion yet. Your fresh symlink conflicts with the server copy. Back to the loop.
After the full purge: the server held. Five minutes. Thirty minutes. An hour.
Here's the mechanism (Apple won't document this, so it's reverse-engineered from behavior): iCloud's file provider monitors synced directories. A file with non-standard xattrs gets flagged as "locally modified." That triggers a sync cycle comparing local vs. server. If the server has a different entry with the same name, conflict detected, local file renamed. The renamed file still carries the custom xattrs, which triggers another sync. Loop. Without custom xattrs, iCloud sees the symlink as unchanged and ignores it completely.
The best protection against iCloud is not attracting its attention.
Your AI App Is Only as Good as the OS Underneath
Here's what gets me. We obsess over prompt engineering, model selection, framework benchmarks. We compare Turbopack vs. Webpack like it's a holy war. And meanwhile the operating system underneath is quietly evicting our dependencies because it thinks node_modules is a photo album.
AI apps are more exposed to this than regular web apps. More files (agents need tooling, embeddings, model configs). More filesystem churn (hot reload cycles, cache regeneration). More things that look like "large folders of stuff iCloud should optimize." If your AI project has node_modules plus a vector store plus model artifacts, iCloud sees a mise en place of files to helpfully manage for you.
Five rules that would have saved me two days:
Always .nosync + symlink for node_modules. Automate it. A wrapper in .zshrc that runs after every npm install, renames the folder, recreates the link. Non-negotiable if your projects are in ~/Documents/.
Never touch xattrs in iCloud directories. No com.apple.bird.metadata. No com.apple.fileprovider.ignore. No clever attributes. They don't protect files from iCloud. They paint a target on them.
When rm -rf hangs, use mv with .nosync. mv node_modules old_nm.nosync is instant on APFS. Without .nosync in the new name, iCloud starts uploading the renamed folder. Hundreds of megabytes of node_modules synced for nothing. (And never mv to /tmp, different volume, triggers a copy.)
Keep .next as a real directory. Turbopack can't handle symlinks for its cache. If iCloud creates .next 2 or .next 3, delete the numbered copies, don't symlink.
Or just move out of iCloud. mv ~/Documents/dev ~/dev. That path isn't synced. Nuclear option, permanent fix. Probably what I should have done from the start instead of spending two days playing whack-a-mole with a system daemon.
Two days down the drain, the kind where you want to hurl your laptop into the pool. And I'm sitting in the sun, WiFi crawling like a three-legged dog on sedatives.
The code was solid. The framework configured right. It worked yesterday (the famous "it worked yesterday"). Some system service built to sync the pathetic vacation photos of people who think "the cloud" is magic storage. Not for dev. Definitely not for dev. And Apple with their compulsive secrecy about how any of this works under the hood...
The fix was one command. But Claude Code only found it after burning through five "solutions" that made everything worse. Each attempt to protect my files gave iCloud one more reason to attack them.
If your app runs on a Mac and your projects sit in ~/Documents/, check. Not your code. Not your deps. Your extended attributes. Because macOS wasn't designed for you. It was designed for Karen from Accounting.
PS: I disabled my own vacation photo sync a long time ago. Just saying. π
Sources: Apple's File Provider documentation (which conveniently says nothing about xattr conflict behavior). The .nosync pattern is community-documented but not officially supported by Apple.
(*) The cover is AI-generated. iCloud was busy evicting the real one. π€·
MacOS iCloud silently sabotaging your Node.js projects? Dive into real-world debugging lessons from a production-tested dev environment.