Skip to main content

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 ~/.local directory (it shares the name but is not a project scratch directory);
  • .Trash and any directory named by an exclude pattern (such as node_modules or .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 confirm rclone version works in your shell.
  • "the remote ... is not reachable" - the name before the : in remote_base must match a remote from rclone listremotes. Create or rename it with rclone 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.rangler and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rangler-0.2.0.tar.gz (56.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rangler-0.2.0-py3-none-any.whl (37.0 kB view details)

Uploaded Python 3

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

Hashes for rangler-0.2.0.tar.gz
Algorithm Hash digest
SHA256 85c01bd17db975372f912e0dfe85bac72f58c7b1dba18ff8e0af1b459c7b7cf7
MD5 e907192b9e1e90ff881fd4a5958475bb
BLAKE2b-256 92f1cd33f763c60f91a688e48ca222afb69a1a1c6da0f131fe5d196012c1895d

See more details on using hashes here.

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

Hashes for rangler-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fb66e3025483cb63d8b49000f69f1af15801eaa85b54a746a195741ac2ef3d4b
MD5 d8b4c5e408ad4598162125ccf4cb228b
BLAKE2b-256 787b5d0f8d2b5b742b226bd99dec9d59e73f920a7f5d5fce572806e7d025d75b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page