Zecret
A modern, encrypted terminal diary.
One entry a day, in your terminal, kept in a single encrypted file that only you can open.
zecret.krfu.dev · Install · How it works · Security · Changelog
Why
A diary should be private by construction, not by promise. Zecret has no server, no account, no sync and no telemetry — there is nowhere for your writing to go. It is one encrypted file on your own disk, opened by a password only you know, in a terminal you already have open.
- 📅 One entry a day — each day is a page, named by its date; come back and it is the same page
- 🗓️ Grouped by month — the diary reads as months, each headed with how much of it you wrote
- 📆 The year at a glance — a calendar view lays out the whole year with the days you have written marked, so the gaps show
- 🔐 Encrypted at rest — Argon2id key derivation, AES-256-GCM per entry
- ✍️ Keyboard-driven — a fast Textual TUI, no mouse required
- 🔎 Instant search — live filtering across everything you have written
- 📄 One portable file — back it up by copying it; it is useless without your password
- 🌗 Light and dark — eight themes, picked in settings and remembered
- 🔒 Locks itself — walks away when you do, and asks for your password again
- ⬛ Covers the page — one key bars every word but the one under your cursor, for writing in public
- ✳️ Emphasis as you type — a phrase between *asterisks* is picked out in colour and bold, and stored exactly as typed
- 🚫 Offline by design — no networking of any kind
Install
uv tool install zecret
zecret
That is the whole of it: uv fetches a
suitable Python along with the program, so nothing depends on what your
system happens to ship. If you would rather use what you already have,
pipx install zecret and pip install zecret both work on Python 3.13 or
newer. To try it without installing anything at all, uvx zecret --path /tmp/try.enc runs it straight from PyPI against a diary you can delete
afterwards.
Your diary lives at ~/.zecret/diary.enc by default. Point somewhere else
with --path /some/where.enc or the ZECRET_DIARY_PATH environment
variable — handy for keeping separate diaries, or trying it out without
touching your real one.
Your settings follow you rather than the file, so a diary opened with
--path still uses your theme. If you would rather it did not — running a
build you are working on, say — --config /some/where.json or
ZECRET_CONFIG_PATH gives that run preferences of its own.
zecret --help lists both flags and both environment variables, and
zecret --version says which Zecret you have without opening the diary.
On first launch there is no diary yet, so Zecret asks you to choose a master password and creates one. Every launch after that asks for that password to unlock it.
Zecret says as much on the screen that asks you to choose one, and again in the dialog that changes it later — in red, where it cannot be mistaken for small print.
It also rates the password as you type it, on both screens where one is chosen:
▰▱▱▱ Weak — this is a very common password.
▰▰▰▰ Strong
Your diary file is guessed at offline if it is ever taken, with nothing to slow an attacker down but the password itself, so the rating is the one piece of feedback worth having. It is a reading and not a rule: Zecret will not refuse you your own diary over it.
The rating comes from zxcvbn, which
scores against lists of real passwords, words, names and keyboard runs
rather than counting characters — because counting characters calls
Password1234 strong, and it is one of the first things anyone guesses.
Using it
Press n to write about today, ctrl+s to save. Saving leaves you in the day with the cursor where you left it, so you can keep going and save again; esc is what goes back, and if you have not saved it offers to before it does. A day holds one entry, so pressing n again later opens what you already wrote rather than starting a second page — the evening simply continues the morning. Every save re-writes the diary file atomically, so there is no draft state to lose track of.
Put a phrase between asterisks — the *only* part I meant to remember —
and it takes a colour of its own, and the weight to go with it, as you
close it. Nothing else is interpreted: there are no headings, no lists and
no italics, because a diary is prose and not a document. The colour is
drawn on its way to the screen and never written down, so the file keeps
the asterisks exactly where you typed them, and a line like 2 * 3 — or
3*4 packs — stays arithmetic.
The list shows what you wrote; press c to see when. The calendar lays out the whole year, every day you have written marked, so a missed week is a hole in a month and a missed month is a hole in the year. The arrow keys move to the day drawn in that direction — across into the month beside, or down into the month below, stepping over the blank days around a month — the page keys move a month, and [/] jump a year either way. Press e (or enter) on any day up to today to write it or carry on with it, d to delete a written one (it asks first, as the list does), and c or esc to go back to the list, which picks up where the calendar left off. It fits three months across an 80-column terminal and spreads out to six on a wide one.
When you already know the date, a is quicker: type it, and enter opens the day. Under the field is the month it falls in, with every day you have already written marked — so the evenings you missed are the ones without a mark. tab (or ↓) steps into it, the arrow keys walk a day at a time, the page keys move a month, and enter takes the day the cursor is on. Type instead and the month appears as you type it, before you have finished the date. Anything up to today is fair game, and days that have not happened yet are neither offered nor accepted.
Press / to search. Your entries are already decrypted in memory for the session, so filtering is instant and nothing touches the disk.
Press ? for every key in one popup, and s for settings — where you pick a theme, choose how long the diary waits before locking itself, and change your master password.
That last one is a dialog of its own, behind a button. It is the only thing
in the app that cannot be undone, so it is not something to stumble into
between two dropdowns — and the warning is the first thing on it rather
than the last line of a form. Your current password is checked before
anything happens, and a change that fails to save leaves the diary openable
by the password you already had. Those preferences are kept in
~/.zecret/config.json, the one file Zecret writes unencrypted; it holds
your settings and nothing about what you wrote.
Zecret locks itself after fifteen minutes without a keystroke, and asks for your password again — press ctrl+l to do it yourself on the way out of the room. Suspending the machine counts as being away, so a laptop closed with the diary open comes back to the lock screen. It works while you are writing, too, and saves the day before locking it away: the alternative would be a question sitting on the screen with the diary open behind it. A half-written entry holds the timer off rather than being thrown away by it. Change the wait, or turn it off, in settings.
Locking covers the diary you have walked away from. ctrl+r covers the one in front of you: every word goes under a bar except the one your cursor is touching, so you can keep writing on a train without the seat behind you reading the page. Press it again to lift the bars, and they stay lifted or lowered as you move between days.
It hides what you have already written, and that is the honest claim: the word you are typing is revealed as you type it, so someone determined to watch you write still can. It is also not a substitute for locking — the bars are on the screen, not on the diary. The text underneath is untouched, so what you save is what you wrote.
Keys
| Key | Where | Does |
|---|---|---|
| n | entry list | Write about today |
| c | entry list | The calendar: the whole year, written days marked |
| a | entry list | Write about another day (asks which) |
| enter | entry list | Open the selected day |
| d | entry list, calendar | Delete the selected day's entry (asks first) |
| r | entry list | Re-read the file, picking up another Zecret's writing |
| / | entry list | Search |
| s | entry list | Settings: theme, locking, master password |
| e | calendar | Write the day under the cursor (enter too) |
| [ / ] | calendar | The same day a year earlier / later |
| c | calendar | Back to the list |
| ctrl+l | everywhere once unlocked | Lock the diary without quitting (saves the day you are writing) |
| ? | entry list, calendar | Help — every key, on one page |
| q | entry list, calendar | Quit |
| ctrl+s | editor | Save, and carry on writing |
| ctrl+r | editor | Cover the writing, leaving only the word you are on |
| esc | anywhere | Back (offers to save first if you have unsaved edits) |
| ctrl+q | anywhere | Quit (offers to save first if you have unsaved edits) |
Getting around a long diary: j/k or the arrow keys move a day at a time, g/G (or home/end) jump to the newest and oldest entries, and PgUp/PgDn move a screenful. These stay out of the bar at the bottom, which only has room for so much — ? lists everything.
Inside a day, the editor answers to the usual text-editing keys, including ctrl+home/ctrl+end for the two ends of the entry, ctrl+↑/ctrl+↓ to move a paragraph at a time, ctrl+←/ctrl+→ to move a word at a time, and ctrl+a to select all of it. Holding shift as well takes the writing with you. Those are not listed under ?: it is a page about the diary, and they mean here what they mean in every other editor.
How it works
- Key derivation — Argon2id (
time_cost=3,memory_cost=64 MiB,parallelism=4, roughly OWASP interactive settings), with a random 16-byte salt generated per diary and stored in the file header. Your password is never stored; the derived key never touches disk. - Encryption — AES-256-GCM with a fresh random nonce for every encryption. Each day's entry is encrypted independently, so editing one day never re-encrypts the others. Only the dates are visible in the file; every word you write is inside the ciphertext.
- Integrity — tampering with a stored entry, or a wrong password, fails the AEAD authentication check and is reported as an error, never as an empty or partial diary. The header carries an encrypted verifier, so this holds even for a diary with no entries yet.
- Durability — saves are atomic (temp file →
fsync→os.replace), so an interrupted save can never leave a half-written diary. Creating a diary instead claims the path withO_EXCL, so two first runs racing each other cannot end with one written over the other; a creation that fails removes what it started. The file is created0600.
Plaintext is never written to disk — not as temp files, not as logs, not for crash recovery.
Security
The threat model, what counts as a vulnerability, and how to report one privately rather than in a public issue are all written down in SECURITY.md. Report through the repository's Report a vulnerability form; only the latest release is supported.
That file also writes down what an encrypted file on your own disk does not protect you from, because a diary that overstates itself is worse than one that does not try:
- The file reveals which days you wrote on. Dates sit outside the ciphertext so a day can be named without decrypting it. It leaks the shape of the habit, never a word of the content.
- An unlocked Zecret is unlocked. While the diary is open its entries are decrypted in memory, and anyone at that terminal can read them. Locking shortens that window; it does not close it.
- Secrets cannot be wiped from memory. Python strings are immutable, so the password and the derived key cannot be overwritten after use, and either may reach swap or a core dump. Full-disk encryption is the answer to that, not this program.
- Anyone who can already run code as you has won. A keylogger, an attached debugger, or a modified Zecret is outside what a local encrypted file can defend against.
Development
git clone https://github.com/kfurtak1024/zecret.git
cd zecret
uv sync # dev tools included (PEP 735 dependency group)
./zecret-dev.sh # run it against a throwaway diary
uv run pytest
uv run ruff check . && uv run ruff format .
uv run mypy
Note ./zecret-dev.sh rather than uv run zecret. Running the app from a
checkout opens your own diary with whatever code is currently checked
out, which is not what you want a half-finished change doing. The script
points the app at .zecret-dev/ instead — gitignored, throwaway, and
seeded on first run with a few hundred generated days so there is enough in
it to see a layout problem. The password is dev. Delete the directory
whenever you like; the next run rebuilds it.
It overrides the preferences file as well as the diary, which matters more than it sounds: without that, trying themes out in a development build changes the theme in the one you write in.
uv run python tools/seed_dev_diary.py --help # different data, same guardrails
The screenshots above are generated, not taken by hand — regenerate them after any change that alters what a screen looks like:
uv run python tools/screenshots.py # needs librsvg or inkscape
CI runs those same checks on every push and pull request, plus coverage and
the slow scale tests that a local uv run pytest skips. uv.lock is
committed and CI installs from it, so regenerate and commit it whenever
pyproject.toml changes.
The rules the code is held to — how the layers are allowed to depend on one another, the cryptographic requirements that are not up for negotiation, the on-disk format, and what a release involves — are written out in CLAUDE.md. CHANGELOG.md is what changed and when.
License
MIT © Krzysztof Furtak — see LICENSE.
Metadata
Release files for zecret 0.8.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zecret-0.8.2.tar.gz | 2.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zecret-0.8.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.4 MB
Release files / zecret-0.8.2.tar.gz
| Download URL | zecret-0.8.2.tar.gz |
|---|---|
| Size | 2.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cda7948841bc4d8957b4f68fec409f5a040e1bc4c8150e6728786766c0a187fa
|
|
BLAKE2b-256 checksum How to use checksums |
5953b8a18a58a64632e659a0a4ee99a131b05342ab04e4af8548c42e8deec691
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / zecret-0.8.2-py3-none-any.whl
| Download URL | zecret-0.8.2-py3-none-any.whl |
|---|---|
| Size | 131.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
479974492619fd80de60f27135e4a3b7bff033629879b284b8d929c8cb397571
|
|
BLAKE2b-256 checksum How to use checksums |
d12b6ab43d6af74a8d030f7f60f5aa51968ad1fe44ffdce166f391e968d501ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency log