safekeep
Selective always-on backups for macOS and Linux: watches source folders with fswatch and copies only the files you chose to their destinations — an allow-list model that never deletes anything in the backup (a file removed or renamed at the source stays in the backup).
Documentation: https://foxhound87.github.io/safekeep/
Features
- Allow-list: a file is copied only if it matches at least one
include:; no match → not copied. Unmatched directories are still traversed, while directories with an excluded verdict are pruned together with their subtree. - Atomic copy, never a half-written file: the temp file lives in the
destination's own directory, followed by
fsync+os.replace→ a reader sees either the old file or the new one, never a partial copy. - Reconcile at startup, every 24h, on remount, and on
sync-once: copies whatever differs by size/mtime, idempotently — lost events, crashes and reboots don't matter, the next reconcile brings everything back in sync. - Unmounted volumes: missing destination →
pendingstate with exponential backoff (1s → 60s cap), then a reconcile once the mount is back. - launchd (macOS) or systemd (Linux) at boot: an agent with
RunAtLoad+KeepAlive(orWantedBy=default.target+Restart=always) keeps the process alive and restarts it if it dies. .synccarries rules only, destinations live only in~/.safekeep: a.syncfile can neither add nor remove destinations (adest:line is an invalid line), so a repo cloned from a third party can't redirect the backup somewhere else.
Installation
pipx install safekeep # recommended for a CLI
# or
pip install safekeep
Requirements: macOS or Linux (POSIX with systemd), Python >= 3.9 and fswatch:
brew install fswatch # macOS
sudo pacman -S fswatch # Arch / Omarchy
sudo apt install fswatch # Debian / Ubuntu
sudo dnf install fswatch # Fedora
The agent (daemon at boot: launchd on macOS, a systemd user unit on Linux)
is installed from a checkout of this repository with ./install.sh — not from
the wheel — see Agent below.
Agent
git clone https://github.com/foxhound87/safekeep.git
cd safekeep
# only external dependency, if not installed yet (see the per-OS list above)
brew install fswatch # macOS — the script suggests the right command on Linux
# copies the example into ~/.safekeep, renders the plist / systemd unit,
# runs preflight checks
bash install.sh
Then:
- Fill
~/.safekeepwith your realdest:entries — and, optionally,source:(the example ships with placeholders — seeexamples/safekeep.example); - drop a
.syncfile in the root of every project you want to follow (seeexamples/sync.example); a directory without a.syncis not tracked; - load the agent:
# macOS (launchd)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.safekeep.agent.plist
# unload with: launchctl bootout gui/$(id -u)/com.safekeep.agent
# Linux (systemd user unit)
systemctl --user enable --now safekeep
# status with: systemctl --user status safekeep
# logs with: journalctl --user -u safekeep -f
# unload with: systemctl --user disable --now safekeep
# start at boot without a login session: loginctl enable-linger $USER
On macOS, once you have granted TCC/FDA (Full Disk Access) permissions to the
Python interpreter and to fswatch, run safekeep doctor for diagnostics. On
Linux the same command checks the inotify watch limit instead.
Quickstart
~/.safekeep (the only place where destinations live):
# observed roots: only directories containing a .sync are followed
source: ~/Code
source: ~/Projects
# destination for ALL projects
dest: ~/Backup/safekeep
# relative (default): <dest>/<path relative to the source root>
# ~/Code/myapp/docs/a.md → ~/Backup/safekeep/myapp/docs/a.md
layout: relative
log_level: info
source: is optional. With it present (source mode) the watched roots are
exactly those entries, as always. Without it safekeep switches to
auto-discovery: it scans $HOME for .sync files (skipping hidden
directories, Library, .Trash, .cache, node_modules, .git, .venv,
__pycache__, venv), watches $HOME, and picks up any .sync created after
startup. dest: stays mandatory in both modes.
~/Code/myapp/.sync (rules only, no destinations):
name: myapp
# allow-list semantics: without these lines NOT A SINGLE file would be copied
*.md
.env
!secrets/old.env # ! = exclude
Rules can be written as bare gitignore-style lines — the inverse of gitignore:
a line lists what to copy, not what to ignore (*.md includes markdown here,
excludes it in a .gitignore), and a leading ! turns it into an exclude:.
Bare lines share the same ordered list as the explicit include:/exclude:
keys, so last-match-wins works the same way (see
examples/sync.example).
Dry run first, then start the daemon:
safekeep sync-once --dry-run # prints what would be copied, copies nothing
safekeep run # daemon: watch + copy (foreground)
CLI commands
safekeep <command> [--config PATH] [-v]
| Command | What it does |
|---|---|
run |
daemon: initial reconcile, fswatch loop, event dispatch, 24h timer |
sync-once [--dry-run] [--project PATH] [--prune] |
a single pass: walks the source and copies whatever differs (--dry-run only prints what it would copy; --prune also removes dest files whose source still exists but is no longer included) |
status |
read-only: config, sources, discovered projects with N rules, destination states |
doctor |
diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails |
Tests
python3 -m unittest discover -s tests
Stdlib (unittest) suite, zero dependencies: 175 tests covering the matcher,
config, atomic copy, volumes, daemon, auto-discovery, CLI and the POSIX
portability layer (platform helper, per-OS fswatch monitor, systemd template).
fswatch is not needed to run the suite — the few doctor checks that talk to
the real binary are skipped when it's missing — but CI installs it so the
Linux job exercises the real inotify monitor.
Documentation
Everything in detail (config format, pattern semantics, event → sync flow,
atomic copy, launchd, edge cases) lives in SPEC.md; commented
examples in examples/.
License
MIT — see LICENSE
Metadata
Release files for safekeep 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| safekeep-0.3.0.tar.gz | 54.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| safekeep-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.2 kB
Release files / safekeep-0.3.0.tar.gz
| Download URL | safekeep-0.3.0.tar.gz |
|---|---|
| Size | 54.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a9aedac4e65f95535bf222c1fea13c3897559ad7b3b76f99119c27da7a9a399
|
|
BLAKE2b-256 checksum How to use checksums |
84d540772f95893376c5b7a22914f2859541d8891d83bca2348853de13a0a1ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Release files / safekeep-0.3.0-py3-none-any.whl
| Download URL | safekeep-0.3.0-py3-none-any.whl |
|---|---|
| Size | 33.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bddc44f9e4eeaee1841462e52f36c6bfc6ac730812b4826ec3ba06fe72786fcf
|
|
BLAKE2b-256 checksum How to use checksums |
075ec813dccce6f8a307bdeb7c9e4d8ea38256264ec5af40b46102b81d1a70eb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|