Small POSIX shell script to back up selected iCloud Drive folders with Borg on macOS.
For each folder listed in ICLOUD_BACKUP_DIRS, the backup script is designed to:
- Force-download (materialize) iCloud files
- Run
borg createto create a new archive in the configured backup repository - Evict local copies to free disk space
Downloading and evicting is done using brctl (bird CLI) by default which comes with macOS.
However, brctl download just requests the download asynchronously, so files might not be downloaded when Borg accesses them. The backup will still work but Borg seeing files as "changed during backup" while iCloud downloads them lazily. This disturbs the incremental backup functionality leading to Borg potentially backing up every file again the next time and using significantly more storage (see borg/discussions/8676).
To solve this, a custom cloud file manager like Cloud File CLI (heichel/cloudfile) can be used that offers a synchronous option. Just follow the installation instructions and set the DOWNLOAD_COMMAND env variable.
- macOS (includes
brctl) - BorgBackup
- Set up a Borg repository (see docs). A great option for affordable backup storage compatible with Borg is a Storage Box from Hetzner*
- 1Password CLI (
op) - Add item with the Borg repository password to your 1Password vault
- Standard tools:
find,sh - Optional: Cloud File CLI (heichel/cloudfile)
- Copy
.env.exampleto.env. - Fill in all required values.
Required variables:
BORG_REPO: Borg repository URL/pathOP_REFERENCE_PASSWORD: 1Password secret reference to the Borg passphraseICLOUD_BACKUP_DIRS: newline-delimited list of folder paths inside iCloud Drive
Note: While you could just set ICLOUD_BACKUP_DIRS='.' to backup the whole iCloud Drive at once, this would download every file at once and thus require you to have enough storage available on your Mac. Instead, provide individual folder names / paths so that the backup is done in chunks. The script will only download the current chunk and free up the space after backing it up. Choose the size of these chunks depending on your available disk space.
Optional variables:
ICLOUD_DRIVE_ROOT: defaults to$HOME/Library/Mobile Documents/com~apple~CloudDocsDOWNLOAD_COMMAND: command used before backup for each file; defaults tobrctl download(recommendation: usecloudfile materialize-sync)EVICT_COMMAND: command used after backup for each file; defaults tobrctl evict(recommendation: usecloudfile evict)
Example:
BORG_REPO='ssh://backup/~/iCloud-Test'
OP_REFERENCE_PASSWORD='op://Personal/Borg/credential'
ICLOUD_BACKUP_DIRS='personal
work
Personal/Tax Documents'
ICLOUD_DRIVE_ROOT="$HOME/Library/Mobile Documents/com~apple~CloudDocs"
DOWNLOAD_COMMAND="cloudfile materialize-sync"
EVICT_COMMAND="cloudfile evict"Run from the repository root:
sh backup.shThe script loads .env, reads BORG_PASSPHRASE via op read, then processes each configured folder.
ICLOUD_BACKUP_DIRSis newline-delimited so paths can include spaces.- Missing folders are skipped with an error message.
- Archive names use local timestamp format:
YYYY-MM-DDTHH-MM-SS.