rsync command with exclude option on Mac: Safe Developer Backups

rsync command with exclude option on Mac: skip node_modules, caches, build folders, and test safe backups before copying.

Mac developer facing a tangled backup workflow before adding rsync exclude rules

rsync command with exclude option is the simplest way to copy a Mac developer project without dragging node_modules, .git, virtual environments, build output, logs, and caches into every backup. The command is not hard; the part worth getting right is the policy: which files define the project, which files can be rebuilt, and how to prove your exclude patterns match before you run a real sync.

rsync command with exclude option: the safe Mac pattern

Start with a dry run. This example mirrors a project to an external drive while skipping common generated folders:

rsync -avhn --delete \
  --exclude 'node_modules/' \
  --exclude '.git/' \
  --exclude '.venv/' \
  --exclude 'venv/' \
  --exclude 'dist/' \
  --exclude 'build/' \
  --exclude 'coverage/' \
  --exclude '.DS_Store' \
  ~/Developer/my-app/ /Volumes/DevBackup/my-app/

The flags matter. -a keeps the usual archive metadata. -v prints what is happening. -h makes sizes readable. -n means dry run, so nothing changes. Keep -n while you are checking the output. Remove only the n when the proposed copy and delete list looks exactly right.

Put every --exclude before the source and destination paths. Quote every pattern. If you write --exclude *.log without quotes, your shell may expand *.log against the current Terminal directory before rsync sees it. --exclude '*.log' leaves pattern matching to rsync, where it belongs.

Why exclude options matter for Mac developer folders

Most code projects are small at the source layer and huge at the filesystem layer. A React app might have a few megabytes of handwritten code and hundreds of megabytes of node_modules, .next/cache, coverage output, and package-manager stores. A Python project might have source files next to .venv, __pycache__, .pytest_cache, and downloaded wheels. A Rails or Ruby project might keep durable migrations beside throwaway tmp, log, and vendor/bundle.

Copying all of that has real costs on macOS:

  • Small-file overhead dominates. Thousands of tiny files require directory traversal, metadata reads, destination comparisons, and sometimes permission checks.
  • Cloud destinations inherit the mess. If the destination lives in iCloud Drive, Dropbox, Google Drive, OneDrive, or Box, the cloud client now has to index and upload files that should never have left the build machine.
  • Restores become less trustworthy. A backup full of stale dependencies can make a project appear recoverable until you try to rebuild it cleanly.
  • Deletion becomes scary. If --delete is present, a bad source/destination path or an over-broad exclude rule can remove files you did not mean to touch.

The fix is not to exclude everything that looks large. The fix is to define the source of truth. For most developer backups, that means source, tests, docs, scripts, lockfiles, project manifests, migrations, and configuration templates. It usually does not mean dependency folders, compiled output, local caches, coverage reports, or editor scratch files.

Exclude before the backup, not after the cloud client chokes Project folder src/ package-lock.json README.md node_modules/ .venv/ dist/ build/ coverage/ .DS_Store rsync filter --exclude node_modules/ .git/ dist/ build/ coverage/ Clean backup src/ tests/ lockfiles docs/ rebuildable junk skipped The destination stays small enough to inspect, restore-test, and safely hand to a cloud client.
--exclude rules run before files reach the destination, which is why they are useful for external drives and cloud-backed backup folders.

Best exclude patterns for Node, Python, and Ruby projects

Use the stack you actually run. A safe starting list for mixed Mac development looks like this:

# JavaScript / TypeScript
--exclude 'node_modules/'
--exclude '.npm/'
--exclude '.pnpm-store/'
--exclude '.yarn/cache/'
--exclude '.next/cache/'
--exclude '.nuxt/'
--exclude '.svelte-kit/'
--exclude '.turbo/'
--exclude '.vite/'
--exclude 'dist/'
--exclude 'build/'
--exclude 'coverage/'

# Python
--exclude '.venv/'
--exclude 'venv/'
--exclude '__pycache__/'
--exclude '.pytest_cache/'
--exclude '.mypy_cache/'
--exclude '.ruff_cache/'

# Ruby / Rails
--exclude 'vendor/bundle/'
--exclude '.bundle/'
--exclude 'tmp/'
--exclude 'log/'

# macOS metadata
--exclude '.DS_Store'

Review .git/ separately. Excluding .git is right for a clean working-tree backup when GitHub, GitLab, or another remote is the source of truth for history. Keeping .git is right when the destination must be a full offline clone with local branches, tags, hooks, reflogs, and unpushed commits. Many developers need both: a Git remote or bundle for repository history, and a filtered folder backup for source files and project context.

Abstract backup pipeline clogged with generated developer folders before rsync filtering
The slow part of a developer backup is often not the source code. It is the generated ecosystem around the code.

How rsync exclude matching works on Mac

rsync patterns are path rules relative to the transfer root. They are not regular expressions. Small differences change what gets skipped:

Usually what you want

  • node_modules/ skips directories named node_modules at any depth.
  • dist/ skips directories named dist, not arbitrary files with dist in the name.
  • *.log skips log files, as long as the pattern is quoted in the shell.
  • .next/cache/ skips a noisy cache without excluding all Next.js project files.

Review before using

  • /node_modules/ only matches at the transfer root, so it can miss nested workspace packages.
  • *cache* may skip real source files whose names happen to include “cache.”
  • *.env can prevent leaking secrets, but may also omit restore clues if you do not keep templates.
  • .* skips far more than macOS metadata; it can hide .gitignore, .editorconfig, and other useful project files.

The trailing slash on the source path is a separate issue from excludes, but it affects every backup. ~/Developer/my-app/ copies the contents of my-app into the destination. ~/Developer/my-app can copy the folder itself into the destination parent. Pick the layout you want and keep it identical between the dry run and the real run.

Test the command before running a real backup

For a serious backup, use itemized output during the dry run:

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

Read the output like a reviewer, not like someone waiting for a progress bar:

  1. Do src/, tests/, docs, scripts, manifests, and lockfiles appear?
  2. Are node_modules, virtual environments, build output, coverage, and caches absent?
  3. If --delete is present, are proposed deletions limited to destination files you truly want removed?
  4. Does the destination path look like the layout you intended, or did the trailing slash produce a nested folder?

If an excluded folder still appears, simplify the test. Run find ~/Developer/my-app -maxdepth 3 -name node_modules -type d to see where the directory actually lives, then match the pattern to the transfer root. If a useful file disappears, narrow the rule. Excluding too little costs time; excluding too much creates a backup that fails during restore.

Use --exclude-from when the command gets long

A few --exclude flags are fine in a one-off command. When the list becomes your backup policy, move it into a file:

cat > ~/Developer/.rsync-dev-excludes <<'EOF'
node_modules/
.git/
.venv/
venv/
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.pnpm-store/
.yarn/cache/
.next/cache/
.turbo/
dist/
build/
coverage/
.DS_Store
EOF

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

Do not write --exclude-from='~/Developer/.rsync-dev-excludes'. Single quotes prevent ~ from expanding. Use $HOME inside double quotes or write the full absolute path. Keep the exclude file in a dotfiles repo or documented backup folder so you can review changes over time.

Organized Mac developer backup workflow after rsync filters remove generated folders
A good filtered backup is boring: source files move, generated folders stay behind, and the result is small enough to restore-test.

When a GUI sync app is the better tool

rsync is excellent when you want a command you can audit, paste into a script, and run over SSH. It is less comfortable when the workflow turns into a small backup system: several source/destination pairs, per-project filters, mounted-volume checks, schedules, logs, notifications, and a way to see whether the last run actually succeeded.

Lsyncer is built for that local Mac developer workflow. It gives you filtered folder sync with developer-friendly exclusions for node_modules, .git, virtual environments, build folders, and caches, plus visible status and schedules. Use rsync when you want full command-line control. Use Lsyncer when you want the same clean-backup idea in a native app without maintaining fragile shell commands. It is a one-time $19.99 App Store purchase, not a subscription.

Best practices for clean developer backups

  • Keep active code local. Work in ~/Developer or ~/Code, then sync a filtered copy to iCloud, an external SSD, a NAS, or another destination.
  • Back up lockfiles, not installed dependencies. Dependency folders are rebuildable; lockfiles are the instructions for rebuilding them.
  • Separate backup types. Time Machine protects the whole Mac. Git protects history. A filtered folder sync protects clean project state.
  • Dry-run before destructive mirrors. Any command using --delete deserves a dry run and a quiet review.
  • Restore-test periodically. Copy the backup to a scratch folder and run npm ci, pip install -r requirements.txt, bundle install, or your stack's equivalent.

FAQ

What is the basic rsync command with exclude option?

Use rsync -avhn --exclude 'node_modules/' source/ destination/ for a dry run, then remove the n after reviewing the output. Add more quoted --exclude flags before the source and destination paths as needed.

Can I use more than one --exclude option?

Yes. Add one --exclude per pattern: --exclude 'node_modules/' --exclude '.git/' --exclude 'dist/'. If the list gets long, put the patterns in a file and use --exclude-from.

Should I exclude node_modules from Mac backups?

Usually yes. node_modules is generated from package.json and a lockfile, and it creates thousands of small files that slow local backups and cloud sync. Back up the manifests and lockfiles, then reinstall dependencies after restore.

Should I exclude .git with rsync?

Exclude .git/ when the backup is a clean working-tree copy and your Git remote preserves history. Keep .git when you need an offline clone with local branches, unpushed commits, hooks, tags, and reflogs.

Why does my rsync exclude option not work?

The most common causes are putting options after the source path, leaving wildcard patterns unquoted, anchoring a pattern to the wrong transfer root, forgetting the trailing slash for directory rules, or testing a different source path than the real command uses. Start with -avnih and inspect the itemized dry-run output.