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.
interval string "6h" Schedule cadence as a duration: 30m, 6h, 1d.
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 on a fixed interval and survives reboot.

rangler schedule --every 30m   # install and load the agent (defaults to config interval)
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. --every accepts a duration (30m, 6h, 1d) and defaults to the interval from the config. unschedule is idempotent: it succeeds even if nothing was installed. status reports the active interval and the time and result of the last run (read from ~/.local/state/rangler/last-run.json).

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.1.0.tar.gz (52.6 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.1.0-py3-none-any.whl (35.4 kB view details)

Uploaded Python 3

File details

Details for the file rangler-0.1.0.tar.gz.

File metadata

  • Download URL: rangler-0.1.0.tar.gz
  • Upload date:
  • Size: 52.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","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.1.0.tar.gz
Algorithm Hash digest
SHA256 0665ba7b069f52984b01214499d085f4b6b0b776dca2ffb9e17019d8cf0d63ff
MD5 4ae65376c9e410851582a76556f5abe9
BLAKE2b-256 1ffb908ea586050d8d5b41b59a53762bd7063b969cc613ff91c15a1f4c3c6308

See more details on using hashes here.

File details

Details for the file rangler-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: rangler-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 35.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 319754b94071647ebc2362de603addb617be0b02979844e1105aaa86ca22e9ca
MD5 6247f40204963d3ec4bebf3b22c173f0
BLAKE2b-256 7cee1ca7e2712c97c16c5d0b742d26fc44a9a7c8e6f626710d2437b585ba5ef5

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