The incident
It started, as most of these things do, with a .env.
I was mid-refactor, needed a key I was sure I'd written down, and the file wasn't
there. Not "stale" — gone. Deleted in a cleanup pass I did at 1am, along with a
.local.json and a couple of notes I'd been keeping next to the code.
The saving grace was a manual backup — a tarball I'd thrown onto an external disk
"just in case" maybe six months earlier. So the damage was two days of archaeology,
not two weeks. But the feeling stuck: my source code had three remotes and a
CI pipeline, and my secrets had… nothing. My own discipline.
The problem: .gitignore protects the repo, not the file
.gitignore is doing exactly the right job. Secrets must never enter the
repository history — that part of the model is correct and shouldn't be softened.
The gap is what happens outside git. A .gitignored file is invisible to every
backup mechanism you've built around git: no history, no snapshots, no offsite
copy. The file most likely to be irreplaceable — an API key you can rotate, sure,
but also the exact production config you hand-tuned — is precisely the one with
the fewest safety nets.
So the situation was: correctly excluded from version control, completely
undefended everywhere else.
The idea: .sync lists what to keep
The fix I wanted was embarrassingly simple. Take .gitignore, flip the polarity,
and you get .sync: a file at the root of a project that lists what should be
copied to backup, instead of what should be ignored by git.
name: myapp
.env # copy this
*.md # `*.md` excludes markdown in .gitignore, includes it here
!draft.md # `!` flips too: here it means exclude
Same syntax you already know — bare gitignore-style patterns, last match wins,
! to negate. The mental model transfers in about four seconds.
Two things fall out of that design for free:
-
Allow-list, not deny-list. A file is copied only if it matches a rule. No
match, no copy. You never end up quietly shipping half of
node_modulesto a backup disk. -
One config, no drift. Destinations live only in
~/.safekeep. A.synccarries rules and nothing else — adest:line there is rejected. Clone a project from a third party and it cannot redirect your backups anywhere.
Why simplicity, lightness, efficiency
Simplicity. Two files: ~/.safekeep for destinations, .sync per project.
A directory without a .sync is simply not tracked. There is no daemon to
configure per-project, no database, no server.
Lightness. The core is pure Python standard library — zero runtime
dependencies. The only external pieces are fswatch and the OS init system
(launchd on macOS, systemd on Linux). One optional extra adds an S3 (Simple
Storage Service) destination, and only if you configure s3_* keys.
Efficiency. fswatch turns filesystem events into copies: change a file,
it's backed up. Reconcile passes at startup, every 24h and on remount catch
anything a missed event dropped. Copy is atomic (temp file in the destination,
fsync, rename), so a reader sees the old file or the new one, never a torn one.
And it never deletes: remove a file at the source and it stays in the backup.
Quick start
pipx install safekeep # or: pip install safekeep
brew install fswatch # macOS; apt/dnf/pacman equivalents on Linux
~/.safekeep:
source: ~/Code # optional — omit for auto-discovery of .sync files
dest: ~/Backup/safekeep
layout: relative
~/Code/myapp/.sync:
name: myapp
.env
.env.*
*.md
!secrets/old.env
Then:
safekeep sync-once --dry-run # show what would be copied, copy nothing
safekeep run # watch + copy, foreground
safekeep daemon install # launchd / systemd unit at boot
safekeep daemon start
safekeep status # sources, projects, destination states
safekeep doctor # diagnostics
What it's for
.env files, TLS certificates, editor and deploy configs, notes and runbooks,
.vault/ directories — anything that must not live in the repo but must
survive a disk failure. Put a .sync in a project root and it's covered;
leave it out and safekeep doesn't touch the directory.
Links
- Repo: https://github.com/foxhound87/safekeep
- Docs: https://foxhound87.github.io/safekeep/
- PyPI (v0.7.0): https://pypi.org/project/safekeep/
MIT licensed, macOS and Linux.
Top comments (0)