A fast, free command-line backup tool for Mac that mirrors a folder or an entire volume onto
another drive, copies only what changed, and proves the result: every copied file is read back
from the destination and SHA-256-checked before the tool says SYNCED. A modern alternative to
rsync -a on macOS — preserves Finder tags, extended attributes, resource forks, permissions and
nanosecond timestamps on APFS — built in Swift with zero dependencies.
- Install
- Quick start
- Common use cases
- Reading the result
- All options
- Who this is for
- Details: trust model · how it works · safety rails · exclusions · isync vs rsync · FAQ · testing · roadmap · license
Requires Xcode or the Command Line Tools (xcode-select --install). Nothing else — no Homebrew,
no packages.
git clone https://github.com/arashkashi/iSyncBackup-.git
cd iSyncBackup-
PREFIX=$HOME/.local make install # → ~/.local/bin/isync, no sudo
isync --version~/.local/bin must be on your PATH (add export PATH="$HOME/.local/bin:$PATH" to ~/.zshrc if
it is not). Prefer a system-wide install? sudo make install → /usr/local/bin/isync.
To upgrade later: git pull && PREFIX=$HOME/.local make install.
Mirror a folder onto an external drive, then keep it up to date:
# 1. first copy — creates the destination folder if its parent exists
isync ~/Pictures /Volumes/BackupDrive/Pictures
# 2. later: preview what a rerun would do (changes nothing)
isync -n --delete ~/Pictures /Volumes/BackupDrive/Pictures
# 3. rerun for real. Unchanged files are not even read; only differences are copied.
isync --delete ~/Pictures /Volumes/BackupDrive/PicturesThe last line of every run is the verdict. If it does not say ✔ SYNCED, the backup is not complete, and the lines above it say why.
isync ~/Documents /Volumes/BackupDrive/DocumentsNew and changed files are copied and verified. Files you deleted from ~/Documents stay in the
backup; the summary tells you how many ("N extra items remain in the destination").
isync --delete ~/Documents /Volumes/BackupDrive/Documents--delete shows what would be removed, then does all the copying first, and only then asks:
1,022 item(s) (21.9 MB) exist only in the destination:
MyProjects/old-app/node_modules/ 612 items inside, 12.1 MB
tmp/export-2024/ 300 items inside, 8.0 MB
Misc/notes-draft.txt 14 KB
You will be asked whether to delete them once copying and verification are done.
…
Delete them? [y] yes [n] no — keep them [l] list every path (default n):
You can walk away during the copy. n (or just Enter) keeps the items and the run still completes.
--yes answers yes without asking (for scripts). It refuses to delete more than 25 % of the
destination unless you add --force.
isync -n --delete SRC DST # -n = dry run: prints every action, changes nothingisync -x --delete /Volumes/Archive /Volumes/ArchiveBackup/Archive-x stays on the source volume (does not descend into other mounted volumes). macOS housekeeping
(.Spotlight-V100, .fseventsd, .Trashes, .DS_Store, …) is skipped automatically. If you see
"Operation not permitted" on folders like ~/Library, give your terminal Full Disk Access in
System Settings → Privacy & Security.
Put a .isyncignore file in the source root (one glob per line, # comments):
.build/
node_modules/
.venv/
__pycache__/
DerivedData/
*.tmp
Name patterns match anywhere in the tree; patterns containing / are relative to the root;
a trailing / means directories only. --exclude PATTERN does the same from the command line.
On a developer's disk these few lines typically remove more than half of all files.
isync --compare hash ~/Documents /Volumes/BackupDrive/DocumentsReads every byte of every file on both sides, compares SHA-256, and repairs whatever differs. This is the only mode that catches a backup drive silently corrupting a file (bit rot). Run it now and then — it is fast on SSDs (about 300 MB/s on small files, disk-bound on large ones).
Hard drives hate per-file seeks. For the initial copy of hundreds of thousands of files onto one, copy in bulk first and let the audit read everything back sequentially afterwards:
isync -x --delete --no-fsync --no-verify SRC DST
isync -x --compare hash SRC DSTAlso keep -j 2 on a hard drive (parallel readers only add seeks), exclude caches (above), and
add the backup volume to System Settings → Spotlight → Search Privacy so indexing does not compete
for the disk.
Exit code 0 means synced; anything else means look. A cron or launchd line:
isync -q --delete --yes --report ~/backup-report.json /Volumes/Archive /Volumes/Backup/Archive \
|| osascript -e 'display notification "Backup needs attention" with title "isync"'--report writes a JSON file with every count, every error and the verdict — the auditable record.
scripts/verify-independent.sh SRC DST --hashAudits a mirror using only find, stat, readlink, xattr, shasum and diff — no isync
code at all.
| Last line | Meaning | Exit |
|---|---|---|
✔ SYNCED — … (all copies verified by SHA-256; …) |
Done. The parenthesis states exactly what was checked. | 0 |
● DRY RUN — N action(s) would be performed |
Nothing was touched. | 0 |
▲ SYNCED WITH WARNINGS — N file(s) changed while being copied |
Something was writing to the source mid-run. Run again. | 3 |
✖ NOT SYNCED — N error(s) |
Everything else was synced; the listed files were not. | 2 |
✖ Refusing: … |
A safety rail stopped it before doing anything (e.g. source drive not mounted). | 1 |
✖ INTERRUPTED |
Ctrl-C. Completed files are complete; the next run picks up the rest. | 130 |
While it runs you see the phase, a progress bar, throughput, ETA, the files each worker is on, and errors the moment they happen.
isync [options] <source> <destination>
-n, --dry-run Show what would happen; change nothing (hashes are still computed).
--delete Remove items from destination that no longer exist in source.
--force Skip the safety guard that refuses to delete >25% of the destination.
-y, --yes Delete without asking (the prompt shows the list; "no" keeps the
items and syncs the rest; no terminal counts as "no").
--compare quick|hash quick (default): size + mtime + permissions, hashing only when in doubt.
hash: SHA-256 every file on both sides (full audit; reads everything).
--no-verify Skip re-reading each copied file to confirm the bytes on disk.
--no-fsync Skip per-file fsync (faster on many small files; less crash-safe).
--exclude PATTERN Glob to skip (repeatable). Name match if no "/", else relative path.
--no-default-excludes Also copy .DS_Store, .Spotlight-V100, .fseventsd, .Trashes, … .
-x, --one-file-system Do not descend into other mounted volumes.
-j, --jobs N Parallel file workers (default: cores, max 8).
--mtime-window SEC Treat mtimes this close as equal (default: 0 APFS→APFS, 1 with HFS+,
2 FAT/exFAT/network).
--report FILE Write a JSON report of the run.
-v, --verbose Print one line per action.
-q, --quiet Only print the final verdict and errors.
--no-color Disable colors (also honours NO_COLOR).
-h, --help / --version
- You keep photos, video, music or project archives on an external drive and want a second drive that is an exact copy — with evidence, not hope.
- You back up one Mac volume to another (a Thunderbolt/USB SSD, a second internal volume) from the terminal or a scheduled job, and you want clear exit codes instead of a GUI.
- Your
rsync -ascript quietly changed behaviour. macOS now ships Apple'sopenrsync, which drops extended attributes and Finder tags unless you add-E, and truncates timestamps to whole seconds. See isync vs rsync for measurements. - You worry about bit rot / silent corruption on backup drives and want an audit mode that reads every byte on both sides and repairs what differs.
- You want a free, scriptable alternative to Carbon Copy Cloner or ChronoSync for plain folder mirroring, and you are comfortable with a command line.
Not for: two-way sync, cloud storage, remote machines over SSH (use rsync there), or
versioned history with "go back to last Tuesday" (that is Time Machine's job). Direction is always
source → destination.
The final line of every run is a verdict. Its wording states exactly what was checked.
| Mode | How a file on both sides is judged unchanged | What is proven when the run says SYNCED |
|---|---|---|
--compare quick (default) |
same size and identical mtime (nanosecond-exact on APFS→APFS) and same permissions/flags. If only the mtime differs, both sides are SHA-256 hashed before deciding. | Every file that was copied was re-read from the destination disk (page cache bypassed) and its SHA-256 matched what was read from the source. Files judged unchanged were not read — that judgement rests on size+mtime. |
--compare hash |
SHA-256 of both sides, always. | Every file on both sides was read and compared. This is the audit mode. |
--no-verify |
as quick | Copies were not re-read. Fastest; use when the destination is a trustworthy local disk and speed matters more. |
Honest limits:
- Quick mode is a heuristic. A file whose content changed while its size and mtime stayed
identical (a deliberately back-dated write, or bit rot on the destination) is invisible to it —
the same is true of
rsync, Time Machine and every other incremental tool. Run--compare hashperiodically to catch it. - Verification proves the copy reached the drive, not that the drive will still return it in five years. For that, keep more than one backup.
- Read-back bypasses the OS page cache (
F_NOCACHE), so it reads what the filesystem stored, but a drive's own write cache can still be between you and the platters/cells. A finalF_FULLFSYNCon the destination asks the drive to flush that cache before the verdict is printed.
-
Scan source and destination in parallel (
readdir+lstat, symlinks never followed). -
Plan — a pure diff producing ordered actions: removals for type changes (file↔dir↔symlink),
mkdirs, file work, deletions (children before parents), directory metadata (deepest first). Big files are scheduled first so all workers stay busy to the end. -
Execute file work on a pool of workers (
--jobs, default = cores, max 8), in two passes:- Pass 1 — copy. Stream source → temp file in the destination directory, hashing the bytes
on the way → atomic
rename()over the target. The file now has the right content but a fresh mtime and0600permissions — deliberately not yet "stamped". - Pass 2 — verify and stamp. Walk the copied files in the order they were written (sequential
on spinning disks):
fsync→ re-read bypassing the page cache → compare SHA-256 → apply permissions, flags, ACLs, xattrs (copyfile(3): Finder tags, resource forks, quarantine) → stamp the mtime that was scanned, not the source's current one.
A crash or Ctrl-C at any point leaves either the old file, the complete new file with a fresh mtime (so the next run re-checks it), or a stray
.isync-tmp-*— never a partial file under the real name, and never a file that merely looks up to date. If verification fails, the bad destination file is deleted so the next run cannot mistake it for a good copy. If the source was modified during the run, the backup keeps the timestamp of the content it actually holds, the run ends with a warning (exit 3), and the next run picks up the new version. - Pass 1 — copy. Stream source → temp file in the destination directory, hashing the bytes
on the way → atomic
-
Deletions (only with
--delete, and only after you have answered the question). -
Verdict + optional JSON report (
--report run.json) listing every error and every count.
Names are matched the way the volume matches them: on case-insensitive APFS/HFS+ (the default),
Readme.md and README.MD are the same file, so no phantom copy/delete pairs.
- Nothing is ever deleted from the destination without
--delete. --deleteshows you exactly what would be removed (whole directories collapsed to one line with their item count;llists every path) and asks after all copying and verification is done, so a slow answer never holds up the real work. "No" keeps the items and finishes the run.--yesskips the question; no terminal on stdin counts as "no".--deleterefuses to remove more than 25 % of the destination unless you pass--force— the classic "source disk wasn't mounted, backup got wiped" accident cannot happen by default.--deleteis also refused when the source scan had errors, since "missing from the source" cannot be trusted then.- Source inside destination → refused. Destination inside source → excluded from the scan.
- Errors (unreadable files, permission denied…) never abort the run; everything else is synced, every error is listed, the verdict is NOT SYNCED and the exit code is 2.
- Ctrl-C finishes the current chunk, removes temp files, and reports what was completed.
- Sockets, FIFOs and device nodes are reported as skipped, never copied.
Default: .DS_Store .Spotlight-V100 .fseventsd .Trashes .TemporaryItems .DocumentRevisions-V100
and a few more volume-housekeeping items (--no-default-excludes to keep them). The same rules
apply to both sides, so an excluded item in the destination is never deleted either.
Add your own with --exclude PATTERN or a .isyncignore file in the source root (see
Skip caches and junk). -x / --one-file-system stops at mount points.
macOS 26 ships Apple's openrsync, not the upstream rsync 3.x. Measured on the same 62,490-entry /
1.9 GB tree, APFS→APFS, Apple silicon:
openrsync -aE |
isync (fsync + read-back verify) |
|
|---|---|---|
| first copy | 37.4 s | 22.7 s |
| no-op rerun | 7.3 s | 1.5 s |
| full-content audit of both sides | 82 s (-aEc, MD4 checksums, single-threaded) |
6.0 s (--compare hash, SHA-256, 8 workers) |
| xattrs / tags / resource forks | only with -E; silently dropped with plain -a |
always |
| mtime | truncated to whole seconds | nanosecond-exact |
| copy verified by re-reading the destination | no | yes |
| atomic temp+rename | yes | yes, plus per-file fsync |
| refuses to wipe the destination | only via --max-delete |
yes, by default |
| machine-readable report | no | JSON |
The two tools' outputs were cross-checked with diff -rq and found identical. rsync remains the
right choice when the far end is a remote machine over SSH; isync is for local disks and mounted
volumes where you want certainty and speed.
isync -x --delete /Volumes/Archive /Volumes/ArchiveBackup/Archive
-x stays on the source volume, --delete makes the copy a true mirror and asks before removing
anything. Re-run the same command whenever you like; unchanged files are not even read, so a rerun
of a 60,000-file tree takes about a second and a half.
Yes, always, via copyfile(3) — the same mechanism Finder uses. Modification times are preserved
to the nanosecond on APFS. Symlinks are copied as symlinks, never followed.
It is opt-in. It lists what would go, does all the copying first, and only then asks; "no" keeps
the items and the run still completes. It refuses outright to delete more than 25 % of the
destination (the "source drive was not mounted" accident) unless you pass --force. Type changes
(a file replaced by a folder) are handled without --delete, since they are updates, not removals.
isync --compare hash reads every byte of every file on both sides and repairs any mismatch. Quick
mode — like rsync and Time Machine — trusts size + modification time for files it did not copy, so
run the hash audit periodically.
Different jobs. Time Machine keeps versioned history of your boot volume; isync keeps one drive an
exact mirror of another. CCC and ChronoSync are excellent GUI tools; isync is free, scriptable and
prints a verdict you can check in a cron job. Apple's bundled openrsync is compared
above; upstream rsync remains the right tool for remote machines.
It detects those filesystems and relaxes the comparison (2-second timestamp window, permissions not
compared) so every run does not "fix" things they cannot store. It has been tested extensively on
APFS→APFS; treat other targets as supported-but-less-tested and run --compare hash after the
first sync.
See First big copy onto a spinning hard drive:
exclude caches, bulk-copy with --no-fsync --no-verify then audit with --compare hash, use
-j 2, and keep Spotlight off the backup volume.
swift test— 32 unit tests: the planner (case folding, mtime windows, type conflicts, delete ordering, flag masking, ignore rules), the two-pass copy (verification failure removes the copy, scanned mtime is stamped even if the source changed, cancellation leaves no temp files) and the executor on real temporary trees (deletion question asked only after copying, decline keeps items, approval deletes children first).scripts/smoke-test.sh— 72 end-to-end checks on a fixture tree: xattrs, modes, exact mtimes, symlinks (relative, dangling, retargeted), unicode names, empty files/dirs, FIFOs, immutable files, same-size edits, touch-only changes, type changes in both directions, extraneous items with and without--delete, silent-corruption detection in audit mode, unreadable files, every safety refusal, destination-inside-source with--delete,.isyncignore.
make test and make smoke run them.
--trash: move deleted items into.isync/trash/<timestamp>/instead of unlinking.- Hard-link preservation (
nlink > 1is already recorded per entry). - Persistent manifest in the destination for bit-rot detection without re-reading the source.
- A SwiftUI front end over
ISyncCore(the engine is a separate library target for this reason).
MIT — see LICENSE.