Back to blog
FILE 0x95·MOVING OFF ONEDRIVE ONTO A SYNCTHING MESH

Moving off OneDrive onto a Syncthing mesh

May 1, 2026 · homelab, syncthing, sync

I had been sync'ing my ~/code folder through OneDrive for years. It worked until it didn't. The breaking point was Xcode and git both fighting OneDrive for ownership of the same files. I moved the whole mesh off OneDrive and onto Syncthing.

What was happening

OneDrive's File Provider on macOS is opinionated. It mangles filenames, replaces large files with zero-byte placeholders that have to be "downloaded" on access, and rate-limits the upload API in ways that make git operations on a big repo feel like they're happening over a dial-up modem.

Specific pain points by the end:

What I found

The actual requirement is "the same ~/code directory on four machines, with a 30-day local history, conflicts resolved deterministically." There is no requirement for cloud storage. Cloud storage was just the transport I picked years ago.

Syncthing is the right transport for this: LAN-only, no API rate limits, deterministic conflict files named by device, configurable per-folder ignore patterns. The trade-off is no offsite copy by default — every peer is in my house.

The fix

The mesh ended up looking like this:

All four peers sync a single folder ID. Type sendreceive, ignorePerms=true, staggered versioning at 30 days. Each peer keeps its own local history.

Two gotchas worth writing down:

Inotify limits on the NAS. A 165k-file folder eats inotify watches. The default Linux limits are too low. Drop a file at /etc/sysctl.d/99-syncthing.conf:

fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 1024

Then sysctl --system and Rescan from the Syncthing UI. The "watcher failed" warning clears within about a minute. Be aware that DSM major upgrades may regenerate /etc/sysctl.conf and revert this, so keep a note.

An ignore file for git repos. Syncthing will happily sync .git/ between peers and produce sync-conflict files inside .git/:

.git/index.sync-conflict-20260506-031312-PEGASUS
.git/AUTO_MERGE.sync-conflict-...
.git/logs/HEAD.sync-conflict-...

These corrupt git state silently. The fix is a .stignore at the folder root, identical on all peers:

**/.git
**/.git/**
**/*sync-conflict-*
**/__pycache__
**/__pycache__/**
**/node_modules
**/node_modules/**
**/.next
**/.venv
**/venv
**/build
**/dist
**/.DS_Store
**/*.pyc
#recycle
@eaDir
**/@eaDir
**/@eaDir/**
**/Thumbs.db
**/desktop.ini

Apply via the REST API rather than editing the file by hand on every peer:

POST http://<peer>:8384/rest/db/ignores?folder=<folder-id>
X-API-Key: <key>
{"ignore": ["<line>", "<line>", ...]}

PowerShell quirk: Get-Content returns rich PSObjects, not strings. You have to cast to [string[]] before ConvertTo-Json, or the serializer will helpfully include PSPath / PSDrive metadata and Syncthing will reject the payload. Also write the JSON without a BOM or Syncthing's JSON parser chokes:

$lines = [string[]][System.IO.File]::ReadAllLines($path)
$json = $lines | ConvertTo-Json
[System.IO.File]::WriteAllText(
  $out, $json,
  (New-Object System.Text.UTF8Encoding $false))

What I'd do differently

I should have set up an offsite copy as a separate step, before ripping out the cloud transport. "Cloud was also my offsite" was an accidental property of the old setup that I didn't replace in the new one for a few weeks. The fix was a nightly packer pushing content-addressed blobs to a personal cloud bucket — same as the backup post — but I shouldn't have had a window where the only copies of my code were four machines on one LAN.

The other thing: .stignore first, sync second. Always. If you let Syncthing index a folder full of .git/ directories before the ignore is in place, you'll be cleaning up sync-conflict files inside .git for a week.