← Back to all posts

pass: Your Terminal Is Already a Secrets Manager

Title card showing three encrypted files on a directory tree, the top one unlocked, with a GPG key alongside

TL;DR: pass is a password manager that is barely a program - every secret is one GPG-encrypted file, directories are namespaces, and the change history is plain git. It was written by Jason Donenfeld, the author of WireGuard. The genuinely useful move isn’t managing your Instagram password with it - it’s pointing PASSWORD_STORE_DIR at a directory inside a repo to get a per-project secret store that replaces .env. It’s a real hygiene upgrade. It is not, despite the hype, a security boundary against an AI agent - and this post is honest about where the seams are.


The other tools in this series - gh-dash, sesh and tmux, wt - are about staying in the terminal for things people usually leave it for. pass is the same idea aimed at secrets. And there’s a separate post on this blog, GitOps secrets, about the production end of the problem. This is the other end: the keys and tokens that live on your laptop while you work.

The hook is the author

The pitch that got me to actually try it: pass was written by Jason Donenfeld - zx2c4, the same security researcher who built WireGuard. When the person who gave us the VPN protocol that got merged into the Linux kernel writes a CLI for passwords, that’s a strong prior. He even hosts the project on his own git server. The tool is small on purpose, and knowing who made it small changes how you read that smallness - it’s a design decision, not an unfinished feature.

What pass actually is

The entire data model fits in one sentence: every password is a file on disk, encrypted with GPG. The directory structure is the namespace (business/work-email, personal/bank), and the change history is ordinary git. There is no server, no account, no subscription, no database format. It is a radical example of the Unix philosophy - one job, done with tools that already exist.

That means everything you already know applies. You can browse the store with ls, grep it, diff it, and if you pass git init, every git subcommand you know works against your secret history:

pass git log        # full history of every secret operation
pass git show HEAD   # what changed last

You can rebase your password history. That’s absurd in the best way, and it’s a direct consequence of not inventing a new storage format.

A short, honest detour through the crypto

Before the tool itself, one distinction the source admits its own author got wrong for years, because almost everyone does:

NameWhat it is
PGPPretty Good Privacy - the original 1990s product; today owned by Broadcom
OpenPGPthe open standard derived from PGP - just a spec, free to implement
GPGGNU Privacy Guard - the free implementation of that standard; the one you actually run

Practical version: you install gnupg, you call gpg, you use the OpenPGP standard. That -----BEGIN PGP SIGNATURE----- header in signed files refers to the standard, not to Broadcom’s product.

The one prerequisite for pass is GPG, so generating a keypair is step zero. A few things worth knowing while you do it:

  • It’s RSA at the maximum length in the demo, and there is no expiry by default - a default worth consciously confirming rather than clicking past.
  • GPG will ask you to move the mouse or hammer the keyboard while it generates. That isn’t theater. The whole value of a key is unpredictability; the OS harvests low-level events - keyboard, mouse, disk - to seed /dev/random. History has real attacks on key generators that leaned on predictable math instead of entropy.
  • Keys land in ~/.gnupg. gpg --list-secret-keys shows what you’ve got.

And two operations people constantly conflate: encrypting needs the recipient’s public key (you can’t encrypt to an address whose provider serves no public key - there’s nothing to encrypt with); signing proves authenticity and uses your private key. Useful flags: --armor for text output instead of binary (that’s where .asc files come from), --detach-sign for a signature file alongside the original, --verify to check one. It’s worth sitting with the fact that this is the same cryptographic primitive underneath Signal, WhatsApp and Telegram - sign for authenticity, encrypt to the recipient’s public key. GPG and an end-to-end messenger are built from the same block.

The CLI, which is deliberately small

pass init <your-gpg-id>     # create the store in ~/.password-store
pass git init               # start versioning it

pass insert business/aws     # add a secret (prompts, hidden input)
pass generate business/aws 24  # generate a 24-char password and store it
pass show business/aws       # print it
pass edit business/aws       # open it in $EDITOR
pass find aws                # search by name fragment
pass mv / pass rm            # move / remove
pass ls                      # tree view of the whole store

Two features I reach for constantly. pass show -c copies the secret to the clipboard and self-destructs it after 45 seconds - the moment you see it, you start imagining the scripts you’ll build around it. And pass find searches by name fragment, which is what you actually want when you can’t remember whether it was aws or amazon. There’s also a multiline mode for secrets that are more than a single password - a whole .pem, a block of connection settings.

The pattern that earns its place: a per-repo secret store

This is the part that changed how I work, and it has nothing to do with personal passwords.

“Secrets management” is really three different jobs wearing one name: personal passwords (Instagram, bank - they need a phone app, browser autofill, OTP), local project secrets (a dev database key, an API token - they need convenience and closeness to the repo), and production secrets (they need rotation, audit, access control, team sharing). The market answers with three tool classes - 1Password/Bitwarden, .env, and Vault/SOPS/cloud secret managers. The claim worth testing is that pass covers the first two at once - and the second one is, in practice, the worst-solved of the three.

The default answer for local secrets is a .env file, and its flaws are well known: it sits on disk in cleartext, one slip in .gitignore puts it in the repo history forever, it has no change history and no record of who set what, and it drifts between machines and teammates. The usual alternative - a real secret manager - gets rejected for a sensible reason: nobody wants to stand up Vault for local development. That gap is exactly what this pattern fills.

# inside the repo
export PASSWORD_STORE_DIR="$PWD/.secrets-store"
pass init <the same GPG key>     # a separate, empty store just for this project

To avoid exporting that variable by hand every session, put it in mise.toml (mise - “mise en place” - is a local environment manager that binds env vars to a project directory):

# mise.toml
[env]
PASSWORD_STORE_DIR = "{{config_root}}/.secrets-store"

Now in this repo, pass sees only this project’s secrets; in an adjacent shell split without the variable, you’re back to your main store. Fresh shell, pass show, empty. What you gain over .env: secrets encrypted at rest so an accidental commit isn’t a catastrophe, git history for free, project scoping, and one interface (pass) for both personal and project secrets.

Secrets and AI agents - where I part ways with the source

The most current claim in the source is that this per-repo pattern is “one of the safest ways for agents that need secrets.” An agent writing code usually needs keys - to a database, to an API - and the default is a cleartext .env sitting in the project directory. Replacing that with an encrypted, project-scoped store sounds like an obvious security win.

It’s a real improvement. But the claim is stronger than the demonstration, and the difference matters, so let me split it cleanly.

What you actually gain:

  • The agent working in the repo doesn’t stumble over a cleartext file.
  • Scope is limited to this project’s secrets.
  • Reading a secret is an explicit, observable action (pass show), not a silent file read.

What you do not gain:

  • If the agent can run commands, it can run pass show and see the cleartext, exactly as it would with .env.
  • Worse: the decrypted value goes to stdout, which means it lands in the model’s context and the session logs - a new leak surface that a .env read by the application itself never had.
  • The GPG key passphrase is cached by the gpg-agent, so the “extra password layer” usually doesn’t fire mid-session.

The honest conclusion: this is repo hygiene, not a security boundary against the agent. A real boundary would require that the agent never receive the value at all - injecting the secret into a child process the agent can’t inspect. If you take the pattern for what it is (no cleartext on disk, scoped, auditable reads), it’s excellent. If you take it as a sandbox that keeps keys away from the agent, it will let you down.

What pass doesn’t solve

Because it’s important to know where the tool stops:

  • Key rotation - changing your GPG key means re-encrypting the entire store.
  • Revocation - a secret someone decrypted once is decrypted forever.
  • Team sharing - this is precisely what you pay Vault and 1Password for.
  • Metadata - file and directory names are not encrypted, so the list of your accounts is in the clear. That matters directly if you follow the mobile setup and push the store to a private GitHub repo - the contents are encrypted, the service list is not.

So: for the solo case - personal passwords plus local project secrets - pass is a strong candidate. For team production secrets, you still need something else.

Honesty about the friction

The source is titled “the perfect password manager,” and its best quality is that the content immediately says “it’s not as easy as you think.” Concretely: the QtPass GUI wouldn’t even launch; the thing that worked was a Raycast extension that shows the whole store and pastes into the app underneath. Mobile takes real work - you push the store to a private git repo and hand the app a path, a branch and a token. Importing from a commercial manager works through extensions (pass import covers most services, including Apple Keychain), and when it doesn’t there are migration scripts - though your first secrets migration is a poor moment to improvise.

None of that is a knock. It’s the right expectation to set: pass is superb for the “I live in the terminal” profile, and a rough ride for anyone expecting the polish of a paid service.

Where it lands in my setup

Like the other tools in this series, it goes in declaratively - gnupg and pass in the package list, and the per-repo pattern lives in each project’s mise.toml rather than in global config, which keeps the blast radius per-project. The store itself is a git repo, so it backs up the same way everything else does.

For the production side of secrets - rotation, audit, sealed secrets in a cluster - that’s a different tool and a different post: GitOps secrets. pass is the laptop end. Start there, with a single project’s .env replaced, and see whether the terminal-native version fits how you already work.