Notebook
Note
Download Markdown

Mac Dropbox Rsync 3 Way Backup

8 min read

A 3-way rsync backup to Dropbox, for a Mac

Why would you need this?

I used to put all my Markdown note files and code in one single folder, but it created problems for me:

  1. I couldn’t easily access my Markdown files on my mobile phone because the easiest way to do that is through Obsidian Sync, and I couldn’t sync the folder to Obsidian because it was too big for the standard sync plan, and it tried to sync all kinds of code files that Obsidian isn’t designed to read.
  2. I couldn’t create a foolproof backup system with just one folder because only some of the subfolders were being backed up to GitHub, and that left many important notes not backed up. When I tried to use rsync to back up the whole huge folder to Dropbox, it kept crashing. You cannot easily sync tens of thousands of files, adding up to 20 GB of data, to Dropbox every hour without creating huge errors and problems.

The solution was to split up my work into three folders: Notes, Code and CoWork.
Here’s how I use them:

  1. Notes — a big folder of markdown documents, sitting on top of a pile of other stuff you don’t want backed up (app config, cache folders, whatever else accumulates). You want to back up only specific file types — docs, spreadsheets, images, PDFs — and ignore everything else, however deep it’s buried.
  2. Data files — spreadsheets, exports, reports. No junk mixed in. You want to back up everything here, because you don’t know in advance what file types you’ll add next, and a filter that only allows known types will silently drop something new.
  3. Code — projects, most of which are already git repositories pushed to GitHub (so they’re already backed up off-site). A few aren’t. You want this backup to cover the gap — skip whatever’s already on GitHub, catch everything that isn’t — without maintaining a hand-typed list of which projects those are.

Those three folders need opposite filtering logic. That’s the whole reason this is a 3-way sync and not one rsync command with a shared exclude list — a filter that’s right for folder 1 would be badly wrong for folder 2, and neither would know to skip a folder just because it happens to be a git repo.

This note covers an approach for automatically, silently backing up three different kinds of folder from a Mac to Dropbox on a timer — using nothing but tools that ship with macOS (rsync, launchd) plus Homebrew’s rsync for reliability. No third-party backup app, no subscription.

This isn’t a generic “back up my whole Mac” tool. It’s for a specific, common situation: you have several folders that are different kinds of thing, and you want each one filtered differently.

The three pieces

NotesData filesCode
Sample source~/Notes/~/Data/~/Code/
Sample destinationDropbox/Notes Backup/Dropbox/Data Backup/Dropbox/Code Backup/
Filter styleAllow-list — name the file types you want, skip everything elseDeny-list — copy everything except a short named listDeny-list, plus a list of git repos generated fresh on every run
Sample exclude fileexcludes-notes.txtexcludes-data.txtexcludes-code.txt

Allow-list vs. deny-list is the one design decision worth understanding before you copy this. An allow-list (+ *.pdf, + *.md, … then - * at the bottom to drop everything else) is right when the folder has a lot of things you never want and a short list of things you do. A deny-list (name the few things to skip, nothing at the end to catch the rest) is right when the opposite is true — because with an allow-list, anything you forget to add a rule for is dropped silently, forever, with nothing telling you it happened. For a folder with no fixed set of file types, that’s a trap. Pick based on which mistake is more likely: “I forgot to exclude something” is loud (you’ll see the extra file in Dropbox); “I forgot to include something” is silent.

Skip repos already on GitHub, automatically. The code-sync leg works out which folders are git repositories on every run, rather than you maintaining a typed list of “these are pushed, skip them.” The upside is specific: a repo you create tomorrow is skipped without you touching this script again, and an ordinary folder you create tomorrow gets backed up without you remembering to add it. A hand-typed list fails the second way silently — you forget to add a folder, and it just never gets backed up, with nothing telling you.

Why not just use one Dropbox-synced folder for everything?

You can, and for some people that’s simpler and the right answer — skip all of this and look at this note.

This pattern is for the specific case where one of your folders is too large or too noisy to live inside Dropbox’s own sync (a “Notes” folder sitting on top of thousands of project files, most of which you don’t want in Dropbox at all), so you need something more selective than “sync this whole folder.” rsync run on a timer gives you that selectivity; Dropbox itself doesn’t.

What macOS makes annoying, and the workaround

On macOS Ventura and later, Dropbox lives at ~/Library/CloudStorage/Dropbox/..., and that whole folder is protected by macOS’s privacy layer (TCC). A plain shell script run by launchd cannot write there — you’ll see rsync error: ...: open: Operation not permitted in the log, every run. And you can’t fix it by granting Full Disk Access to /bin/bash: macOS deliberately blocks the system shells from being added to Full Disk Access at all, to stop exactly this kind of workaround.

The fix that actually works: wrap the shell script in a tiny AppleScript .app (osacompile -e 'do shell script "/path/to/script.sh"'). A .app can be granted Full Disk Access, and the grant reaches the rsync process it launches. launchd then runs the .app’s binary (Contents/MacOS/applet) instead of calling the script directly.

This example doesn’t include the .app-wrapping step — it’s a handful of extra commands (osacompile, then setting a bundle identifier with PlistBuddy, then re-signing with codesign because editing the Info.plist invalidates the default signature) that are mechanical rather than interesting. If you want that step spelled out in full, ask an AI coding assistant that can run shell commands on your Mac to build it — paste in this README as the goal, and the four points in this section as the constraints, and it can wire up the .app, the Full Disk Access grant, and the launchd job end-to-end.

Files in this example

Copy the sync-3way-backup.txt file to somewhere like ~/Scripts/,, rename it to sync-3way-backup.sh so you Mac knows it’s a shell command file, copy the three excludes-*.txt files next to it, edit the SOURCE/DESTINATION lines and the three exclude-file paths near the top of the script, then set up a launchd job (~/Library/LaunchAgents/com.example.dev-backup.plist) with a StartInterval in seconds to run it on a timer. RunAtLoad: true also runs it once whenever you log in.

The one exit-code trap worth knowing about before you rely on this

rsync’s exit code alone is not a reliable success/fail signal. Exit code 23, for example, fires both for “some files genuinely failed to copy” and for “rsync tried to delete some now-empty folders in the destination and couldn’t, because they still hold leftover files from before you changed what gets backed up” — which is completely harmless. Treating every non-zero exit code as a real failure means you get a false alarm every single run, forever, for a backup that’s actually working. The script separates the two by counting how many of the log lines are that one specific harmless warning versus anything else, and only reports failure when something else is wrong. See the comments in the script for the full reasoning — it’s the most important part of this pattern, and the part most tutorials skip.

What this pattern does NOT do