Skip to main content

safekeep

PyPI Python

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 → pending state 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 (or WantedBy=default.target + Restart=always) keeps the process alive and restarts it if it dies.
  • .sync carries rules only, destinations live only in ~/.safekeep: a .sync file can neither add nor remove destinations (a dest: 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:

  1. Fill ~/.safekeep with your real dest: entries — and, optionally, source: (the example ships with placeholders — see examples/safekeep.example);
  2. drop a .sync file in the root of every project you want to follow (see examples/sync.example); a directory without a .sync is not tracked;
  3. 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.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for safekeep 0.3.1
File Size Uploaded
safekeep-0.3.1.tar.gz 57.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for safekeep 0.3.1
File Interpreter ABI Platform
safekeep-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 91.9 kB

Release files / safekeep-0.3.1.tar.gz

Download URL safekeep-0.3.1.tar.gz
Size 57.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9b5c66898443c59991b17310ac9571793cde6b3c243a882d45a9c7b360d5fae6
BLAKE2b-256 checksum
How to use checksums
8fcbdbbf34a3c5fdfd680526445b015a69c7704a4a6508c84596dc8977360f23
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.1-py3-none-any.whl

Download URL safekeep-0.3.1-py3-none-any.whl
Size 34.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
33bcb77bb0acbe19d38a0ad821a269fb3db28dc62a43d4e64a2b0f6d43569593
BLAKE2b-256 checksum
How to use checksums
29abd9c8c40227714e8bb192a78289d029367af664df0c3c5cc4d77c741dbb7c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

0.5.0

2 release files

0.4.0

2 release files

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page