The passwords file is just a file
Septima has been able to batch-compress for a while: stage twenty folders, flip create a separate archive for each item, and you get twenty archives instead of one big one. Adding encryption to that turns out to be a more interesting problem than adding encryption to a single archive, and not for cryptographic reasons.
One password across twenty archives is one compromise across twenty
archives. Twenty different passwords means twenty things to remember, and the
moment you write them down carelessly — a passwords.txt next to
the .7z files — you've undone the encryption you just paid for.
So the interesting question was never "which cipher." It was: where do the
passwords go, and who is responsible for them?
The integration trap
The obvious answer is to integrate. Talk to KeePassXC. Talk to Bitwarden. Write to the GNOME keyring over D-Bus.
Each of those is a separate API, a separate failure mode, and a separate thing to keep working forever. Worse, inside a Flatpak each one is a hole argued for in the sandbox — and the whole premise of this app is that it reaches your files through portals and nothing else. Pick two integrations and you've told everyone using the third that their tool doesn't count.
So Septima integrates with nothing, on purpose. It writes a passwords file and hands it to you. You put it wherever you keep secrets — a password manager, an encrypted volume, a printed page in a safe. Every vault is supported by default, because a file is the one interface they all already have.
Why it's JSON — and why CSV is still in there
The primary use of this file is a human reading it. You open it, you look at what you just made, you paste entries somewhere. Columns are hostile to that; self-describing keys are not. Each entry records the archive name, what it was made from, the password, a SHA-256, when it was written, and the cipher in plain words:
{
"archive": "tax-2024.7z",
"source": "tax-2024",
"password": "…64 characters…",
"sha256": "…",
"created": "2026-08-16T10:14:02Z",
"encryption": "7z, AES-256, encrypted headers"
}
That last field is the one that earns its place. Six months later nobody remembers whether the file names were encrypted too — so the file says so, in words, rather than making you reconstruct it from the app version.
The CSV writer that existed first didn't get deleted. It stays as an export
path, because KeePassXC and Bitwarden bulk-import CSV and not
arbitrary JSON. Moving the primary format to JSON also retired a whole edge
case: in a spreadsheet, a field beginning with =, +,
- or @ is a formula, not text. Generated passwords
are drawn from an alphanumeric set — 62 characters, no punctuation — which
keeps them clear of that, and of shell quoting, at the cost of a few bits per
character that 64 characters of length make irrelevant.
Ordering is the crash-safety story
Here's the failure that decides the whole write path: the machine dies halfway through archive eleven. If passwords were recorded as each archive finished, you now own eleven encrypted archives and ten passwords. Archive eleven is scrap — indistinguishable from random noise, permanently.
So the rule is inverted. Every password is generated and written to
disk before the first 7zz ever spawns. An archive can
never exist whose password isn't already recorded. As each job completes, its
checksum and completion time land in the file and it's rewritten — one
serialized writer, atomic temp-and-rename each time, so a crash mid-write
leaves the previous good file rather than half of a new one.
The cost is that an interrupted run leaves passwords for archives that were never created. That's the right direction to fail in: a password with no archive is litter, an archive with no password is a loss.
Protecting the file itself
A plain file full of passwords is fine while it's in transit from Septima to
your vault, and a liability the moment it isn't. So the file can be protected
with a password of its own — symmetric OpenPGP, via the gpg that's
already in the GNOME runtime:
gpg --batch --yes --quiet --symmetric --cipher-algo AES256 \
--pinentry-mode loopback --passphrase-fd 0 --no-symkey-cache \
--output - -
Symmetric only. No keys, no keyring, no reaching out to the host's
~/.gnupg. The output is an ordinary OpenPGP file that any
gpg on any machine opens with the passphrase — which matters,
because the worst possible property for a disaster-recovery file is that it can
only be opened by the app that wrote it. The passphrase and the plaintext both
travel in over stdin, so neither appears in argv, the environment,
or a temp file.
The choice happens before the batch runs, in the dialog, together with the passphrase. That's not a UI preference — it's what keeps a protected run from having a plaintext moment on disk. If the decision came afterwards, the file would have to exist in the clear first, and "we deleted it" is not the same as "it was never written." Decline protection and you get a plain-words warning about what you're leaving next to the archives, which is the honest version of a default.
The gpg-agent detour
The first protected run worked, and then the app wouldn't exit. Window
closed, process gone, flatpak run still sitting there. The cause:
gpg's first call had daemonized a gpg-agent inside the
sandbox, and bwrap waits for every process in its namespace.
The tempting fix — --no-autostart, symmetric ops don't need an
agent — was wrong, and instructively so. In GnuPG 2.4 the loopback-passphrase
machinery lives inside the agent, so agentless symmetric encryption
fails outright with "no gpg-agent running." The first test that seemed to pass
had been borrowing an ambient socket directory: luck, not design.
What it did prove is that the ordering rule works. With gpg broken, protected batches failed before writing a single archive — refusing to start rather than producing files whose passwords couldn't be saved. A feature can be broken and still fail in the right direction.
The real fix is unglamorous: let the agent start as gpg wants, then
gpgconf --kill gpg-agent after every operation, success or
failure. Nothing lingers to hold the sandbox open, and
--no-symkey-cache keeps the passphrase out of the cache the agent
briefly has.
The other half: giving the file back
A passwords file you can only read yourself is a notepad. The point is handing it back. Open one in Septima — with the Open button, by dropping it on the window, or from the command line — and instead of trying to browse it as an archive, Septima recognises it, asks for its passphrase if it's protected, and offers to extract everything it lists, each archive with its own recorded password.
Two details from building that side:
- Only basenames are trusted. The file records
tax-2024.7z, never a path, and the reader resolves it next to the file itself. A hand-edited../../evilcan't walk out of the folder — the manifest is a document, and documents from disk get treated as input, not instructions. - Portals move the goalposts. A file opened through the file chooser sits alone in its own document directory, so "next to the passwords file" can legitimately contain nothing at all. That isn't an error to shrug at; Septima says the archives weren't found and offers to let you point at the folder that really holds them.
And because "extract all twenty" is often not what you want, the picker lists every entry with its password status and the item it came from, with select-all, clear, and a filter once the list gets long.
What's next
The obvious follow-up came from the first person to use it: you're using
gpg to set a password, not to encrypt to a key. Correct. A third mode —
gpg -e -r <your key> — means nothing to remember at write
time, and it only ever touches your public key, so the sandbox stays
exactly as clean. The engineering there isn't the encryption, it's the key
picker, and the honest trade-off to put in front of you: a password-protected
file can be opened anywhere, a key-encrypted one only where the secret key
lives. Both are your call, not the app's.
Batch encryption ships in Septima v0.5.0. Install from the signed repo or read the source on GitHub.