Copy Only Changed Files Mac: Safe Developer Backups

Copy only changed files Mac guide: use rsync dry runs, exclusions, and filtered sync for clean developer project backups.

Mac developer watching a project folder copy stall while thousands of generated files churn

copy only changed files Mac searches usually start after a developer has dragged the same project folder to a backup drive for the third time and watched macOS copy far more than needed. A codebase is not a photo album. Most of the bytes and file count often live in node_modules, .venv, .next/cache, dist, build, test caches, and other generated folders. The goal is not just “copy newer files.” The goal is to update a backup with the files that define the project, skip the junk your tools can recreate, and avoid turning every backup into a full filesystem crawl.

copy only changed files Mac: the safe developer workflow

The safest built-in answer on macOS is rsync. Finder can merge folders, and it may skip files that already exist in some conflict flows, but it is not a reliable incremental backup tool for developer projects. It does not give you a dry run, it does not show itemized copy/delete decisions, and it does not understand that node_modules is disposable while package-lock.json is source-of-truth.

A practical incremental copy starts like this:

rsync -avnih --delete \
  --exclude 'node_modules/' \
  --exclude '.venv/' \
  --exclude 'venv/' \
  --exclude '.next/cache/' \
  --exclude '.turbo/' \
  --exclude 'dist/' \
  --exclude 'build/' \
  --exclude 'coverage/' \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

The important pieces are:

  • -a keeps normal archive behavior: recursive copy, permissions, timestamps, symlinks, and similar metadata.
  • -v prints what is happening.
  • -n means dry run. Nothing is changed yet.
  • -i adds itemized change output so you can see why a file would move.
  • -h makes sizes readable.
  • --delete makes the destination match the source after exclusions. Use it for mirrors, not archives.

Review the output. If the copied paths and delete list make sense, run the same command without n:

rsync -avih --delete \
  --exclude 'node_modules/' \
  --exclude '.venv/' \
  --exclude 'venv/' \
  --exclude '.next/cache/' \
  --exclude '.turbo/' \
  --exclude 'dist/' \
  --exclude 'build/' \
  --exclude 'coverage/' \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

Why developer folders make incremental copy harder than normal folders

Most “copy changed files” advice assumes a folder full of documents where each file is meaningful. Developer projects are different. A small application can contain a few hundred source files and tens of thousands of generated files. Package managers, compilers, bundlers, test runners, type checkers, and language servers all write to the project tree during normal work.

That changes the economics of a backup. Even if rsync copies only changed file contents, it still has to walk the tree and compare metadata. A folder with 80,000 tiny files is expensive to scan. If the destination lives under iCloud Drive, Dropbox, Google Drive, OneDrive, a NAS sync tool, or another watcher, every changed generated file can become another event for a second tool to inspect.

The usual offenders are predictable:

  • node_modules/, .pnpm-store/, and package-manager caches in Node.js projects.
  • .venv/, venv/, __pycache__/, .pytest_cache/, and .mypy_cache/ in Python projects.
  • vendor/bundle/, tmp/, and log/ in Ruby projects.
  • dist/, build/, .next/cache/, .nuxt/, .turbo/, and coverage/ in frontend projects.
  • target/, .gradle/, and compiled artifacts in Rust, Java, and JVM projects.

Those folders are not bad. They are doing their job. They just do not belong in most project backups. Keep the files that let another machine rebuild the project: source, tests, docs, migrations, manifests, lockfiles, configuration templates, and scripts. Skip folders that a deterministic install or build command can recreate.

Incremental copy still needs a filter Source project src/ changed tests/ changed package-lock.json node_modules/ churn .next/cache/ churn coverage/ dist/ Compare timestamps sizes delete list excludes -n first Clean backup src/ tests/ docs/ lockfiles Changed-file detection reduces transfer work; exclusions reduce scan noise and restore risk.
An incremental copy compares source and destination, but developer projects still need exclusions so generated churn does not become backup churn.

Method 1: use rsync to copy only changed files

For a local external-drive backup, start with a source folder and destination folder that already exist. Use a trailing slash on the source when you want the contents of the folder copied into the destination folder:

mkdir -p /Volumes/DevBackup/my-app

rsync -avnih \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

On the first run, most files will appear as new. On later runs, rsync compares file size and modification time by default and transfers only files that appear different. That is usually the right tradeoff for local project backups: much faster than checksumming every file, accurate enough for normal file edits, and easy to inspect with itemized output.

If you need stronger comparison because timestamps are unreliable, add --checksum. Use it deliberately. Checksums force rsync to read file contents on both sides, which can be slower than the copy you were trying to avoid on large trees.

rsync -avnih --checksum \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

For most Mac developer workflows, fix the tree first with exclusions before reaching for checksums.

Abstract visualization of changed source files moving through a filter while dependency caches create noise
Changed-file copying solves transfer waste. Excluding generated folders solves the bigger scan-and-sync waste.

Method 2: use an exclude file for repeatable incremental backups

Once a command grows past a few flags, put the policy in a file. This makes your backup easier to review and less likely to drift between projects.

mkdir -p ~/Developer
cat > ~/Developer/.dev-backup-excludes <<'EOF'
node_modules/
.pnpm-store/
.yarn/cache/
.next/cache/
.nuxt/
.turbo/
dist/
build/
coverage/
.venv/
venv/
__pycache__/
.pytest_cache/
.mypy_cache/
vendor/bundle/
target/
.gradle/
tmp/
log/
.DS_Store
EOF

Then run the incremental copy with the same exclude file every time:

rsync -avnih --delete \
  --exclude-from="$HOME/Developer/.dev-backup-excludes" \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

When the dry run looks right, remove n:

rsync -avih --delete \
  --exclude-from="$HOME/Developer/.dev-backup-excludes" \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

Keep the exclude file boring. A long list of obvious generated directories is safer than a clever glob that accidentally skips source. If a project has a special generated folder, add it by name and leave a short comment near the rule in your team docs or backup script.

Method 3: decide whether you are updating a mirror or an archive

“Copy only changed files” can mean two different things:

Mirror

The destination should match the source after exclusions. Deleted source files should disappear from the destination. Use --delete, but only after a dry run.

Archive

The destination should keep older files even if they no longer exist in the source. Leave out --delete. Consider date-stamped folders if you need restore points.

Most working project backups should be mirrors because stale build output and old renamed files create confusion during restores. Archives are useful for deliverables, client handoff snapshots, and “freeze this exact folder before a risky refactor” moments.

If you choose mirror mode, search the dry-run output for deletions before every first run against a new destination:

rsync -avnih --delete \
  --exclude-from="$HOME/Developer/.dev-backup-excludes" \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/ | grep deleting

No output from grep deleting can be fine. A huge delete list can also be fine if you expected a cleanup. The point is to make the destructive part visible before it happens.

Method 4: avoid Finder for recurring changed-file backups

Finder is fine for a one-off drag when the folder is small and clean. It is a weak tool for recurring developer backups because the rules live in your head. There is no saved exclusion policy, no dry-run report, no scheduled job, and no clear status after the copy finishes.

macOS also includes ditto, which is useful when you need a faithful copy of a clean folder:

ditto ~/Developer/docs /Volumes/DevBackup/docs

Use ditto for straightforward Mac copies. Use rsync when you need incremental comparison, exclusions, dry runs, and delete behavior. Use a filtered GUI sync tool when the workflow becomes repetitive enough that maintaining shell commands is the annoying part.

Organized Mac developer backup workflow with source files flowing through a clean filter to a backup destination
A good recurring backup becomes boring: same source, same exclusions, same destination, visible result.

When LSyncer fits this workflow

If you already have a short rsync command you trust, keep it. The command line is still the best choice for SSH targets, CI scripts, dotfile-managed backup jobs, and edge cases where you need exact flags.

LSyncer fits the case where you want changed-file project sync without turning your backup into a private shell utility. It is a native macOS app for folder pairs, schedules, visible status, failure alerts, and developer-aware exclusions for folders such as node_modules, .git, virtual environments, build output, and caches. It works locally between folders you choose, and it is a one-time $19.99 Mac App Store purchase, not a subscription.

The useful distinction is simple: rsync is a sharp tool. LSyncer is a focused workflow. If you want to review exact flags, use rsync. If you want a saved Mac app workflow for recurring clean project sync, use LSyncer.

Best practices for copying only changed files on Mac

  • Keep active development outside cloud-synced folders. Work in ~/Developer or ~/Code, then sync a filtered copy to iCloud Drive or another destination if needed.
  • Use lockfiles as the restore contract. Back up package-lock.json, pnpm-lock.yaml, yarn.lock, poetry.lock, requirements.txt, and Gemfile.lock. Rebuild dependency folders after restore.
  • Run dry runs after changing rules. A destination path, trailing slash, exclude file, or --delete change deserves a preview.
  • Do not back up secrets by accident. Decide whether .env, certificates, local databases, and private keys belong in the destination.
  • Restore-test occasionally. Copy the backup to a scratch folder and run npm ci, pnpm install --frozen-lockfile, pip install -r requirements.txt, or the equivalent for your stack.
  • Keep logs somewhere boring. If a recurring job runs unattended, save enough output to answer: when did it last run, what changed, and did it fail?

FAQ

How do I copy only changed files on Mac?

Use rsync. Start with a dry run such as rsync -avnih source/ destination/. If the preview is correct, remove the n and run rsync -avih source/ destination/. Add --delete only when the destination should mirror the source.

Does Finder copy only changed files?

Finder can merge folders in some copy flows, but it is not a dependable incremental backup tool. It does not provide a dry run, itemized change output, reusable exclusions, or clear delete behavior. For recurring project backups, use rsync or a filtered sync app.

Should I use --checksum with rsync on Mac?

Usually no. rsync normally compares size and modification time, which is fast and appropriate for local backups. Use --checksum only when timestamps are unreliable and you accept the extra read cost on both source and destination.

Can I copy only changed files while excluding node_modules?

Yes. Add an exclusion such as --exclude 'node_modules/', or better, use --exclude-from with a reusable developer exclude file. Run with -n first and confirm that node_modules does not appear in the transfer output.

What is the safest command for a Mac project mirror?

A safe pattern is rsync -avnih --delete --exclude-from="$HOME/Developer/.dev-backup-excludes" ~/Developer/my-app/ /Volumes/DevBackup/my-app/. Review the dry run, then remove only n when the output matches your intent.