Local encrypted password manager that lives in your terminal
Project description
pw-manager
A local, encrypted password manager that lives in your terminal. No browser, no cloud, no daemon — one encrypted file on disk and a small Python CLI in front of it.
$ python pw.py get github
master password:
username: dt@example.com
password copied to clipboard
Install
From PyPI (recommended — pipx keeps CLI tools isolated):
pipx install basic-password-manager # or: pip install basic-password-manager
pw init
From source:
git clone https://github.com/Daisentaur/password-manager && cd password-manager
pipx install .
For development: python3 -m venv venv && ./venv/bin/pip install -e .
Usage
Run pw with no arguments for the interactive TUI: unlock, then / to
filter, Enter to copy the selected password, n to add, e to edit,
d to delete, t to switch themes (persisted across runs), q to quit.
The search box matches every word of your query anywhere in an entry —
name, username, notes, and the password itself — with matches highlighted
in the theme's accent color (password matches show the row but highlight
nothing, since passwords are never displayed). Rarer operations live in the
command palette (ctrl+p): importing a browser CSV, changing the master
password.
The bundled themes — muted-slate (default), dawn, matrix — are borrowed with
admiration from tuxedo; Textual's
built-in themes are in the picker too.
Or use the subcommands:
| Command | What it does |
|---|---|
pw init |
create a new empty vault |
pw add github |
store an entry (prompts for username/password/notes) |
pw add github --gen |
same, but generates a strong random password |
pw get github |
copy the password to the clipboard, show the username |
pw edit github |
update an entry field by field (Enter keeps current) |
pw passwd |
change the master password |
pw ls |
list entry names |
pw rm github |
delete an entry |
pw find "dt bank" |
search names, usernames, notes — every word must match, any order |
pw gen -l 32 |
just print a random password |
pw import passwords.csv |
import a browser CSV export (see below) |
The vault lives at ~/.local/share/pw-manager/vault. Set the PW_VAULT
environment variable to put it somewhere else.
The notes field takes any text you want kept secret alongside the password —
recovery codes, PINs, the answer to "what was your first pet".
Moving off browser password storage
- Export: Chrome →
chrome://password-manager/settings→ "Export passwords". Firefox →about:logins→ ⋯ menu → "Export passwords". Both produce a CSV. pw import passwords.csv- Delete the CSV — it is every password you own in plaintext:
shred -u passwords.csv - Turn off saving and delete saved passwords in the browser settings.
How it works
Three layers, ~120 lines total. Read them in this order:
1. crypto.py — password → key, key → ciphertext
An AES key must be 32 unpredictable bytes; your master password is neither.
Argon2id bridges the gap: derive_key(password, salt) always returns the
same 32 bytes for the same inputs — that's what makes unlocking possible.
It is deliberately slow and memory-hungry (64 MiB per guess), so an attacker
who steals the vault file can't brute-force short passwords on a GPU the way
they could against a plain hash.
The salt is 16 random bytes stored unencrypted in the vault file. It's not a secret: its job is making your derived key unique so precomputed attack tables are useless.
Encryption is AES-256-GCM, which is authenticated: decrypting with the wrong key, or decrypting data that was modified by even one bit, fails loudly instead of returning garbage. Each encryption uses a fresh random 12-byte nonce (also stored unencrypted — also not a secret). The one iron rule of GCM is that a (key, nonce) pair must never repeat, which is why it's random every time.
2. vault.py — the file format
[4 bytes "PWV1"][16-byte salt][12-byte nonce][ciphertext...]
The ciphertext is just your entries as JSON, encrypted. Load = read →
derive key → decrypt → json.loads. Save = the reverse, with a fresh salt
and nonce, written to a temp file and atomically renamed so a crash mid-write
can't destroy the vault.
Your master password is never stored anywhere, in any form. "Wrong password" is detected purely by GCM authentication failing.
3. cli.py and tui.py — the interfaces
Both are thin skins over the same two modules — tui.py (built with
Textual) holds the decrypted entries in
memory for the session; the CLI re-derives the key per command.
argparse subcommands over the two modules above. Passwords are copied to the
clipboard (wl-copy/xclip/xsel, whichever exists) rather than printed,
so they don't sit in your terminal scrollback. CLI find searches names,
usernames, and notes — deliberately not passwords, so fragments of secrets
never land in your shell history; the TUI search box does cover passwords,
because nothing typed there is logged anywhere.
Backups
Every save automatically keeps the previous version as vault.bak next to
the vault — one-step undo if a save goes wrong or you delete the wrong entry.
That protects against mistakes, not against the disk dying. For that, the vault is one file: copy it anywhere — another disk, a USB stick, even somewhere untrusted, since it's useless without the master password.
cp ~/.local/share/pw-manager/vault /some/backup/location/
Troubleshooting
"I forgot the master password." The data is gone. Not "gone until support
resets it" — mathematically gone; that's the entire design. This is why you
keep the master password memorable (a long passphrase beats Tr0ub4dor&3)
and why backups protect you from file loss but not from forgetting.
"wrong master password (or the vault file is corrupted)" — 99% of the
time it's a typo'd password. If you're certain the password is right, the
file was damaged; restore the automatic backup
(cp ~/.local/share/pw-manager/vault.bak ~/.local/share/pw-manager/vault)
or an off-machine copy.
Deleted or overwrote an entry by mistake — the state before your last
save is in vault.bak; restore it as above. Only one generation is kept, so
do it before the next save.
"not a vault file (bad magic bytes)" — the file at the vault path isn't
one of ours (truncated, overwritten, or wrong path). Check echo $PW_VAULT
and restore from a backup.
"no vault at ..." — run pw init, or PW_VAULT points somewhere
unexpected.
Password gets printed instead of copied — no clipboard tool was found.
Install one: sudo apt install wl-clipboard (Wayland) or xclip (X11).
Unlock feels slow (~1s) — that's Argon2 doing its job; the delay is the brute-force resistance. It's per-command, not per-keystroke.
MemoryError from Argon2 — the KDF needs 64 MiB free; something is
eating your RAM.
Import brought in junk entries — browser CSVs include everything, even
ancient accounts. pw rm <name> the ones you don't want, or edit the CSV
before importing.
Honest limitations
- No clipboard auto-clear: the password stays on the clipboard until you copy over it. Copy something else when you're done.
- While a command runs, decrypted data briefly exists in process memory. Malware already on your machine could read it — true of every password manager; the vault protects the file at rest, not a compromised machine.
- Single file, no sync. Syncing is your problem (and
cpis a fine answer).
Running the checks
./venv/bin/python test_vault.py
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file basic_password_manager-0.2.2.tar.gz.
File metadata
- Download URL: basic_password_manager-0.2.2.tar.gz
- Upload date:
- Size: 13.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cef066e900bba9456b9585603648b04eda7caac624a2f0d0320e24125c3c81bb
|
|
| MD5 |
3712e76fe7686ba8bda445d6f96dd38c
|
|
| BLAKE2b-256 |
df3d30a077b031f1ab3e99810e3e584a091122e9bb75858eb84daeadd1db2a2b
|
Provenance
The following attestation bundles were made for basic_password_manager-0.2.2.tar.gz:
Publisher:
publish.yml on Daisentaur/password-manager
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
basic_password_manager-0.2.2.tar.gz -
Subject digest:
cef066e900bba9456b9585603648b04eda7caac624a2f0d0320e24125c3c81bb - Sigstore transparency entry: 2186833727
- Sigstore integration time:
-
Permalink:
Daisentaur/password-manager@3c077f8e47d09732529076be795992a3fe7459e6 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/Daisentaur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3c077f8e47d09732529076be795992a3fe7459e6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file basic_password_manager-0.2.2-py3-none-any.whl.
File metadata
- Download URL: basic_password_manager-0.2.2-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c236267a6d9d32dbd079ad28092c521b5ca84f3cf8811154b73f935f5ac78214
|
|
| MD5 |
2ac22779b1dca9de5d2c4b19c39cd945
|
|
| BLAKE2b-256 |
2e2721635cf409bb8cd1f735862a2aabb3bb3be434cb51e2d1b8fbddfa6ff527
|
Provenance
The following attestation bundles were made for basic_password_manager-0.2.2-py3-none-any.whl:
Publisher:
publish.yml on Daisentaur/password-manager
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
basic_password_manager-0.2.2-py3-none-any.whl -
Subject digest:
c236267a6d9d32dbd079ad28092c521b5ca84f3cf8811154b73f935f5ac78214 - Sigstore transparency entry: 2186833791
- Sigstore integration time:
-
Permalink:
Daisentaur/password-manager@3c077f8e47d09732529076be795992a3fe7459e6 -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/Daisentaur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3c077f8e47d09732529076be795992a3fe7459e6 -
Trigger Event:
push
-
Statement type: