macOS home-directory backup to an rclone remote, with versioning and scheduling.
Project description
rangler
rangler is a macOS command-line tool that backs up a single user's home
directory to an rclone remote. It mirrors an explicit
list of configured paths (under files/) and auto-discovered project .local
directories (under locals/), namespaces everything by hostname, keeps prior
versions of changed and deleted files, and can run unattended on a schedule.
Backups are one-way (local to remote). rangler never writes to your local
filesystem except its own configuration, logs, runtime state, and its
LaunchAgent. It shells out to the rclone binary you have installed; it does not
install or configure rclone, and it adds no encryption of its own (use an rclone
crypt remote if you want that).
Requirements
- macOS, single user.
- rclone installed and at least one remote configured.
- Python 3.13+ and uv for installation from source.
Installation
brew install rclone
rclone config # create a remote, e.g. a `local` remote named `backup`
uv tool install --editable . # or run ad hoc with: uv run rangler ...
Configuration
rangler reads a single TOML file at ~/.config/rangler/config.toml. Scaffold a
starter file with rangler init, then edit it. Every path must live under your
home directory.
| Key | Type | Default | Notes |
|---|---|---|---|
remote_base |
string | (required) | rclone remote in name:path form, e.g. backup:rangler. |
paths |
array of string | [] |
Explicit paths under $HOME to back up. ~ is expanded. |
scan_roots |
array of string | ["~/Code"] |
Roots scanned for .local directories. |
max_depth |
integer | 4 |
Maximum descent depth under each scan root. |
excludes |
array of string | see below | rclone filter patterns applied to every target. |
time |
string or time | "12:00" |
Daily local time backups run, as "HH:MM" (24-hour). |
notify |
string | "failure" |
Desktop notifications: failure, always, or never. |
Default excludes: node_modules/**, .git/**, .DS_Store, *.log,
.cache/**, __pycache__/**. At least one of paths or scan_roots must be
non-empty. Unknown keys are reported as warnings (typo protection) rather than
aborting.
Usage
rangler init
Scaffold a starter config.toml. Does nothing if one already exists unless you
pass --force to overwrite it.
rangler init
rangler check
Validate the configuration, confirm rclone is present and the remote is
reachable, and print the full resolved target list (each source and its computed
destination) without transferring anything. Exits 0 when valid, 2 when the
configuration is invalid, and 3 when rclone is missing or the remote is
unreachable.
rangler check
rangler run
Back up every resolved target. Each explicit path is mirrored to
<remote_base>/<hostname>/files/<path-relative-to-home>. Symlinks are followed
so symlinked dotfiles are stored as content. Prior copies of changed or deleted
files are moved into <remote_base>/<hostname>/_versions/<run-timestamp>/...
rather than being hard-deleted. Each run prints per-target progress and a final
summary, and writes a timestamped log to ~/Library/Logs/rangler/.
rangler run # perform the backup
rangler run --dry-run # report intended changes without writing to the remote
A configured path that does not exist on this machine is reported as skipped and
does not fail the run (paths legitimately differ across machines). A path that
exists but errors during transfer is recorded as a failed target; the run
continues with the others and exits non-zero. Overlapping runs are prevented by a
lock: a second concurrent run exits cleanly with skipped: already running.
Auto-discovery of .local directories
Beyond the explicit paths list, rangler scans each entry in scan_roots
(default ~/Code) for project .local directories down to max_depth (default
4). Each discovered <root>/.../project/.local is mirrored to
<remote_base>/<hostname>/locals/<project-path-relative-to-home> - the .local
segment itself is dropped. For example, ~/Code/pathling/.local is backed up to
locals/Code/pathling.
Discovery deliberately skips:
- the XDG
~/.localdirectory (it shares the name but is not a project scratch directory); .Trashand any directory named by an exclude pattern (such asnode_modulesor.git), which are pruned without being descended into.
If the same directory is both discovered and listed explicitly in paths, it is
backed up once, with the explicit files/ mapping taking precedence. Run
rangler check to preview the full resolved target list - explicit and
discovered alike - before backing up.
Scheduling (schedule / unschedule / status)
rangler can run unattended via a macOS LaunchAgent that fires once a day at a fixed local time and survives reboot.
rangler schedule --at 02:00 # install and load the agent (defaults to config time)
rangler status # report whether scheduling is active and the last run
rangler unschedule # unload and remove the agent
schedule writes a LaunchAgent plist to
~/Library/LaunchAgents/au.id.grimes.john.rangler.plist and loads it with launchctl bootstrap; re-running it replaces any existing agent. --at accepts a 24-hour
HH:MM local time and defaults to the time from the config (12:00 if unset).
Because launchd starts agents with a minimal PATH that omits the Homebrew
directories, the plist sets an EnvironmentVariables PATH covering both
Homebrew prefixes (/opt/homebrew/bin and /usr/local/bin) so the scheduled
run can locate rclone.
The agent uses launchd StartCalendarInterval, so the schedule is anchored to a
wall-clock time rather than a relative countdown: reinstalling the agent never
delays the next run, and if the machine is asleep or off at the scheduled time
launchd runs the missed backup on the next wake. unschedule is idempotent: it
succeeds even if nothing was installed. status reports the scheduled daily time
and the time and result of the last run (read from
~/.local/state/rangler/last-run.json).
Breaking change: earlier versions scheduled on a repeating interval via an
interval config key and a rangler schedule --every <duration> option. Both
have been removed. A stale interval key is now reported as an unknown key
(warning) and ignored, and the daily time default applies. An agent installed
by an older version keeps running on its old interval until you next run rangler schedule, which migrates it to the daily-time agent; status flags such a
legacy agent and points you at the reschedule.
You can confirm the loaded job directly with launchctl:
launchctl print gui/$(id -u)/au.id.grimes.john.rangler
Notifications
For unattended runs, rangler can raise a macOS desktop notification at run
completion. The notify config key controls when:
failure(default): notify only when one or more targets failed.always: notify on every completed run, success or failure.never: stay silent.
Notifications fire for real runs (manual or scheduled); a --dry-run preview is
always silent.
Remote layout
Given remote_base = "backup:rangler" and hostname mymac:
backup:rangler/
└── mymac/ # Hostname segment.
├── files/ # Explicit configured paths.
│ ├── .ssh/... # from ~/.ssh
│ └── Documents/notes/... # from ~/Documents/notes
├── locals/ # Auto-discovered .local directories.
│ └── Code/pathling/... # from ~/Code/pathling/.local
└── _versions/ # Versioned overwrites/deletes.
└── 2026-06-19T03-21-55Z/ # One sub-tree per run that changed something.
├── files/...
└── locals/...
This layout is a stable contract that manual restore relies on. rangler only
ever writes under <remote_base>/<hostname>/, so other machines' trees are never
touched.
Restore (manual)
Restore is performed manually with rclone against the layout above. For example:
rclone copy backup:rangler/mymac/files/.ssh ~/.ssh
rclone copy backup:rangler/mymac/locals/Code/pathling ~/Code/pathling/.local
Earlier versions of a file are found under
backup:rangler/mymac/_versions/<timestamp>/....
Troubleshooting
- "rclone is not installed or not on your PATH" - install rclone
(
brew install rclone) and confirmrclone versionworks in your shell. - "the remote ... is not reachable" - the name before the
:inremote_basemust match a remote fromrclone listremotes. Create or rename it withrclone config. - A configured path is reported as skipped - the path does not exist on this machine. This is expected when paths differ across machines; it does not fail the run.
- A target is reported as failed - check the per-run log under
~/Library/Logs/rangler/for the rclone error (for example a permission problem). The run still backs up every other target and exits non-zero. - "skipped: already running" - another rangler run holds the lock at
~/.local/state/rangler/rangler.lock. Wait for it to finish; the lock is released automatically even if that process is killed. - Scheduled runs are not happening - confirm the agent is loaded with
launchctl print gui/$(id -u)/au.id.grimes.john.ranglerand inspect~/Library/Logs/rangler/launchd.err.log.
Licence
rangler is licensed under the Apache License, Version 2.0. See the NOTICE file for attribution.
Copyright © 2026 John Grimes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rangler-0.2.0.tar.gz.
File metadata
- Download URL: rangler-0.2.0.tar.gz
- Upload date:
- Size: 56.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85c01bd17db975372f912e0dfe85bac72f58c7b1dba18ff8e0af1b459c7b7cf7
|
|
| MD5 |
e907192b9e1e90ff881fd4a5958475bb
|
|
| BLAKE2b-256 |
92f1cd33f763c60f91a688e48ca222afb69a1a1c6da0f131fe5d196012c1895d
|
File details
Details for the file rangler-0.2.0-py3-none-any.whl.
File metadata
- Download URL: rangler-0.2.0-py3-none-any.whl
- Upload date:
- Size: 37.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb66e3025483cb63d8b49000f69f1af15801eaa85b54a746a195741ac2ef3d4b
|
|
| MD5 |
d8b4c5e408ad4598162125ccf4cb228b
|
|
| BLAKE2b-256 |
787b5d0f8d2b5b742b226bd99dec9d59e73f920a7f5d5fce572806e7d025d75b
|