rsync exclude multiple directories on Mac: Developer Backup Guide

rsync exclude multiple directories on Mac without copying node_modules, .git, venv, caches, and build output into developer backups.

Mac developer dealing with a cluttered backup workflow full of generated project folders

rsync exclude multiple directories on Mac is the command pattern you need when a project backup should copy source code, configs, docs, and lockfiles — but skip node_modules, .git, .venv, dist, build, caches, and other generated folders. The hard part is not typing one --exclude. The hard part is making a repeatable rule set that does not accidentally omit real source files or copy a million files you can rebuild.

rsync exclude multiple directories on Mac: the safe starting point

The direct form is a sequence of quoted --exclude rules before the source and destination paths:

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

Leave the n in -avhn while you are testing. It means dry run: rsync prints what it would copy or delete without changing the destination. Once the output looks right, run the same command as -avh. That one habit prevents most bad backup days.

Quote every pattern. On macOS, your shell will try to expand unquoted wildcards before rsync sees them. A rule such as --exclude *.log can behave differently depending on files in the current Terminal directory. A quoted rule such as --exclude '*.log' keeps the pattern in rsync's hands, where it belongs.

Why multiple directory excludes matter for developer projects

Most codebases are not large because the code is large. They are large because the surrounding toolchain creates deep directory trees. A Node project may have a small src directory and a huge node_modules. A Python app may keep important source next to a disposable .venv. A Rails app may have durable migrations and throwaway tmp or log folders. A frontend app may rebuild dist, .next, .turbo, or coverage at any time.

Copying those folders into a backup creates three side effects:

  • Traversal gets expensive. Even tiny files require directory walks, metadata checks, and destination comparisons.
  • Cloud destinations start doing unnecessary work. If your mirror lands inside iCloud Drive, Dropbox, Google Drive, or OneDrive, every generated file can become a sync event.
  • Restore tests get noisy. A restored project with stale dependencies can hide whether the real source, lockfiles, and setup instructions are complete.

The fix is to exclude every generated directory that is reproducible from source or tied to one machine. You still back up the files that define the project: package.json, lockfiles, pyproject.toml, Gemfile, migrations, source, tests, docs, scripts, and configuration templates.

Multiple excludes turn a noisy workspace into a clean backup Mac project folder src/ package-lock.json README.md node_modules/ .git/ dist/ coverage/ .venv/ rsync filter --exclude node_modules/ .git/ dist/ build/ coverage/ Clean backup src/ tests/ package.json lockfiles junk skipped The backup remains useful because it keeps source-of-truth files and skips folders that package managers or build tools can recreate.
Each exclude rule removes one class of generated work before it reaches the backup destination or cloud sync queue.

Best rsync exclude patterns for Mac developers

Start with the stack you actually use. Do not paste every rule on the internet into a destructive mirror. The safest common list 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/'

# Rust, JVM, and general build output
--exclude 'target/'
--exclude '.gradle/'
--exclude 'out/'

# macOS and local metadata
--exclude '.DS_Store'

Be deliberate with .git/. Excluding .git is correct when the backup is a working-tree copy and your Git remote is the place where history lives. Including .git is correct when you need an offline clone with local branches, reflogs, tags, hooks, and unpushed commits. A lot of developers need both: GitHub or GitLab for history, plus a clean folder backup for files that should survive a laptop failure.

Abstract folder sync pipeline clogged by generated directories before exclusions are applied
Long backup runs are often a symptom of unclear policy: the command is copying everything because nobody told it what is disposable.

Use an exclude file when the command gets long

Multiple --exclude flags are fine for a quick one-off. Once the list represents your real backup policy, move it into a file. That makes it reviewable, reusable, and less error-prone.

cat > ~/Developer/.rsync-dev-excludes <<'EOF'
# Dependencies and package-manager stores
node_modules/
.npm/
.pnpm-store/
.yarn/cache/

# Frontend build output and caches
.next/cache/
.nuxt/
.svelte-kit/
.turbo/
.vite/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Ruby / Rails
vendor/bundle/
.bundle/
tmp/
log/

# Other generated output
target/
.gradle/
out/

# Local metadata
.DS_Store
EOF

Then point rsync at the file:

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

Notice the double quotes around $HOME. Do not write --exclude-from='~/Developer/.rsync-dev-excludes'. Single quotes prevent ~ from expanding, and rsync will look for a literal path beginning with a tilde. Use $HOME in double quotes or an absolute path.

How rsync pattern matching works

rsync exclude patterns are path rules, not regular expressions. The small syntax choices matter:

Usually right

  • node_modules/ skips directories named node_modules at any depth.
  • .next/cache/ skips a specific noisy cache while keeping the rest of the project visible.
  • *.log skips log files when logs are not part of the source of truth.
  • dist/ skips directories, not files named dist.

Review first

  • /node_modules/ only matches at the transfer root, so it can miss workspace packages.
  • *cache* can hide real source files with “cache” in the name.
  • *.env protects secrets, but may also omit templates or local setup clues.
  • .git/ changes the backup from a clone-like copy into a working-tree copy.

A leading slash anchors the pattern to the transfer root. That can be useful if you only want to skip the top-level /build/ but keep docs/examples/build/. For monorepos, unanchored directory rules are often better because generated folders appear under apps/web/, packages/ui/, or services/api/.

Test the excludes before running a real mirror

Use a dry run with itemized output before trusting the rule set:

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

Read the output with a short checklist:

  1. Do source files, tests, docs, migrations, scripts, and lockfiles appear?
  2. Are node_modules, virtual environments, build output, and cache directories absent?
  3. If --delete is present, are proposed deletions limited to files that should be removed from the mirror?
  4. Does the destination path look right, or did a missing trailing slash create a nested folder?

The trailing slash on the source path is worth repeating. ~/Developer/my-app/ copies the contents of my-app. ~/Developer/my-app can copy the directory itself into the destination parent. Neither is universally wrong, but mixing them between dry run and real run is a classic way to produce confusing backup layouts.

Manual fixes and workarounds that do not require an app

If you only need a clean backup occasionally, rsync may be all you need. Keep a local project folder such as ~/Developer, keep generated folders out of cloud-synced active work, and run a dry-run-first mirror to an external SSD or another local folder.

For a one-time archive, you can also create a compressed copy with exclusions:

tar --exclude='node_modules' \
  --exclude='.git' \
  --exclude='.venv' \
  --exclude='dist' \
  -czf my-app-source-backup.tgz my-app

For teams, put the exclusion policy in a documented file near the backup script. A shared .rsync-dev-excludes in a dotfiles repository is easier to review than a Slack message with a command nobody can find three months later.

Clean organized Mac developer backup workflow after generated folders are filtered out
A clean mirror should feel boring: the source files move, the generated folders stay behind, and the destination stays small enough to inspect.

When Lsyncer is easier than maintaining rsync excludes

rsync is excellent when you want a script and you are comfortable reviewing command output. It is less pleasant when the script grows into a small backup product: multiple folder pairs, per-project exclusions, schedules, logs, notifications, mounted-drive checks, and a way to see whether yesterday's run actually happened.

Lsyncer is built for that local Mac workflow. It is a native folder sync app for developers, with exclusions for folders such as node_modules, .git, virtual environments, build output, and caches. Use rsync for remote SSH workflows or fully customized scripts. Use Lsyncer when you want repeatable local project sync with visible status and fewer fragile shell commands.

The product is a one-time $19.99 App Store purchase, not a subscription. The right mental model is simple: keep active projects local, keep generated folders out of sync, and copy the project files that would let you rebuild on a new machine.

Best practices for clean Mac project backups

  • Keep active development out of cloud folders. Work in ~/Developer or ~/Code, then sync a filtered copy where you want it.
  • Back up lockfiles, not installed dependencies. Lockfiles make dependencies reproducible; dependency directories make backups heavy.
  • Use dry runs whenever exclusions change. A new --exclude rule should never go straight into a destructive mirror.
  • Separate full-system backup from project sync. Time Machine is useful, but a clean project mirror solves a different problem.
  • Restore-test occasionally. A backup is only trustworthy if you can clone or copy it elsewhere and rebuild the project from source.

FAQ

How do I rsync exclude multiple directories on Mac?

Add one quoted --exclude flag per directory, before the source and destination paths. For example: --exclude 'node_modules/' --exclude '.git/' --exclude 'dist/'. If the list gets long, put the patterns in a file and use --exclude-from="$HOME/Developer/.rsync-dev-excludes".

Should I use --exclude or --exclude-from?

Use --exclude for a short one-off command. Use --exclude-from when the list is your real backup policy or when you want the same rules across multiple projects. A file is easier to review and less likely to break when pasted into a scheduled job.

Does node_modules/ exclude nested dependency folders?

Yes, without a leading slash it can match directories named node_modules at different depths in the transfer. That is usually right for workspaces and monorepos. A leading slash, as in /node_modules/, anchors the pattern to the transfer root.

Should I exclude .git from a Mac project backup?

Exclude .git if the backup is a clean working-tree copy and your Git remote is the source of truth for history. Keep .git if you need unpushed branches, reflogs, hooks, tags, or a full offline clone. The important part is deciding intentionally.

Why is my rsync backup still slow after excluding folders?

Check whether the patterns match the actual paths, run with -n and itemized output, and inspect the destination. The bottleneck may also be the destination disk, macOS permissions, Spotlight indexing, or a cloud client processing files after rsync finishes.