Automated borgmatic backups on MacOS with Full Disk Access
Borg offers a powerful backup solution with deduplication, compression, and encryption. Combined with borgmatic, it provides user-friendly features like alerting through healthchecks.io. However, Apple's macOS privacy framework (Transparency Consent and Control, or TCC) complicates backing up protected folders like Documents, Mail, and Photos.
MacOS Privacy Settings
The Problem
macOS treats borgmatic as a generic Python process, prompting full disk access permissions for Python itself. Since granting wide-ranging permissions to every Python script poses security risks, this is far from ideal.
Here's how I've navigated this challenge effectively.
The Solution
My solution involves:
- Creating a simple Swift-based command line wrapper for borgmatic
- Running the wrapper via a LaunchDaemon to ensure proper permissions
- Escalating file permission warnings to errors for reliable monitoring
1. Creating the Swift Wrapper
To securely backup folders outside your home directory, run borgmatic with sudo
. Assuming you've installed borgmatic via Homebrew (which borgmatic
will verify your path), here's a simple Swift wrapper (main.swift
):
import Foundation
// Create a Process to run the command
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
process.arguments = ["/opt/homebrew/bin/borgmatic", "-v", "1"]
// Pipe for reading the output
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
do {
// Run the process
try process.run()
process.waitUntilExit()
// Capture and print the output
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
print(output)
}
} catch {
print("Error: \(error)")
}
Compile it:
swiftc main.swift -o borgmaticWrapper
Then place the resulting executable (borgmaticWrapper
) into /usr/local/bin
and grant it "Full Disk Access" via System Settings > Privacy & Security.
borgmaticWrapper with Full Disk Access
2. Using LaunchDaemon for Reliable Permissions
Running the wrapper via LaunchDaemon ensures superuser privileges and consistent access to specially protected macOS resources, such as Photos. Here's the configuration (/Library/LaunchDaemons/com.borgmatic.backup.plist
):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UserName</key>
<string>root</string>
<key>GroupName</key>
<string>wheel</string>
<key>Label</key>
<string>com.borgmatic.backup</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/borgmaticWrapper</string>
<string>-v</string>
<string>1</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>20</integer>
<key>Minute</key>
<integer>15</integer>
</dict>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/borgmatic_backup.log</string>
<key>StandardErrorPath</key>
<string>/var/log/borgmatic_backup_error.log</string>
<key>KeepAlive</key>
<dict>
<key>AfterInitialDemand</key>
<true/>
</dict>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/sbin:/opt/homebrew/bin</string>
</dict>
</dict>
</plist>
My LaunchDaemon runs every day at 8:15pm and outputs logs. It is important to include your PATH Environment, as borgmatic/borg will otherwise fail.
Activate it:
sudo launchctl load /Library/LaunchDaemons/com.borgmatic.backup.plist
The LaunchDaemon will immediately start your first backup because we set RunAtLoad
to true
.
3. Escalate permission problems
borg and borgmatic treat file permission issues as warnings rather than errors. I'd like to escalate via healthchecks.io if something goes wrong, especially given how opaque Apples TCC is and how it has evolved. Thankfully, borgmatic allows customization of warnings and errors, and this example with exit code 105 is exactly what we need. Just add the following line to your config.yaml and sleep well because you know any upcoming issues won't fail silently.
borg_exit_codes:
# The exit code for file access errors
- code: 105
treat_as: error
I back up my Mail folder and have observed aforementioned access problems on one file (thank you, exit code 105!), namely recentSearches.plist
of my MailData. I do not care enough about my recent searches to worry about backing it up, I just include it from my backup, by adding the following to my config.yaml:
patterns:
- '- Library/Mail/*/MailData/recentSearches.plist'
And that's it! In one of the next posts, I will explain how to create a small menubar backup observer with Swiftbar.