Skip to main content

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; mtime is restored best-effort. POSIX permissions are not preserved — restored files get non-world-readable defaults (0600).
  • pull never 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.age is 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 to rage, 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)

Source distribution for fafnir-store 0.1.0
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)

Table of built distributions (wheels) for fafnir-store 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

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