most password managers ask for trust in two places at once: trust that the encryption is sound, and trust that the company holding the synced copy stays honest and online. the database format is usually proprietary, the sync runs through someone else’s servers, and the only way to read a password is through the application that wrote it.
pass takes the opposite position. each password is a single file encrypted with gpg, and the collection is a directory tree on disk. there is no database to corrupt and no service to depend on. because the store is just files, version control and synchronization are not features that have to be built in - they are git, pointed at the directory.
the short version
pass keeps one gpg-encrypted file per secret under ~/.password-store. the directory structure is the organization: a file at email/mail.kontrolplane.dev.gpg is the password named email/mail.kontrolplane.dev. retrieving a secret decrypts one file with the gpg key on the machine. because the store is a plain directory, pass git init turns it into a git repository, and every insert, edit, or removal becomes a commit. pushing that repository to a remote gives central storage without any sync service holding plaintext.
initializing the store
pass is built around a gpg key, so a key has to exist first. the key id - usually the email address it was created with - tells pass which public key to encrypt every secret against.
pass init levi@kontrolplane.dev
this writes a .gpg-id file into ~/.password-store containing the key id. every file pass encrypts from this point uses that key, and only the matching private key can decrypt them. the store is now functional but local.
turning it into a git repository is one command:
pass git init
pass runs git inside the store directory and creates the first commit. from here, every operation that changes the store - inserting a password, editing one, removing one - produces a commit automatically. there is no separate step to remember.
adding and reading secrets
inserting a secret prompts for the value and encrypts it into a file named by the path argument:
pass insert email/mail.kontrolplane.dev
this creates ~/.password-store/email/mail.kontrolplane.dev.gpg and commits it. the email/ prefix is just a directory; pass creates intermediate directories as needed, and the tree is the only organization the store has.
reading a secret decrypts the file to stdout:
pass email/mail.kontrolplane.dev
gpg asks for the key passphrase, decrypts the one file, and prints it. nothing else in the store is touched or decrypted. copying to the clipboard instead of printing - and clearing it after a timeout - is the -c flag:
pass -c email/mail.kontrolplane.dev
pass can also generate a value rather than prompt for one, writing it straight into a new encrypted file:
pass generate email/mail.kontrolplane.dev 32
the file as the data model
the convention is that the first line of the decrypted file is the password, and everything after it is free-form text. this means a secret is not limited to a single value - a login can carry its username, recovery codes, and a url in the same file.
correct-horse-battery-staple
username: levi@kontrolplane.dev
url: https://mail.kontrolplane.dev
recovery: 8f3a-92bc-44de
pass email/mail.kontrolplane.dev prints the whole file; tools built on top read the first line as the password and parse the rest as metadata. past is one such tool, a terminal ui for browsing the store and copying secrets without printing them to the terminal. because the format is just text inside a gpg envelope, there is no schema to migrate and nothing stops a plain gpg -d from reading a file if pass itself is unavailable.
git as central storage
the store being a git repository is what makes synchronization a solved problem. adding a remote and pushing is ordinary git, run through pass so it operates in the store directory:
pass git remote add origin git@github.com:levi/password-store.git
pass git push -u origin main
what lands on the remote is only ever ciphertext. the .gpg files are encrypted against the gpg key, and the remote - github, a private gitea, a bare repository on a server - never sees a plaintext password or the gpg private key. this is the property that makes hosting the store on infrastructure outside your control acceptable: compromising the remote yields encrypted files and a commit history, not secrets.
on a second machine, the setup mirrors a normal clone. the gpg private key has to be present first, then the store is cloned into place:
git clone git@github.com:levi/password-store.git ~/.password-store
from there pass git pull and pass git push keep machines in sync, and every change is an auditable commit with a message describing what pass did.
sharing with multiple keys
a store is not limited to one recipient. pass init accepts several key ids, and pass then encrypts every secret to all of them, so any of the corresponding private keys can decrypt.
pass init levi@kontrolplane.dev info@kontrolplane.dev
this rewrites the .gpg-id file and re-encrypts the existing secrets against the new set of keys. a subdirectory can carry its own .gpg-id with a different recipient list, so a shared/ tree can be readable by a team while the rest of the store stays personal. adding or removing a recipient is a re-encryption of the affected files, committed like any other change.
what to watch out for
the gpg key is the single point of failure. lose the private key and every secret in the store is unreadable - git history included. the key and its revocation certificate need their own backup, kept separately from the password store, because the store cannot recover them.
git history retains deleted secrets. removing a password commits the deletion, but the encrypted blob stays in the history. rotating a leaked credential matters more than scrubbing history, but if a secret must be purged entirely, the repository has to be rewritten with a tool like git filter-repo and force-pushed.
filenames and structure are not encrypted. gpg encrypts file contents, not paths. anyone with access to the repository can see that banking/bank.kontrolplane.dev and email/mail.kontrolplane.dev exist, even without being able to read them. the directory layout leaks which accounts exist.
adding a recipient does not re-encrypt automatically in every case. changing .gpg-id re-encrypts existing secrets, but a recipient added through a manual edit, or secrets inserted while a key was missing from the list, can end up encrypted to the wrong set. pass init with the full intended key list is the reliable way to reconcile the store.
clipboard and terminal leak plaintext. pass prints to stdout and pass -c puts the value on the clipboard. shell history, terminal scrollback, and clipboard managers can all retain it. prefer -c, which clears the clipboard after a timeout, and be aware of what else is watching the clipboard.
references
[1] pass documentation. “pass: the standard unix password manager.”
passwordstore.org
[2] pass manual. “pass(1) man page.”
git.zx2c4.com/password-store/about
[4] git documentation. “git-filter-repo.”
git-scm.com/docs/git-filter-repo
[5] kontrolplane. “past - pass + tui.”
kontrolplane.dev/projects#past