Fafnir
A small CLI that stores your administrative documents in the cloud, using a git repository as an encrypted, portable source of truth.
Your documents stay where they are — ~/Documents/admin, /Volumes/scans, wherever. Fafnir
encrypts them into an opaque git repo it owns, and pushes that repo to a private GitHub repo (or
any git remote). The remote never sees a filename, a folder name, or a byte of content.
- Source of truth — one command to store every change, one to restore everything.
- Nothing readable in the cloud — hash-named age blobs plus an encrypted index. Real paths exist only inside that index.
- No lock-in, including on fafnir — blobs are standard age files:
age -d -i <identity>gets your documents back without fafnir ever being installed. - Git-like —
push,pull, ignore rules, a single commit per change set.
How it works
Fafnir keeps two filesystem worlds apart:
source folders (plaintext) fafnir home (encrypted git repo)
~/Documents/admin/*.pdf ──push──▶ blobs/<id>.age + index.age
/Volumes/scans/*.pdf ◀──pull── (git commit / push / pull to the remote)
- push — scan the source folders, diff them against the index, encrypt what changed into blobs, rewrite the index, commit once, push.
- pull — pull the repo, decrypt blobs, write the plaintext back into the source folders.
Everything fafnir owns lives in one directory ($XDG_CONFIG_HOME/fafnir, else ~/.config/fafnir),
so the CLI works from any working directory.
Encryption is an age keypair: every blob is encrypted to the public key, and the private
identity is itself protected by your passphrase and committed as identity.age. That single
passphrase-protected file is all you need to recover the store from scratch. Blob names are a
keyed fingerprint — HMAC-SHA256(key, content) — so identical files deduplicate, changes are
cheap to detect, and nobody with repo access can confirm that a guessed document is in there.
Setup
Requires Python 3.11+ and git, on Linux or macOS.
pip install fafnir-store
Or run from a checkout with uv:
uv sync
uv run fafnir --help
Create the store, set the passphrase (asked once, no recovery if lost), and track a folder:
fafnir init
fafnir config add ~/Documents/admin
fafnir config set remote git@github.com:you/admin-store.git # a PRIVATE repo
fafnir push
Usage
fafnir push # store every local change (shows a recap, asks to confirm)
fafnir push -f # no confirmation
fafnir pull # restore documents from the store
fafnir verify # check the store decrypts, matches the index, and spot drift
push prints a plain-language recap before doing anything:
Changes to push:
added admin/tax/2025/notice.pdf
modified admin/insurance/policy.pdf
deleted admin/old/scan.pdf
3 change(s), 1.4 MB to encrypt.
Encrypt and commit these changes? [y/N]
Roots
Each tracked top-level folder is a named root. The index stores (root, relative path) and
never an absolute path, which is what keeps the store portable across machines and mount points.
fafnir config add ~/Documents/admin # root name defaults to the folder name
fafnir config add /Volumes/scans --name scans
fafnir config rm scans # stops tracking; stored files are kept
fafnir config list
If a root's folder is missing (an unplugged drive), fafnir says so and leaves its files alone — an unavailable root is never mistaken for a mass deletion.
Ignore rules
<home>/ignore uses gitignore syntax and applies to every root:
.DS_Store
*.tmp
drafts/
Sessions
The passphrase is asked once per session (idle timeout: 15 min), then the unlocked identity is
cached in <home>/session (mode 0600).
fafnir unlock # start a session now
fafnir lock # end it
echo -n "$PASSPHRASE" | fafnir unlock # non-interactive (scripts, cron)
Recovery on a new machine
This is the flow that makes fafnir a real backup:
fafnir clone git@github.com:you/admin-store.git # asks for the passphrase, rebinds roots
fafnir pull # decrypts everything back to disk
clone lists the roots the store knows about and asks where each one lives on this machine
(--root name=/path does it non-interactively).
Recovery without fafnir
Blobs are ordinary age files. With the passphrase and a checkout of the repo:
age -d repo/identity.age > identity.txt # your age identity
age -d -i identity.txt repo/index.age # the index: paths, fingerprints, sizes
age -d -i identity.txt repo/blobs/<id>.age > document.pdf
Config
fafnir config list
fafnir config set session-timeout 0 # 0 = never time out
fafnir config set max-file-size 52428800
| Config key | Default | Description |
|---|---|---|
remote |
(unset) | Git remote URL for push/pull |
branch |
main |
Branch to push to and pull from |
session-timeout |
900 |
Session idle timeout in seconds (0 = no timeout) |
max-file-size |
99614720 (95 MB) |
A single file above this is refused, before git can choke on it |
Config lives at <home>/config.toml. It is local to the machine (it binds root names to
absolute paths) and is never committed.
What fafnir stores
- Regular files only. Symlinks and special files are skipped; empty directories are dropped (like git) and recreated from file paths on restore.
- Content and logical path are preserved;
mtimeis restored best-effort. POSIX permissions are not preserved — restored files get non-world-readable defaults (0600). pullnever deletes: files that exist only on disk are reported and left alone.
Security notes
- The remote is untrusted for confidentiality: it only ever sees encrypted blobs. It can observe how many blobs exist, their sizes, and commit times — accepted, on a private repo.
- The passphrase is set once at
fafnir init. There is no recovery. identity.ageis committed on purpose: it is passphrase-protected (age scrypt mode, work factor 2²⁰) and makes the repo self-contained for disaster recovery.- Decrypted documents live in plaintext in your source folders by design — that is what they are. Encryption applies to the cloud side.
- Crypto is age via
pyrage(bindings torage, the Rust implementation). Fafnir writes no cryptography of its own.
Releasing
Build and publish instructions live in publish.md.
Metadata
Release files for fafnir-store 0.1.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 | Provenance | Actions |
|---|---|---|---|---|
| fafnir_store-0.1.0.tar.gz | 38.7 kB | Without provenance | Details for fafnir_store-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| fafnir_store-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for fafnir_store-0.1.0-py3-none-any.whl |
Total release size: 65.8 kB
Release files / fafnir_store-0.1.0.tar.gz
| Download URL | fafnir_store-0.1.0.tar.gz |
|---|---|
| Size | 38.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1c3fc05fb3fdbebfba9922b6ac9880c63bd3f8245a89a852c86319a71c5beb70
|
|
BLAKE2b-256 checksum How to use checksums |
2614e4f7b8abdc68d4f6a6ac89b66259eb0a78574ad421d189817ef102e6b3f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / fafnir_store-0.1.0-py3-none-any.whl
| Download URL | fafnir_store-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9604f6506c52cd861be5278bba183f69f2dfc92a3446440b9eca083012ef259d
|
|
BLAKE2b-256 checksum How to use checksums |
2bb84534ba96feca4d7cac9658dbc82920b37cec915550bb2de4c6235b0960b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for fafnir-store 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.