If you’ve ever used the Unix pass password manager, you know it’s beautifully Unix-y: just gpg(1), the filesystem, and a thin bash wrapper. That same simplicity makes it tempting to expose to an AI agent — but the moment you do, you’re handing a non-deterministic LLM a shell-shaped foot-gun.
That’s why I built unix-pass-mcp — a typed, hardened Model Context Protocol server over the safe subset of pass. Read-only by default. Writes, key administration, and network ops behind explicit env opt-ins. No shell, no clipboard, no secrets in logs.
The Problem
You want your AI assistant to:
- Pull a credential into a browser MCP for a signup flow
- Generate a password for a new account and store it back
- Compute a TOTP code on demand
- Audit which entries exist without decrypting them
What you don’t want is the model deciding to overwrite half your store because a poisoned tool result told it to.
How It’s Gated
The server exposes 24 tools across four capability tiers, each with its own env flag:
- Read-only — always available.
store_info,list,find,show,show_field,show_metadata,grep,otp,otp_uri,unlock_agent. The expensive ones (grepdecrypts the whole store) require explicit confirmation. - Writes —
PASS_MCP_ALLOW_WRITES=1.insert,set_field,generate,mv,cp, and friends. Passwords go in via stdin only — never argv — so they don’t leak into process listings. - Git —
git_statusandgit_logalways;git_pull/git_pushneedPASS_MCP_ALLOW_NETWORK=1. Pulls are--ff-only. Pushes are never--force. - Destructive —
PASS_MCP_ALLOW_DESTRUCTIVE=1.initandreencrypt, which re-encrypt the store to a new recipient set. There is no undo.
Scope it further with PASS_MCP_ALLOWED_PATHS="work/*,personal/notes/*" and the agent literally cannot reach your banking entries — the allowlist is honoured by grep and unlock_agent too, so there are no scope escapes.
Security Posture
- No shell. Every
passinvocation goes through one chokepoint usingsubprocess.runwithshell=Falseand an arg list. Audited by aruff S404allowlist on a single file. - Strict input validation. Pass-names match a tight regex; no
.., no leading-, no control chars. - Stderr sanitization. Anything between
-----BEGIN/-----ENDmarkers is stripped before being raised back to the caller. - Sensitive output flagged.
show,generate,otp, andgrepcarrymeta.sensitive = trueso MCP hosts can refuse to log or cache them. - Append-only audit log. Records pass-names only — never values, never grep patterns.
- Strict startup. Refuses to start if
PASSWORD_STORE_UMASKis weaker than077or the store directory is world-readable.
The one real wart is pinentry — the MCP server can’t pop a passphrase prompt itself, since gpg would hang on stdin. So you either configure a GUI pinentry in ~/.gnupg/gpg-agent.conf, pre-warm the agent in a real terminal, or call the unlock_agent tool to bounce the prompt through zenity/kdialog. Either way, the LLM never sees the passphrase.
Honest Disclaimer
The whole point of this server is to grant an LLM access to your secrets. The capability gates, path allowlist, and audit log reduce the blast radius of mistakes and prompt injection — they don’t make it safe to point an unsupervised agent at a production credential store. With writes and network on, an agent that’s been prompt-injected by a hostile webpage in a sibling browser MCP can absolutely overwrite or exfiltrate credentials within scope.
I run mine read-only against a scoped subfolder, and only flip writes on when I’m actively running an agent through a signup flow.
Give It a Try
346 unit tests, 45 real-GPG integration tests, Python 3.11 / 3.12 / 3.13 in CI, MIT-licensed: GitHub – Neanderthal/unix-pass-mcp
If you’re already running an agent loop that combines a browser MCP with shell access, this is the bridge that lets you stop pasting passwords into chat.